动手开发自己的mvc-2----完善控制层,提供自动注入和注解上传等功能

  当表单提交的内容过多 ,让懒惰的程序员一个个getParameter()是很让人抓狂的,所以自动注入表单域是mvc不可或缺的功能,另外,文件上传也是一个特殊的表单域,你想看到程序员发觉上传只需要注入就能完成功能时的那种欣喜吗  ? 我们一起做做看。
    我们依然从最简单的开始做,慢慢的润色。
    注入表单的思路比较简单:
    1,在form里面的name需要设置成诸如userinfo.username这类的,userinfo表示注入的目标对象,username表示userinfo对象的属性。这个对象必须是Action里面声明的
    2,MainServlet在接收表单时,从getParameterMap()得到所有表单域,拆分出目标对象和属性,通过反射执行set方法
  
注意:由于每个请求都会产生一个Action的新实例,所以在Action类的属性不会被多个请求共享,是线程安全的。

实现方式如下:
1,打开MainServlet,首先声明
Map<String,Object[]> paramMap = request.getParameterMap();

//此map对象用来缓存单页面的目标注入对象,比如此页面有多个Userinfo的属性需要注入,不可能每次注入都要生成Userinfo对象,肯定得在同一个对象中注入(小细节)
Map<String, Object> fieldMap = new HashMap<String, Object>();

得到请求信息后进行迭代
Set<Entry<String,Object[]>> paramSet = paramMap.entrySet();
for (Entry<String,Object[]> ent : paramSet) {
                   String paramName = (String) ent.getKey();
                   
                   Object[] paramValue = ent.getValue();
                   
                   handField(fieldMap,paramName,paramValue,action);
}


handField方法用来处理注入功能。
方法体和详细注释如下:
//.这个字符是不能直接用正则的,需要转义
          String[] paramVos = paramName.split("\\.");
          //这里只支持 对象.属性的表单注入,对于多级的大家可以自行实现,相信不是难事儿。
           if (paramVos. length == 2) {
              Class actionClass = action.getClass();
              Object fieldObj = fieldMap.get(paramVos[0]);
              //从你的action得到目标注入对象
              Field field  = actionClass.getDeclaredField(paramVos[0]);;
               if (fieldObj == null) {
                   //假如是第一次注入,为空,则实例化目标对象
                   Class fieldClass = field.getType();
                   fieldObj = fieldClass.newInstance();
                   //放入缓存,第二次直接从缓存取,保证同一个form注入的是同一个对象 
                   fieldMap.put(paramVos[0], fieldObj);
            }
              //构造目标属性的set方法 
              String setMethod = "set"
                        + paramVos[1].substring(0, 1).toUpperCase()
                        + paramVos[1].substring(1);
              Field fieldField = null;
              fieldField = fieldObj.getClass().getDeclaredField(
                        paramVos[1]);

              
               if(realValue!= null){
                   InvocakeHelp. invokeMethod(fieldObj, setMethod,
                              new Object[] { paramValue }); 
              }
              

          }


到此,基本的注入功能就有了,测试一下.
我们编写一个Userinfo对象,属性为username,email等,并在TestAction里声明Userinfo对象,名为user。
然后form表单里写好表单
<input type="text" name="user.username" />
<input type="text" name="user.email" />
提交表单,发觉已经被注入了,测试成功。
但是,这里只是测试的字符串类的表单域,假如Userinfo里面还有个age属性,Integer型的,该怎么办呢?Integer类型的接收String型的肯定是不行的。
有人说,那就在MainServlet直接判断呗,直接得到属性type,判断假如是Integer,就Integer.parseInt一下。
但是假如是double类型的呢?假如是更多类型的呢,假如是自定义的类型呢? 所以这里显然得做成可扩展的。
回想一下直接判断是个怎样的过程:
1,获取注入属性的类型(type)
2,根据不同类型,用不同方式转换值,比如Java.lang.Integer对应parseInt,Double对应parseDouble。
3,注入转换后的值
假如做成可扩展的,那么转换的类型是程序员自定义的,另外根据不同类型,配置不同的转换器,然后让MainServlet读取并自动转换。
那么我们的配置看起来应该是这样的:
 <converter type= "java.lang.Integer" handle= "org.love.converter.IntegerConverter" >
     </converter >


让IntegerConverter实现你设定好的接口,让MainServlet统一接口调用。
接口代码如下:
public interface TypeConverter {
     
     /**
      *
      * @param value 将要转换的值
      * @param field 将要转换的属性 元数据包含了更多的信息
      * @return 得到被转换后的对象
      */
     public Object convertValue(Object value, Field field);
}


IntegerConverter实现代码如下:
public class IntegerConverter implements TypeConverter {

     public Object convertValue(Object value,Field field) {
          
           if(value== null || value.equals( "")){
               return null;
          }
          //假如这里是数组,那么组装Integer数组并返回
           if(field.getType().isArray()){
              String[] intStr=(String[])value;
              Integer[] returnInt= new Integer[intStr.length];
               for( int i=0;i<intStr. length;i++){
                    if(intStr[i]!= null&&!(intStr[i].trim().equals( ""))){
                        returnInt[i]=Integer. parseInt(intStr[i]);  
                   }
                   
              }
               return returnInt;
          } else{
               return Integer. parseInt(value.toString());        
          }
          
          
     }

}


,然后打开ControlXML类,加上
private Map<String,TypeConverter> convertMap= new HashMap<String, TypeConverter>();

读取 converter元素并装配到convertMap.(详细代码就不贴了,可以对照源代码看)
现在可以在MainServlet里调用了,首先获得注入属性的类型
//得到被反射的属性的类别名称,假如是数组,也返回原始类型
String fieldTypeClassName = (fieldField.getType().isArray()?fieldField.getType().getComponentType().getName():fieldField.getType().getName());

这句代码比较长,我们没有简单的getType().getName(),原因是,假如注入属性的类别是Integer数组(或者其他类别数组),用getType()是得不到原始类别的(因为数组也是个特殊的类别),所以这里判断假如是数组就得到原始类别,方便后面做判断。
//接收原始值 
 Object realValue =paramValue;


/*
               * 假如传递过来的是字符串数组(比如多选)并且被注入的元素类别是非数组
               * 那么会被以逗号分割拼接,假如是数组就不用处理,直接用数组接收
              */
               if(paramValue instanceof String[]){                 
                  if(!fieldField.getType().isArray()){
                         realValue = Utils.join((String[])paramValue,","); 
                   }    
              }

//最后调用
if (realValue!=null&&convertMap.containsKey(fieldTypeClassName)) {
                   realValue = convertMap.get(fieldTypeClassName)
                             .convertValue(realValue, fieldField);
              }


realValue就是转换后的值,反射注入搞定!

接口的设计为程序的扩展提供无限可能,我们趁热打铁,假如注入属性是Date类型呢?很好办。
创建DateConverter类实现TypeConverter,并且在control.xml里配置
<converter type ="java.util.Date" handle= "org.love.converter.DateConverter">
</converter >

实现过程也比较简单,想必都会,我在这里写了个稍微功能强一点的Date转换器,代码如下,仅供参考:
public class DateConverter implements TypeConverter {

     private static final String[] FORMAT = {
           "HH",  // 2
           "yyyy", // 4
           "HH:mm", // 5
           "yyyy-MM", // 7
           "HH:mm:ss", // 8
           "yyyy-MM-dd", // 10
           "yyyy-MM-dd HH", // 13
           "yyyy-MM-dd HH:mm", // 16
           "yyyy-MM-dd HH:mm:ss" // 19
     }; 
     
     private static final DateFormat[] ACCEPT_DATE_FORMATS = new DateFormat[FORMAT.length]; //支持转换的日期格式  
     
     static {      
           for( int i=0; i< FORMAT. length; i++){
               ACCEPT_DATE_FORMATS[i] = new SimpleDateFormat(FORMAT[i]);
          }
     }
     
     public Object convertValue(Object value,Field field) {   
       
           if(value== null||value.equals( "")){
               return null;
          }
        //String[] params = (String[])value;   
        String dateString = (String)value; //获取日期的字符串
        int len = dateString != null ? dateString.length() : 0;
        int index   = -1;
       
        if (len > 0) {
               for ( int i = 0; i < FORMAT. length; i++) {
                    if (len == FORMAT[i].length()) {
                         index = i;
                   }
              }
          }
      
       
        if(index >= 0){
           try {
                    return ACCEPT_DATE_FORMATS[index].parse(dateString);
              } catch (ParseException e) {      
                    return null;
              }
        }
        return null;   
    }

}


也就是说支持数组中的各种时间格式。


设计程序时需要不断调整和重构,当我发现这两个转换器属于程序必备品,为什么还需要每次配置在control.xml里面呢?所以干掉这两个converter配置
,直接在ControlXML中加入代码:
convertMap .put("java.util.Date" , new DateConverter());
convertMap.put("java.lang.Integer" , new IntegerConverter());

到目前位置,C层的开发貌似已经有模有样了,可以放心大胆的测试了。

我们回到本章的开头,假设一个表单里面不仅有文本,还有文件上传,那么用这个框架肯定是搞不定的,因为你没法同时接收到文本数据和二进制流,而上传实在是一个烂大街的功能,所以我们必须搞定它。
用过struts2的都知道它是通过common-fileupload组件接收数据流,产生临时文件,然后绑定到注入的File属性,程序员只需要copy一下到自己的路径就行了,这里我不打算按照这个方式实现,
直接通过注解配置路径,自动上传。在这里依然要用到common-fileupload组件,它依赖common-io.jar。
当form上传文件时必须设置enctype="multipart/form-data",我们在MainServlet通过request的contentType来判断是否二进制请求流,假如为true,
则通过common-fileupload得到所有文件和文本对象,很自然而然的,代码就成了这样:
Map<String,Object[]> paramMap = new HashMap<String,Object[]>();
          String contentType=request.getContentType();
          
           //假如是带有上传,那么利用common fileupload封装表单
           if(contentType!= null && contentType.startsWith("multipart/form-data" )){
             //文件项工厂
              FileItemFactory factory = new DiskFileItemFactory();
              ServletFileUpload upload = new ServletFileUpload(factory);
              //得到所有表单项
              List<FileItem> items = upload.parseRequest(request);
              ...
           paramMap=fileParamMap.put(表单域名,FileItem[])
               
          }else{
           paramMap=request.getParameterMap(); 
        }


注意,这里的items不光是上传的文件数据,也包含文本数据。
现在paramMap里不仅装有文本数据,也有上传的文件流数据,下一步可以根据类别的不同做不同的注入了。但是这里有个问题,你仍然没有办法在自己编写的action中用
getParameter()或者getParameterMap()的方式得到提交的表单信息,所以这里需要改造一下。
     Java Web中提供一种装饰模式,让HttpServletRequest请求被处理之前改造自身。
    首先创建一个MulRequestWraper,继承HttpServletRequestWrapper,并且需要提供带HttpServletRequest参数的构造方法,然后重写一些重要的方法,代码如下:
public class MulRequestWraper extends HttpServletRequestWrapper {

     private Map<String, Object[]> paramMap = new HashMap<String, Object[]>();

     public MulRequestWraper(HttpServletRequest request) {
           super(request);

           try {
              FileItemFactory factory = new DiskFileItemFactory();
              ServletFileUpload upload = new ServletFileUpload(factory);
              List<FileItem> items = upload.parseRequest(request);
              Iterator<FileItem> iter = items.iterator();
            String  encoding=(request.getCharacterEncoding()==null ?"UTF-8" :request.getCharacterEncoding());
               while (iter.hasNext()) {
                   FileItem item = (FileItem) iter.next();

                   String fieldName = item.getFieldName();
                    if ( paramMap.containsKey(fieldName)) {
                        Object[] paramValue = paramMap.get(fieldName);

                         // 构造同类数组
                        Object[] paramValueTemp = (Object[]) Array.newInstance(
                                  paramValue[0].getClass(), paramValue. length + 1);
                         for ( int i = 0; i < paramValue.length; i++) {
                             paramValueTemp[i] = paramValue[i];
                        }

                         if (item.isFormField()) {
                             paramValueTemp[paramValueTemp. length - 1] = item
                                       .getString(encoding);
                        } else {
                              if (item.getSize() > 0) {
                                  paramValueTemp[paramValueTemp. length - 1] = item;
                             }
                        }

                         paramMap.put(fieldName, paramValueTemp);
                   } else {
                         if (item.isFormField()) {
                              paramMap.put(fieldName,
                                       new String[] { item.getString(encoding) });
                        } else {
                              if (item.getSize() > 0) {
                                   paramMap.put(fieldName, new FileItem[] { item });
                             }
                        }

                   }

              }
          } catch (Exception e) {
              e.printStackTrace();
          }

     }

     public String getParameter (String name) {
          Object[] values= paramMap.get(name);
           if(values. length>0){
               return (String)values[0];
          }
           return super.getParameter(name);
     }

     public String[] getParameterValues (String name) {
          Object[] values= paramMap.get(name);
           if(values!= null){
               return (String[])values;
          }
           return super.getParameterValues(name);
     }
     
     
     
     public Map getParameterMap () {
           paramMap.putAll( super.getParameterMap());
           return paramMap;
     }



因为HttpServletRequestWrapper类是实现HttpServletRequest接口的,所以可以在外面直接接收,外面的判断代码可以改造成如下:
//假如是带有上传,那么利用common fileupload封装表单
           if(contentType!= null && contentType.startsWith("multipart/form-data" )){
              request= new MulRequestWraper(request);
          }
          
          paramMap=request.getParameterMap();


逻辑更加清晰了,并且在后面的Action里都可以得到所有的表单信息(二进制的或者文本的)。

jdk5引入的注解不光可以简化各种配置信息,也逐渐成为了程序功能的一个部分
用注解实现上传配置的大致流程如下:
1,框架提供文件信息的Bean类:FilePo
2,自定义注解,(Field类型)
3,MainServlet通过转换FilePo,读取注解信息,实现上传
FilePo主要属性有:
      // 文件名 带后缀
     private String filename;

     // 相对于web的路径
     private String webpath;

     // 实际文件
     private File file;

     // 类型
     private String contentType;
     
     //大小 kb
     private double size;


,自定义注解的关键字是:@interface,代码如下
/**
 * 自动上传,这个注解用在FilePo对象上
 * @author 杜云飞
 *
 */
@Retention(RetentionPolicy.RUNTIME)
//表示本注解用在属性上
@Target(ElementType.FIELD )
public @interface UploadFile {
     
     /**
      * 上传的路径,默认上传到根目录
      */
   public String path() default "";
  
   /**
    * 文件名 为""表示用原文件名
    */
   public String name() default "";
}


暂时只用这几个属性,其实还可以加上传限制或者后缀等。
还记得之前我们做的TypeConverter么,现在咱们需要一个拦截FilePo的转换器,
新建FileConverter,继承TypeConverter,读取Field上的注解并且获取path和name信息,通过common-fileupload上传,就这么简单
具体实现如下:
public Object convertValue(Object value, Field field) {
           if(!(value instanceof FileItem) && !(value instanceof FileItem[])){
               return null;
          }
           UploadFile uf = field.getAnnotation(UploadFile.class );
          HttpServletRequest request = ActionContext.getRequest();
           try {

               if (uf != null) {
                   String path = uf.path();
                    logger.debug( "path: " + path);
                   //path =Utils.resolvePlaceHolder(path , ActionReplaceHolder.getInstance());
                   String name = uf.name();
                   String realpath = request.getRealPath(path);
                    logger.debug( "realpath: " + realpath);
                   File dsk = new File(realpath);
                    if (!dsk.exists()) {
                        dsk.mkdirs();
                   }
                   
                   FilePo[] fps= new FilePo[0];
                    if (field.getType().isArray()) {
                        
                         // 假如是数组,那么保存在同一个注释的文件夹里面,并且文件名为源文件的名字
                        FileItem[] fis = (FileItem[]) value;
                         for ( int i = 0; i < fis. length; i++) {
                             FileItem item = fis[i];
                              long filesize=item.getSize();
                             String filename = item.getName();
                              if(filename.indexOf(File. separator)>=0){
                                  filename=filename.substring(filename.lastIndexOf(File. separator)+1);
                             }
                              logger.debug( "filesize: "+filesize);
                             File file = new File(realpath + File.separator
                                      + filename);
                             
                             item.write(file);
                       
                             FilePo[] fps_temp=fps;
                             fps= new FilePo[fps.length+1];
                              for( int j=0;j<fps_temp.length;j++){
                                  fps[j]=fps_temp[j];
                             }
                             FilePo fp= new FilePo();
                             fp.setFile(file);
                             fp.setFilename(filename);
                             fp.setWebpath(path+filename);
                             fp.setContentType(item.getContentType());
                             fp.setSize(filesize/1024);
                             fps[fps. length-1]=fp;
                        }
                         if(fps!= null && fps. length>0){
                              return fps;
                        }
                         return null;
                  
                   } else {
                        FileItem[] items=(FileItem[])value;
                        FileItem item=items[0];
                         long filesize=item.getSize();
                         if(filesize==0){
                              return null;
                        }
                        String filename = item.getName();
                         if(!name.equals( "")){
                             String exts = filename.substring(filename
                                      .lastIndexOf( "."));
                             filename=name+exts ;
                        } else{
                             filename=filename.substring(filename.lastIndexOf(File. separator)+1);
                        }
                        File file = new File(realpath + File.separator
                                  + filename);
                        item.write(file);
                        FilePo fp= new FilePo();
                        fp.setFile(file);
                        fp.setFilename(filename);
                        fp.setWebpath(path+filename);
                        fp.setContentType(item.getContentType());
                        fp.setSize(filesize/1024);
                         return fp;
                   }
              } else {
                    // 没有注解的暂时不作任何处理
                    return null;
              }
              
          } catch (Exception ex) {
              ex.printStackTrace();
               logger.error( "上传模块出现错误:" +ex.getMessage());
          }

           return null;
     }


有时候我们希望path路径是从上下文资源里面取到的,而不是“死”的,比如从${requestScope.xxx},${sessionScope.xxx}或者${param.xxx}等地方获取需要的路径信息,注释的那段代码正是此功能的实现。
path=Utils.resolvePlaceHolder(path, ActionReplaceHolder.getInstance());(策略模式的运用)
ActionReplaceHolder是一个单例类,实现了ReplaceHolder接口,用于产生替换值,核心代码如下:
public String extract(String value) {
           if(value.indexOf( "requestScope.")!=-1){
              String prop=value.substring(value.indexOf("requestScope." )+"requestScope." .length());
               return (String)ActionContext.getRequest().getAttribute(prop);
          } else if(value.indexOf( "sessionScope.")!=-1){
              String prop=value.substring(value.indexOf("sessionScope." )+"sessionScope." .length());
               return (String)ActionContext.getRequest().getSession().getAttribute(prop);
          } else if(value.indexOf( "param.")!=-1){
              String prop=value.substring(value.indexOf("param." )+"param." .length());
               return ActionContext.getRequest().getParameter(prop);
          }
           return value;
     }


resolvePlaceHolder方法用于解析${}这类占位符,参考实现如下:
/**
      * 解析占位符具体操作
      * @param property
      * @return
      */
     public static String resolvePlaceHolder(String property,ReplaceHolder rh) {
           if ( property.indexOf( PLACEHOLDER_START ) < 0 ) {
               return property;
          }
          StringBuffer buff = new StringBuffer();
           char[] chars = property.toCharArray();
           for ( int pos = 0; pos < chars. length; pos++ ) {
               if ( chars[pos] == '$' ) {
                    if ( chars[pos+1] == '{' ) {
                        String propertyName = "";
                         int x = pos + 2;
                         for (  ; x < chars. length && chars[x] != '}'; x++ ) {
                             propertyName += chars[x];
                              if ( x == chars. length - 1 ) {
                                   throw new IllegalArgumentException( "unmatched placeholder start [" + property + "]" );
                             }
                        }
                        String systemProperty = rh.extract( propertyName );
                        buff.append( systemProperty == null ? "" : systemProperty );
                        pos = x + 1;
                         if ( pos >= chars. length ) {
                              break;
                        }
                   }
              }
              buff.append( chars[pos] );
          }
          String rtn = buff.toString();
           return isEmpty( rtn ) ? null : rtn;
     }


这也是hibernate中的标准解析方式。
详细可以参考本项目源码。
最后记得注册这个转换器,
convertMap .put( "org.love.po.FilePo", new FileConverter());

那么以后上传就可以直接注解FilePo属性就行了
 @UploadFile(path="uploadfiles/${param.folderPath}/" )
    private FilePo[] myimg;
   
    @UploadFile(path="uploadfiles/${sessionScope.folderPath}/" )
    private FilePo myimg0;
   
    @UploadFile(path="uploadfiles/xxx/")
    private FilePo myimg1;



当然,假如有人希望不用注解上传,也依然可以在Action里得到上传信息(因为之前request已经被包装过了,已经存有此类对象。)
比如你可以这样做:
FileItem[] fis = (FileItem[]) request.getParameterMap().get("myimg");
然后通过api自己实现上传。
是不是灰常方便啊。
到现在为止,我们控制层的大部分功能都已实现,但毕竟是一个演示项目,有很多代码需要优化,功能也有很多需要完善并且改进的地方,不过我觉得,只要思路清晰,整体框架没有偏离基准,添砖加瓦是很容易的事情。
下一章开始讲解业务逻辑容器。


By 阿飞哥 转载请说明
腾讯微博: http://t.qq.com/duyunfeiRoom
新浪微博: http://weibo.com/u/1766094735
原文地址: http://duyunfei.iteye.com/blog/1773715

猜你喜欢

转载自duyunfei.iteye.com/blog/1773715