文件上传的整个流程

文件上传的整个流程:

第一阶段: 构造struts2中针对请求字节流而构造的封闭类MultiPartRequestWrapper

1.FilterDispatcher

在doFilter方法中调用了prepareDispatcherAndWrapRequest方法,为了包装出Struts2自己的request对 象,在prepareDispatcherAndWrapRequest方法中调用Dispatcher类的wrapRequest方法,在这个方法里, 会根据请求内容的类型(提交的是文本的,还是multipart/form-data格式),决定是使用tomcat的 HttpServletRequestWrapper类分离出请求中的数据,还是使用Struts2的MultiPartRequestWrapper来 分离请求中的数据。

注:向服务器请求时,数据是以流的形式向服务器提交,内容是一些有规则东东,我们平时在jsp中用request内置对象取parameter时,实际上是由tomcat的HttpServletRequestWrapper类分解好了的,无需我们再分解这些东西了。


当然,在这里,我们研究的是上传文件的情况,所以,由于form中设定的提交内容是媒体格式的,所以,Dispatcher类的wrapRequest方法会将请求交由MultiPartRequestWrapper类来处理。


MultiPartRequestWrapper这个类是Struts2的类,并且继承了tomcat的 HttpServletRequestWrapper类,也是我们将用来代替HttpServletRequest这个类的类,看名字也知道,是对多媒体 请求的包装类。

Struts2本身当然不会再造个轮子,来解析请求,而是交由Apache的commons-fileupload组件来解析了。

在MultiPartRequestWrapper的构造方法中,会调用MultiPartRequest(默认为JakartaMultiPartRequest类)的parse方法来解析请求。

注:MultiPartRequestWrapper最终的一些操作比如getParameter都是通过调用JakartaMultiPartRequest相应的方法完成的.所以,MultiPartRequestWrapper,JakartaMultiPartRequest都看成是request. 只是有个委托的关系.

在Struts2的JakartaMultiPartRequest类的parse方法中才会真正来调用commons-fileupload组件的 ServletFileUpload类对请求进行解析,至此,Struts2已经实现了将请求转交commons-fileupload组件对请求解析的 全过程

剩下的就是等commons-fileupload组件对请求解析完毕后,拿到分解后的数据,根据field名,依次将分解后的field名和值放到 params(HashMap类型)里,同时JakartaMultiPartRequest类重置了HttpServletRequest的好多方法, 比如熟知的getParameter、getParameterNames、getParameterValues,实际上都是从解析后得到的那个 params对象里拿数据,在这个过程,commons-fileupload组件也乖乖的把上传的文件分析好 了,JakartaMultiPartRequest也毫不客气的把分解后的文件一个一个的放到了files(HashMap类型)中,实际上此 时,commons-fileupload组件已经所有要上传的文件上传完了。

至此,Struts2实现了对HttpServletRequest类的包装,当回到MultiPartRequestWrapper类后,再取一下上述解析过程中发生的错误,然后把错误加到了自己的errors列表中了。

同样我们会发现在MultiPartRequestWrapper类中,也把HttpServletRequest类的好多方法重载了,毕竟是个包装类 嘛,实际上对于上传文件的请求,在Struts2后期的处理中用到的request都是MultiPartRequestWrapper类对象,比如我们 调用getParameter时,直接调用的是MultiPartRequestWrapper的getParameter方法,间接调的是 JakartaMultiPartRequest类对象的getParameter方法。

第二阶段: 执行inteceptor及action.

doFilter方法中,会进一步调用actionMapper的getMapping方法对url进行解析,找出命名空间和action名等,以备后面根据配置文件调用相应的拦截器和action使用。

关于doFilter方法中下一步对Dispatcher类的serviceAction方法的调用,不再描述,总之在action被调用之前,会首先走 到fileUpload拦截器(对应的是FileUploadInterceptor类),在这个拦截器中,会先看一下request是不是 MultiPartRequestWrapper,如果不是,就说明不是上传文件用的request,fildUpload拦截器会直接将控制权交给下一 个拦截器;如果是,就会把request对象强转为MultiPartRequestWrapper对象,然后调用hasErrors方法,看看有没有上 传时候产生的错误,有的话,就直接加到了Action的错误(Action级别的)中了。

另外,在fileUpload拦截器中会将MultiPartRequestWrapper对象中放置的文件全取出来,把文件、文件名、文件类型取出来, 放到request的parameters中,这样到了params拦截器时,就可以轻松的将这些内容注入到Action中了,这也就是为什么 fileUpload拦截器需要放在params拦截器前面的理由。在文件都放到request的parameters对象里之后,fileUpload 拦截器会继续调用其他拦截器直到Action等执行完毕,他还要做一个扫尾的工作:把临时文件夹中的文件删除(这些文件是由commons- fileupload组件上传的,供你在自己的Action中将文件copy到指定的目录下,当action执行完了后,这些临时文件当然就没用了)。

详细代码跟踪:

Dispatcher:

    public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
        // don't wrap more than once
        if (request instanceof StrutsRequestWrapper) {
            return request;
        }

        String content_type = request.getContentType();
    //根据表单的提交类型转换成MultiPartRequestWrapper.
        if (content_type != null && content_type.indexOf("multipart/form-data") != -1) {
            MultiPartRequest multi = getContainer().getInstance(MultiPartRequest.class);
            request = new MultiPartRequestWrapper(multi, request, getSaveDir(servletContext));
        } else {
            request = new StrutsRequestWrapper(request);
        }

        return request;
    }

->
 //建立 MultiPartRequestWrapper 时解析(parse) request:
 public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request, String saveDir) {
        super(request);
        //JakartaMultiPartRequest
        multi = multiPartRequest;
        try {
            //parse的过程中,会判断是否超过struts.multipart.maxSize,感觉stack里面的parameter={}是在parse有错误的时候清空的.
            multi.parse(request, saveDir);
            //如果上传的文件超过struts.multipart.maxSize,会在这里加一个错误.
        for (Object o : multi.getErrors()) {
                //错误信息:the request was rejected because its size (12555091) exceeds the configured maximum (5242880)
                String error = (String) o;
                addError(error);
            }
        } catch (IOException e) {
            addError("Cannot parse request: "+e.toString());
        }
    }

附:
//JakartaMultiPartRequest实现自MultiPartRequest接口. 如果想替换JakartaMultiPartRequest,我们要做的也是实现该接口.
在JakartaMultiPartRequest的parse方法里有下面的代码片段:

public void parse(HttpServletRequest servletRequest, String saveDir)
            throws IOException {
        DiskFileItemFactory fac = new DiskFileItemFactory();
        // Make sure that the data is written to file
        fac.setSizeThreshold(0);
        if (saveDir != null) {
            fac.setRepository(new File(saveDir));
        }

        // Parse the request
        try {
        //这里是parse的核心代码.将parse的又委托给common-fileupload里的ServletFileUpload,ServletFileUpload extends        

         //FileUpload,FileUpload extends FileUploadBase, 最终是调用了FileUploadBase的parseRequest

            ServletFileUpload upload = new ServletFileUpload(fac);
            upload.setSizeMax(maxSize);
            List items = upload.parseRequest(createRequestContext(servletRequest));
        //----

            for (Object item1 : items) {
                FileItem item = (FileItem) item1;
                if (log.isDebugEnabled()) log.debug("Found item " + item.getFieldName());
                if (item.isFormField()) {
                    log.debug("Item is a normal form field");
                    List<String> values;
                    if (params.get(item.getFieldName()) != null) {
                        values = params.get(item.getFieldName());
                    } else {
                        values = new ArrayList<String>();
                    }

                    // note: see http://jira.opensymphony.com/browse/WW-633
                    // basically, in some cases the charset may be null, so
                    // we're just going to try to "other" method (no idea if this
                    // will work)
                    String charset = servletRequest.getCharacterEncoding();
                    if (charset != null) {
                        values.add(item.getString(charset));
                    } else {
                        values.add(item.getString());
                    }
                    params.put(item.getFieldName(), values);
                } else {
                    log.debug("Item is a file upload");

                    // Skip file uploads that don't have a file name - meaning that no file was selected.
                    if (item.getName() == null || item.getName().trim().length() < 1) {
                        log.debug("No file has been uploaded for the field: " + item.getFieldName());
                        continue;
                    }

                    List<FileItem> values;
                    if (files.get(item.getFieldName()) != null) {
                        values = files.get(item.getFieldName());
                    } else {
                        values = new ArrayList<FileItem>();
                    }

                    values.add(item);
                    files.put(item.getFieldName(), values);
                }
            }
        } catch (FileUploadException e) {
            log.error(e);
            errors.add(e.getMessage());
        }
    }


->
下面是common-fileupload 1.2版本中的FileUploadBase里的方法:

public List /* FileItem */ parseRequest(RequestContext ctx)
            throws FileUploadException {
        try {
            //与之前版本相比, 将判断文件大小等重构到getItemIterator.
            FileItemIterator iter = getItemIterator(ctx);
            List items = new ArrayList();
            FileItemFactory fac = getFileItemFactory();
            if (fac == null) {
                throw new NullPointerException(
                    "No FileItemFactory has been set.");
            }
            while (iter.hasNext()) {
                FileItemStream item = iter.next();
                FileItem fileItem = fac.createItem(item.getFieldName(),
                        item.getContentType(), item.isFormField(),
                        item.getName());
                try {
                    Streams.copy(item.openStream(), fileItem.getOutputStream(),
                            true);
                } catch (FileUploadIOException e) {
                    throw (FileUploadException) e.getCause();
                } catch (IOException e) {
                    throw new IOFileUploadException(
                            "Processing of " + MULTIPART_FORM_DATA
                            + " request failed. " + e.getMessage(), e);
                }
                items.add(fileItem);
            }
            return items;
        } catch (FileUploadIOException e) {
            throw (FileUploadException) e.getCause();
        } catch (IOException e) {
            throw new FileUploadException(e.getMessage(), e);
        }
    }

->
getItemIterator(ctx)方法代码:
    public FileItemIterator getItemIterator(RequestContext ctx)
    throws FileUploadException, IOException {
        return new FileItemIteratorImpl(ctx);
    }
->

FileItemIteratorImpl(RequestContext ctx)
                throws FileUploadException, IOException {
            if (ctx == null) {
                throw new NullPointerException("ctx parameter");
            }

            String contentType = ctx.getContentType();
            if ((null == contentType)
                    || (!contentType.toLowerCase().startsWith(MULTIPART))) {
                throw new InvalidContentTypeException(
                        "the request doesn't contain a "
                        + MULTIPART_FORM_DATA
                        + " or "
                        + MULTIPART_MIXED
                        + " stream, content type header is "
                        + contentType);
            }

            InputStream input = ctx.getInputStream();

            if (sizeMax >= 0) {
                int requestSize = ctx.getContentLength();
                if (requestSize == -1) {
                    input = new LimitedInputStream(input, sizeMax) {
                        protected void raiseError(long pSizeMax, long pCount)
                                throws IOException {
                            FileUploadException ex =
                                new SizeLimitExceededException(
                                    "the request was rejected because"
                                    + " its size (" + pCount
                                    + ") exceeds the configured maximum"
                                    + " (" + pSizeMax + ")",
                                    pCount, pSizeMax);
                //我们打印的运行时异常就来自这里.
                            throw new FileUploadIOException(ex);
                        }
                    };
                } else {
                    if (sizeMax >= 0 && requestSize > sizeMax) {
                        throw new SizeLimitExceededException(
                                "the request was rejected because its size ("
                                + requestSize
                                + ") exceeds the configured maximum ("
                                + sizeMax + ")",
                                requestSize, sizeMax);
                    }
                }
            }

            String charEncoding = headerEncoding;
            if (charEncoding == null) {
                charEncoding = ctx.getCharacterEncoding();
            }

            boundary = getBoundary(contentType);
            if (boundary == null) {
                throw new FileUploadException(
                        "the request was rejected because "
                        + "no multipart boundary was found");
            }

            notifier = new MultipartStream.ProgressNotifier(listener,
                    ctx.getContentLength());
            multi = new MultipartStream(input, boundary, notifier);
            multi.setHeaderEncoding(charEncoding);

            skipPreamble = true;
            findNextItem();
        }



->


在wrapRequest方法执行完之后,才会进入拦截器FileUploadInteceptor的intercept方法
在该方法中会将MultiPartRequestWrapper的error转换为action的error.

    ......

        if (multiWrapper.hasErrors()) {
            for (Iterator errorIter = multiWrapper.getErrors().iterator(); errorIter.hasNext();) {
                String error = (String) errorIter.next();

                if (validation != null) {
                    validation.addActionError(error);
                }

                log.error(error);
            }
        }
    ......




-------------------------------------------------------------------------------------------

struts2里面struts.properties可以显示的配置自定义的MultiPartRequest实现类.而不是使用struts2默认的
org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest,这种方式common-fileupload提示信息不能国际化.
如果修改代码,只记日志,不抛出运行时异常来避免页面提交参数丢失.但是引出的问题是如果不抛出运行时异常,是不能阻止文件的上传的. 所以,有人认为还是要抛出运行时异常,同时将错误返回到另一个页面.利用浏览器的回退来复原最初表单的提交数据.

下面是struts2提供的重置struts.multipart.parser的方式:
 <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest"    name="jakarta_yourself"  
 class="com.xxxxx.util.JakartaMultiPartRequest"  
 cope="default" optional="true" /> 

struts.multipart.parser=jakarta_yourself



另一种解决方案是抛弃common-fileupload,其它可选的开源上传方式是smart, cos.
推荐使用cos进行文件的上传.具体可以参考http://www.iteye.com/topic/316626

posted @ 2011-06-08 17:37  highriver  阅读(6649)  评论(1编辑  收藏  举报