欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

动手开发自己的mvc-2----完善控制层,提供自动注入和注解上传等功能 博客分类: 实现自己的MVCjava综合 java框架xmlmvc 

程序员文章站 2024-03-22 10:41:28
...
  当表单提交的内容过多 ,让懒惰的程序员一个个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