【问题标题】:SpringBoot: Large Streaming File Upload Using Apache Commons FileUploadSpringBoot:使用 Apache Commons FileUpload 上传大型流文件
【发布时间】:2015-12-23 06:39:00
【问题描述】:

我正在尝试使用“流式”Apache Commons File Upload API 上传一个大文件。

我使用 Apache Commons File Uploader 而不是默认的 Spring Multipart 上传器的原因是当我们上传非常大的文件大小 (~2GB) 时它会失败。我正在开发一个 GIS 应用程序,这种文件上传很常见。

我的文件上传控制器的完整代码如下:

@Controller
public class FileUploadController {

    @RequestMapping(value="/upload", method=RequestMethod.POST)
    public void upload(HttpServletRequest request) {
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (!isMultipart) {
            // Inform user about invalid request
            return;
        }

        //String filename = request.getParameter("name");

        // Create a new file upload handler
        ServletFileUpload upload = new ServletFileUpload();

        // Parse the request
        try {
            FileItemIterator iter = upload.getItemIterator(request);
            while (iter.hasNext()) {
                FileItemStream item = iter.next();
                String name = item.getFieldName();
                InputStream stream = item.openStream();
                if (item.isFormField()) {
                    System.out.println("Form field " + name + " with value " + Streams.asString(stream) + " detected.");
                } else {
                    System.out.println("File field " + name + " with file name " + item.getName() + " detected.");
                    // Process the input stream
                    OutputStream out = new FileOutputStream("incoming.gz");
                    IOUtils.copy(stream, out);
                    stream.close();
                    out.close();

                }
            }
        }catch (FileUploadException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @RequestMapping(value = "/uploader", method = RequestMethod.GET)
    public ModelAndView uploaderPage() {
        ModelAndView model = new ModelAndView();
        model.setViewName("uploader");
        return model;
    }

}

问题在于 getItemIterator(request) 总是返回一个没有任何项目的迭代器(即 iter.hasNext() )总是返回 false

我的application.properties文件如下:

spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:19095/authdb
spring.datasource.username=georbis
spring.datasource.password=asdf123

logging.level.org.springframework.web=DEBUG

spring.jpa.hibernate.ddl-auto=update

multipart.maxFileSize: 128000MB
multipart.maxRequestSize: 128000MB

server.port=19091

/uploader 的 JSP 视图如下:

<html>
<body>
<form method="POST" enctype="multipart/form-data" action="/upload">
    File to upload: <input type="file" name="file"><br />
    Name: <input type="text" name="name"><br /> <br />
    Press here to upload the file!<input type="submit" value="Upload">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>
</body>
</html>

我可能做错了什么?

【问题讨论】:

  • 您是否禁用了 springs 多部分支持,否则您的解决方案将无法正常工作,并且 Spring 已经解析了请求。将所有 multipart 属性替换为单个 multipart.enabled=false 以禁用默认处理。
  • 我没有为禁用 spring 多部分支持做任何具体的事情。我尝试在我的application.properties 文件中添加multipart.enabled=false。但是,一旦我这样做了,我每次上传时都会收到 405: Request method 'POST' not supported 错误。
  • 这将表明错误的映射或发布到错误的 url... 启用调试日志记录并查看您发布到哪个 URL 以及您的控制器方法与哪个 URL 匹配。
  • 这绝对不是发布到错误 URL 的情况,因为当我删除 multipart.enabled=false 时,我的控制器确实被调用(我再次遇到上面帖子中描述的问题)。跨度>
  • 不,您不想要任何MultipartResolver,因为它会解析传入的请求并将文件放入内存中。您想自己处理它,因此您不希望其他任何事情弄乱您的多部分请求。

标签: spring spring-boot apache-commons apache-commons-fileupload


【解决方案1】:

您可以简单地添加弹簧属性:

spring.servlet.multipart.max-file-size=20000KB
spring.servlet.multipart.max-request-size=20000KB

这里我的最大文件大小是 20000KB,如果需要,您可以更改。

【讨论】:

    【解决方案2】:

    我使用 kindeditor + springboot。当我使用 (MultipartHttpServletRequest) 请求时。我可以得到文件,但我使用 appeche-common-io:upload.parse(request) 返回值为 null。

    public BaseResult uploadImg(HttpServletRequest request,String type){
                    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
                    MultiValueMap<String, MultipartFile> multiFileMap = multipartRequest.getMultiFileMap();
    

    【讨论】:

      【解决方案3】:

      如果您使用的是最新版本的 spring boot(我使用的是 2.0.0.M7),则属性名称已更改。 Spring 开始使用特定于技术的名称

      spring.servlet.multipart.maxFileSize=-1

      spring.servlet.multipart.maxRequestSize=-1

      spring.servlet.multipart.enabled=false

      如果由于多个实现处于活动状态而导致 StreamClosed 异常,则最后一个选项允许您禁用默认的 spring 实现

      【讨论】:

        【解决方案4】:

        请尝试在application.properties文件中添加spring.http.multipart.enabled=false

        【讨论】:

          【解决方案5】:

          感谢 M.Deinum 提供的一些非常有用的 cmets,我设法解决了这个问题。我已经清理了一些原始帖子,并将其发布为完整的答案以供将来参考。

          我犯的第一个错误是没有禁用 Spring 提供的默认 MultipartResolver。这最终在解析器处理HttpServeletRequest 并因此在我的控制器可以对其采取行动之前消耗它。

          感谢M. Deinum,禁用它的方法如下:

          multipart.enabled=false
          

          然而,在这之后还有另一个隐藏的陷阱在等着我。一旦我禁用了默认的多部分解析器,我在尝试上传时就开始收到以下错误:

          Fri Sep 25 20:23:47 IST 2015
          There was an unexpected error (type=Method Not Allowed, status=405).
          Request method 'POST' not supported
          

          在我的安全配置中,我启用了 CSRF 保护。这需要我以以下方式发送我的 POST 请求:

          <html>
          <body>
          <form method="POST" enctype="multipart/form-data" action="/upload?${_csrf.parameterName}=${_csrf.token}">
              <input type="file" name="file"><br>
              <input type="submit" value="Upload">
          </form>
          </body>
          </html>
          

          我还稍微修改了我的控制器:

          @Controller
          public class FileUploadController {
              @RequestMapping(value="/upload", method=RequestMethod.POST)
              public @ResponseBody Response<String> upload(HttpServletRequest request) {
                  try {
                      boolean isMultipart = ServletFileUpload.isMultipartContent(request);
                      if (!isMultipart) {
                          // Inform user about invalid request
                          Response<String> responseObject = new Response<String>(false, "Not a multipart request.", "");
                          return responseObject;
                      }
          
                      // Create a new file upload handler
                      ServletFileUpload upload = new ServletFileUpload();
          
                      // Parse the request
                      FileItemIterator iter = upload.getItemIterator(request);
                      while (iter.hasNext()) {
                          FileItemStream item = iter.next();
                          String name = item.getFieldName();
                          InputStream stream = item.openStream();
                          if (!item.isFormField()) {
                              String filename = item.getName();
                              // Process the input stream
                              OutputStream out = new FileOutputStream(filename);
                              IOUtils.copy(stream, out);
                              stream.close();
                              out.close();
                          }
                      }
                  } catch (FileUploadException e) {
                      return new Response<String>(false, "File upload error", e.toString());
                  } catch (IOException e) {
                      return new Response<String>(false, "Internal server IO error", e.toString());
                  }
          
                  return new Response<String>(true, "Success", "");
              }
          
              @RequestMapping(value = "/uploader", method = RequestMethod.GET)
              public ModelAndView uploaderPage() {
                  ModelAndView model = new ModelAndView();
                  model.setViewName("uploader");
                  return model;
              }
          }
          

          其中 Response 只是我使用的一个简单的通用响应类型:

          public class Response<T> {
              /** Boolean indicating if request succeeded **/
              private boolean status;
          
              /** Message indicating error if any **/
              private String message;
          
              /** Additional data that is part of this response **/
              private T data;
          
              public Response(boolean status, String message, T data) {
                  this.status = status;
                  this.message = message;
                  this.data = data;
              }
          
              // Setters and getters
              ...
          }
          

          【讨论】:

          • 当multipart.enabled=false 时,MockMultipartFile 在单元测试用例中不起作用。有没有使用MockMvc 上传文件的单元测试用例示例
          • 我尝试设置 spring.servlet.multipart.enabled=false,但迭代器仍然没有返回任何值。为什么会这样?