Servlet & Spring对Multipart数据请求的支持

  • Post author:
  • Post category:其他



参考资料



(1)

RFC 1867


(2)Java Servlet Specification 3.1;

(3)《Java Web高级编程》;

1. Multipart FormData

Multipart是HTML中表单文件上传的基本格式,一般通过如下方法可以通过HTTP上传文件:

    <form action="_URL_" method="POST" enctype="multipart/form-data">
        <input type="text" name="username" />
        <input type="file" name="userfile1" />
        <input type="submit" value="submit" />
    </form>

有两个地方是使用Multipart的关键:

(1)对于POST请求来说,enctype的默认值是application/x-www-form-urlencoded,而这里要是用

multipart/form-data



(2)

<input />

的type设置为

file

1.1 Multipart的数据格式

基于Multipart,请求的每个部分都有指定的边界分隔开,都有一个值为form-data的

Content-Disposition

和匹配表单输入名称的

name



如果是文件类型字段,还将有

filename

,匹配MIME类型的

Content-Type



使用下面的表单提交单个文件和其他文本域:

测试1:单文件上传
    <form action="/s/upload/1" method="post" enctype="multipart/form-data">
        <fieldset>
            <legend>测试1:单文件上传</legend>
            <p><label for="name">名称 </label><input id="name" type="text" name="name" /></p>
            <p><label for="files">文件 </label><input id="files" type="file" name="files" /></p>
            <p><label for="location">地区 </label><input id="location" type="text" name="location" /></p>
            <input type="submit" />
        </fieldset>
    </form>

请求数据内容:

------WebKitFormBoundaryBDujyAl87MaTQd9J
Content-Disposition: form-data; name="name"

串个沙
------WebKitFormBoundaryBDujyAl87MaTQd9J
Content-Disposition: form-data; name="files"; filename="说明.txt"
Content-Type: text/plain


------WebKitFormBoundaryBDujyAl87MaTQd9J
Content-Disposition: form-data; name="location"

地球
------WebKitFormBoundaryBDujyAl87MaTQd9J--

可以看到每个部分有边界像分隔,这个边界值由客户端决定,是随机生成的(这里我使用chrome做的实验,因此可以看到“WebKit”)。

测试2:多文件上传
    <form action="/s/upload/1" method="post" enctype="multipart/form-data">
        <fieldset>
            <legend>测试2:多文件上传</legend>
            <p><label for="name">名称 </label><input id="name1" type="text" name="name" /></p>
            <p><label for="files">文件 </label><input id="files1" type="file" name="files1" multiple="multiple" /></p>
            <p><label for="location">地区 </label><input id="location1" type="text" name="location" /></p>
            <input type="submit" />
        </fieldset>
    </form>

请求头Content-Type:

    Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryZGDkB6nM3LvHX0KI

请求数据内容:

------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="name"

2个文件
------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="files1"; filename="说明.txt"
Content-Type: text/plain


------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="files1"; filename="说明 (copy).txt"
Content-Type: text/plain


------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="location"

中国
------WebKitFormBoundaryLnO8YOO2DPoP3M6y--

基于chrome得到的结果,没有出现一个Content-type为

multipart/mixed

的嵌套部分,而是用多个文件字段部分表示。

2. Servlet 3.0对Multipart的支持

在Servlet3.0之前对于文件上传的支持,可以通过Commons FileUpload等第三方工具来实现。Java EE 6中Servlet 3.0新增了对它的支持,这样可以不再依赖第三方库。主要通过HttpServletRequest的

getParts()



getPart()

方法。

结合前面的测试表单,我用一个Servlet和Jsp来演示处理的具体方法:

UploadServlet2:

@WebServlet (
        name = "uploadServlet2",
        urlPatterns = "/upload2",
        loadOnStartup = 1
)
@MultipartConfig (
        fileSizeThreshold = 5_242_880,
        maxFileSize = 20_971_520L,
        maxRequestSize = 41_943_040L
)
public class UploadServlet2 extends HttpServlet {
    private static final Logger logger = LogManager.getLogger();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("/p/upload/upload.jsp").forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Collection<Part> parts = req.getParts();
        logger.debug("uploading...");
        req.setAttribute("parts", parts);
        req.getRequestDispatcher("/p/upload/resolvePart.jsp").forward(req, resp);
    }
}

通过ServletContainerInitializer配置:

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        logger.info("ServletContextListener invoked");
        //获取ServletContext对象
        ServletContext servletContext = sce.getServletContext();
        //添加Servlet
        ServletRegistration.Dynamic uploadRegistration = servletContext.addServlet("uploadServlet", UploadServlet.class);
        uploadRegistration.addMapping("/upload/*");
        uploadRegistration.setLoadOnStartup(1);
        MultipartConfigElement configElement = new MultipartConfigElement(".", 52428800,
                52428800, 0);
        uploadRegistration.setMultipartConfig(configElement);
    }

必须启用multipart支持,配置一样有三种方法:注解,编程(ServletContextListener/ServletContainerInitializer),XML,但是基本的配置项是一样的:


location

:临时文件目录,一般可以使用默认的由服务器软件决定;


fileSizehreshold

:超过这个阈值放入临时文件目录,否则在内存中有垃圾回收处理;


maxFileSize



maxRequestSize

location的配置需要注意,Servlet规范中的说明:

location元被解析为一个绝对路径且默认为 javax.servlet.context.tempdir。如果指定了相对地址,它将是相对于 tempdir 位置。绝对路径与相对地址的测试必须使用 java.io.File.isAbsolute。

3. Spring对Multipart的支持

服务端主要就是解析Multipart数据。

启用Multipart,这里通过Spring的WebApplicationInitializer初始化器进行配置,本质是通过ServletContainerInitializer。启用指定DispatcherServlet的Multipart支持。还是通过

Registration

配置的和上面的ServletContextListener一样。这里配置了自定义的临时目录,如果指定的目录不存在或是无法访问,通过”.”配置,因为前面引用的Serlvet规范已经说明了,相对目录是基于容器的临时目录的。

Spring同时兼容了基于Commons FileUpload和Servlet 3.0标准API两种方式的解析。

@Order(1)
public class BootStrap implements WebApplicationInitializer {
    /* 略 */
    @Override
    public void onStartup(ServletContext container) throws ServletException {
        /* 略 */
        DispatcherServlet dispatcherServlet = new DispatcherServlet(cgContext);
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
        ServletRegistration.Dynamic dispatcherCG = container.addServlet("cgServlet",
                dispatcherServlet);
        dispatcherCG.setLoadOnStartup(1);
        //文件上传支持
        //设置临时文件目录
        File path = new File(container.getRealPath("/tmp/web_yjh_files/"));

        if(path.exists() || path.mkdirs()) {
            dispatcherCG.setMultipartConfig(new MultipartConfigElement(
                    path.getAbsolutePath(), 200_971_520L, 401_943_040L, 0
            ));
        } else {
            dispatcherCG.setMultipartConfig(new MultipartConfigElement(
                    ".", 200_971_520L, 401_943_040L, 0
            ));
        }
        /* 略 */
    }
}

Spring提供了一个Multipart的解析器:MultipartResolver,因此在

@Configuration

类中添加一个Bean,有两种选择

CommonsMultipartResolver



StandardServletMultipartResolver

,分别基于Commons File Upload和Servlet 3.0标准API;

    @Bean
    public MultipartResolver multipartResolver() {
        return new CommonsMultipartResolver();
    }

编写Controller method,使用Spring的

MultipartFile

    @RequestMapping(value = "upload", method = RequestMethod.POST, produces = "text/html")
    @ResponseBody
    public String upload(String username,
                         @RequestParam(value = "attachment") MultipartFile parts) throws Exception {
        return "success " +
                "name:" + parts.getName() +
                "size:" + parts.getSize() +
                "contentType:" + parts.getContentType() +
                (parts.getContentType() == null ? "" : ("filename" + parts.getOriginalFilename())) +
                "" + IOUtils.toString(parts.getInputStream()) +
                "size:" + parts.getSize();
    }

使用Part API:

Serlvet Part的标准API包括:

(1)

name

:表单字段名;

(2)

size

:Part数据内容的大小;

(3)

submittedFileName

:Servlet 3.1新增方法,获取文件名,之前必须通过

getHeader("content-disposition")

解析;

(4)

contentType

:如果

<input>

类型是type这个值是

null

,如果是文件,可以获得它的MIME类型;

(5)

getHeader

:获取指定头;

(6)

getInputStream

:获取输入流;

对于表单数据的 Content-Disposition,即使没有文件名,也可使用 part 的名称通过 HttpServletRequest 的

getParameter 和 getParameterValues 方法得到 part 的字符串值。

    <h1>Multipart解析</h1>
    <c:forEach items="${parts}" var="part">
      <p>
        <h3>Part: ${part.name}</h3>
        <span>Size: ${part.size}</span><br />
        <span>SubmittedFileName: ${part.submittedFileName},Servlet 3.1新增方法</span><br />
        <span>ContentType: ${part.contentType}</span><br />
        <c:choose>
          <c:when test="${part.contentType == null || part.contentType eq 'text/plain'}">
            <p>
                ${IOUtils.toString(part.inputStream)}
            </p>
          </c:when>
          <c:when test="${part.contentType eq 'application/octet-stream'}">
            <p>
                ${part.getHeader("content-disposition")}
            </p>
          </c:when>
        </c:choose>
      </p>
    </c:forEach>
  <c:import url="upload.jsp" />



版权声明:本文为Zerohuan原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。