场景

借助 apache commons upload 实现文件上传。

概览

Commons FileUpload软件包使向Servlet和Web应用程序添加强大的高性能文件上传功能变得容易。

FileUpload解析符合 RFC 1867 HTML中基于表单的文件上载”的HTTP请求。

也就是说,如果使用 POST 方法提交了HTTP请求,并且其内容类型为 multipart/form-data,则FileUpload可以解析该请求,并以调用方易于使用的方式提供结果。

从版本1.3开始,FileUpload处理 RFC 2047 编码的标头值。

向服务器发送多部分/表单数据请求的最简单方法是通过网络表单,即

最简单的定义方式

<form method="POST" enctype="multipart/form-data" action="fup.cgi">
  File to upload: <input type="file" name="upfile"><br/>
  Notes about the file: <input type="text" name="note"><br/>
  <br/>
  <input type="submit" value="Press"> to upload the file!
</form>

快速开始

使用 commons-upload

FileUpload可以以多种不同方式使用,具体取决于应用程序的要求。

在最简单的情况下,您将调用一个方法来解析servlet请求,然后在将它们应用于您的应用程序时处理它们的列表。

另一方面,您可能决定自定义FileUpload,以完全控制单个项目的存储方式。 例

如,您可能决定将内容流式传输到数据库中。

在这里,我们将描述FileUpload的基本原理,并说明一些更简单且最常见的使用模式。

FileUpload的自定义在其他地方介绍。

FileUpload 依赖 Commons-IO,因此在继续之前,请确保您具有类路径中的依赖项页面上提到的版本。

版本依赖

依赖界面

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

v1.4 对应的 commons-io 版本为:v2.2

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.2</version>
</dependency>

你可以手动下载依赖,个人建议使用 maven 进行包管理。

工作原理

文件上载请求包括根据RFC 1867“ HTML中基于表单的文件上载”进行编码的项目的有序列表。

FileUpload可以解析此类请求,并为您的应用程序提供各个上载项目的列表。每个此类项目均实现FileItem接口,而不管其基础实现如何。

此页面描述了commons文件上传库的传统API。传统的API是一种便捷的方法。但是,为了获得最终性能,您可能更喜欢更快的 Streaming API

每个文件项都有许多您的应用程序可能需要的属性。

例如,每个项目都有一个名称和内容类型,并且可以提供一个InputStream来访问其数据。

另一方面,您可能需要根据项目是常规表单字段(即,数据来自普通文本框还是类似的HTML字段)还是上传的文件,对项目进行不同的处理。

FileItem接口提供了进行这种确定以及以最适当的方式访问数据的方法。

FileUpload使用FileItemFactory创建新的文件项。

这就是FileUpload的最大灵活性。工厂对每个项目的创建方式拥有最终控制权。

FileUpload当前随附的工厂实现将项目的数据存储在内存中或磁盘上,具体取决于项目的大小(即数据字节)。

但是,可以自定义此行为以适合您的应用程序。

入门例子

apache.jsp

<!DOCTYPE html>
<%@page contentType="text/html; charset=UTF-8" language="java"%>
<html lang="zh">
<head>
    <title>JSP 实现文件上传和下载</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data" action="/apache/upload">
    File to upload:
    <input type="file" name="file"><br/>
    Notes about the file:
    <input type="text" name="note"><br/>
    <br/>
    <input type="submit" value="Press"> to upload the file!
</form>
</body>
</html>

直接根据官方的例子

ApacheController.java

对应的后端代码

package com.github.houbb.jsp.learn.hello.controller;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.List;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
@Controller
public class ApacheController {

    /**
     * 实现文件上传
     *
     * @param request  请求
     * @param response 响应
     * @return
     */
    @GetMapping("/apache")
    @PostMapping("/apache")
    public String index(HttpServletRequest request,
                        HttpServletResponse response) {
        return "apache";
    }

    /**
     * 实现文件上传
     *
     * @param request  请求
     * @param response 响应
     * @return 页面
     */
    @PostMapping(value = "/apache/upload")
    public String upload(HttpServletRequest request,
                         HttpServletResponse response) throws Exception {
        // 上传文件夹
        String uploadDir = request.getServletContext().getRealPath("/WEB-INF/upload/");
        File tempDir = new File(uploadDir);

        // file less than 10kb will be store in memory, otherwise in file system.
        final int threshold = 10240;
        final int maxRequestSize = 1024 * 1024 * 4;	// 4MB

        if(ServletFileUpload.isMultipartContent(request)) {
            // Create a factory for disk-based file items.
            FileItemFactory factory = new DiskFileItemFactory(threshold, tempDir);
            // Create a new file upload handler
            ServletFileUpload upload = new ServletFileUpload(factory);
            // Set overall request size constraint.
            upload.setSizeMax(maxRequestSize);

            List<FileItem> items = upload.parseRequest(request);

            for(FileItem item : items) {
                // 普通的表单字段
                if(item.isFormField()) {
                    String name = item.getFieldName();
                    String value = item.getString();
                    System.out.println(name + ": " + value);
                } else {
                    // 真实的文件
                    //file upload
                    String fieldName = item.getFieldName();
                    String fileName = item.getName();
                    File uploadedFile = new File(uploadDir + File.separator + fieldName + "_" + fileName);
                    item.write(uploadedFile);
                }
            }
        }  else {
            // 文件解析失败
        }
        return "apache";

    }

}

问题

upload.parseRequest(request) 这一步处理的结果并不是如网上说的那样有结果。

打断点发现文件确实是传过来了。

于是晚上查了一下,应该是 springboot 对 request 已经进行了一次封装,不再是最基本的 request 请求。

解决方案

使用Apache Commons FileUpload组件上传文件时总是返回null,调试发现ServletFileUpload对象为空,在Spring Boot中有默认的文件上传组件,在使用ServletFileUpload时需要关闭Spring Boot的默认配置 ,

禁用MultipartResolverSpring提供的默认值

禁用 springboot 转换:

  • application.properties
spring.servlet.multipart.enabled=false

再次执行就可以了。

代码分析

其实整体比较简单。

对转换后的 FileItem 通过 item.isFormField() 判断是否为普通的 form 字段。

普通 form 字段

比如我测试,日志输出:

note: 123

这个对应的是 note 属性,及其页面我输入的值 123。

文件信息

File uploadedFile = new File(uploadDir + File.separator + fieldName + "_" + fileName);
item.write(uploadedFile);

这两行代码可以在指定的文件夹下创建对应的文件信息。

当然这个性能不是很好,因为实际上是借助了临时文件夹,然后做文件的处理。

临时文件的清空

本节仅在使用DiskFileItem时适用。

换句话说,如果您在处理之前将上传的文件写入临时文件,则适用。

如果不再使用这些临时文件(更确切地说,如果DiskFileItem的相应实例已被垃圾回收),则会自动将其删除。

这是由 org.apache.commons.io.FileCleanerTracker 类以静默方式完成的,该类启动了收割线程。

如果不再需要该收割线程,则应停止该线程。

在Servlet环境中,这是通过使用称为FileCleanerCleanup的特殊Servlet上下文侦听器来完成的。

为此,请将以下内容添加到web.xml中:

<web-app>
  ...
  <listener>
    <listener-class>
      org.apache.commons.fileupload.servlet.FileCleanerCleanup
    </listener-class>
  </listener>
  ...
</web-app>

观察上传进度

如果您希望上传非常大的文件,那么最好向用户报告已收到多少文件。

甚至HTML页面也允许通过返回 multipart/replace 响应或类似的东西来实现进度条。

观看上传进度可以通过提供进度监听器来完成:

//Create a progress listener
ProgressListener progressListener = new ProgressListener(){
   public void update(long pBytesRead, long pContentLength, int pItems) {
       System.out.println("We are currently reading item " + pItems);
       if (pContentLength == -1) {
           System.out.println("So far, " + pBytesRead + " bytes have been read.");
       } else {
           System.out.println("So far, " + pBytesRead + " of " + pContentLength
                              + " bytes have been read.");
       }
   }
};
upload.setProgressListener(progressListener);

像上面一样,帮自己一个忙,实现第一个进度监听器,因为它向您展示了一个陷阱:进度监听器被频繁调用。

根据servlet引擎和其他环境工厂的不同,可能会为任何网络数据包调用它!

换句话说,您的进度侦听器可能会成为性能问题!

一个典型的解决方案可能是减少进度侦听器活动。

例如,如果兆字节数已更改,则可能仅发出一条消息:

//Create a progress listener
ProgressListener progressListener = new ProgressListener(){
   private long megaBytes = -1;
   public void update(long pBytesRead, long pContentLength, int pItems) {
       long mBytes = pBytesRead / 1000000;
       if (megaBytes == mBytes) {
           return;
       }
       megaBytes = mBytes;
       System.out.println("We are currently reading item " + pItems);
       if (pContentLength == -1) {
           System.out.println("So far, " + pBytesRead + " bytes have been read.");
       } else {
           System.out.println("So far, " + pBytesRead + " of " + pContentLength
                              + " bytes have been read.");
       }
   }
};

Streaming API

更快的实现

用户指南中描述的传统API假定文件项必须存储在某个位置,然后用户才能实际对其进行访问。 这种方法很方便,因为它允许轻松访问项目内容。 另一方面,这是内存和时间消耗。

流式API使您可以牺牲一点便利来获得最佳性能和低内存配置文件。

此外,API更轻巧,因此更易于理解。

Streaming API 上传速度相对较快。

因为它是利用内存保存上传的文件,节省了传统API将文件写入临时文件带来的开销。

官方: http://commons.apache.org/proper/commons-fileupload/streaming.html

http://stackoverflow.com/questions/11620432/apache-commons-fileupload-streaming-api

工作原理

同样,FileUpload类用于按客户端发送表单字段和字段的顺序访问表单字段和字段。 但是,FileItemFactory被完全忽略。

入门例子

stream.jsp

<!DOCTYPE html>
<%@page contentType="text/html; charset=UTF-8" language="java"%>
<html lang="zh">
<head>
    <title>JSP 实现文件上传和下载</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data" action="/stream/upload">
    File to upload:
    <input type="file" name="file"><br/>
    Notes about the file:
    <input type="text" name="note"><br/>
    <br/>
    <input type="submit" value="Press"> to upload the file!
</form>
</body>
</html>

直接请求 /stream/upload

后端

package com.github.houbb.jsp.learn.hello.controller;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
@Controller
public class StreamController {

    /**
     * 实现文件上传
     *
     * @param request  请求
     * @param response 响应
     * @return
     */
    @GetMapping("/stream")
    @PostMapping("/stream")
    public String index(HttpServletRequest request,
                        HttpServletResponse response) {
        return "stream";
    }

    /**
     * 实现文件上传
     *
     * @param request  请求
     * @param response 响应
     * @return 页面
     */
    @PostMapping(value = "/stream/upload")
    public String upload(HttpServletRequest request,
                         HttpServletResponse response) throws Exception {
        // 上传文件夹
        String uploadDir = request.getServletContext().getRealPath("/WEB-INF/upload/");

        if(ServletFileUpload.isMultipartContent(request)) {
            // 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()) {
                    System.out.println("Form field " + name + " with value "
                            + Streams.asString(stream) + " detected.");
                } else {
                    String fileName = item.getName();
                    File uploadedFile = new File(uploadDir + File.separator + fileName);
                    OutputStream os = new FileOutputStream(uploadedFile);
                    // write file to disk and close outputstream.
                    Streams.copy(stream, os, true);
                }
            }
        }  else {
            // 文件解析失败
        }

        return "stream";
    }

}

感觉上确实要快一些。

JSP 直接实现

这也是看到一些文章直接可以通过 jsp 调用 jsp。

实际上 jsp 的原理就是执行 java 代码,这里记录一下,平时也不是很建议这样使用。

upload.jsp

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<html>
 <body>
  <form method="POST" enctype="multipart/form-data" action="traditionalapi.jsp">
  File to upload: <input type="file" name="file" size="40"><br/>
  <input type="submit" value="Press"> to upload the file!
	</form>
 </body>
</html>

traditionalapi.jsp

<%@page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" language="java"%>
<%@page import="java.io.File"%>
<%@page import="java.util.List"%>
<%@page import="org.apache.commons.fileupload.*"%>
<%@page import="org.apache.commons.fileupload.disk.DiskFileItemFactory"%>
<%@page import="org.apache.commons.fileupload.servlet.ServletFileUpload"%>
 
<%
	request.setCharacterEncoding("UTF-8");
	// file less than 10kb will be store in memory, otherwise in file system.
	final int threshold = 10240;
	final File tmpDir = new File(getServletContext().getRealPath("/") + "fileupload" + File.separator + "tmp");
	final int maxRequestSize = 1024 * 1024 * 4;	// 4MB
	// Check that we have a file upload request
	if(ServletFileUpload.isMultipartContent(request))
	{
		// Create a factory for disk-based file items.
		FileItemFactory factory = new DiskFileItemFactory(threshold, tmpDir);
		
		// Create a new file upload handler
		ServletFileUpload upload = new ServletFileUpload(factory);
		// Set overall request size constraint.
		upload.setSizeMax(maxRequestSize);
		List<FileItem> items = upload.parseRequest(request);	// FileUploadException 
		for(FileItem item : items)
		{
			if(item.isFormField())	//regular form field
			{
				String name = item.getFieldName();
				String value = item.getString();
%>
				<h1><%=name%> --> <%=value%></h1>
<%
			}
			else
			{	//file upload
				String fieldName = item.getFieldName();
				String fileName = item.getName();
				File uploadedFile = new File(getServletContext().getRealPath("/") +
							"fileupload" + File.separator + fieldName + "_" + fileName);
				item.write(uploadedFile);
%>
				<h1>upload file <%=uploadedFile.getName()%> done!</h1>
<%
			}
		}
	}
%>

拓展阅读

JSP 远程调用

axios 实战

maven 引入

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.2</version>
</dependency>

前端

<div class="upload">
    <div class="upload-title">附件上传</div>
    <div class="upload-item">
        <input ref="createFile" class="file" accept=".zip,.rar"  type="file" @change="fileChange"> 
        <el-button size="small" type="primary" class="btn">选择文件</el-button>
    </div>
    <div></div>
    <div class="upload-text">文件大小不超过30M,仅支持ZIP、RAR文件格式</div>
</div>

对应的上传 js

fileChange(e){
    const files = e.target.files;
    if(files && files[0]) {
        const file = files[0];
        if(file.size > 1024 * 1024 *30) {
            this.$message({ showClose: true,  message: '文件大小不能超过30M!', type: 'warning'});
            return
        } 
        var formData = new FormData()
        formData.append("file",files[0])
        var config = {
            headers: {
                "Content-Type": "multipart/form-data;charset=UTF-8"
            }
        };
        axios.post("file/upload",formData, config).then((res) => {
            console.log(formData,res);
            if (res.data.respCode == '0000') {
                this.fileName= files[0].name 
                this.$refs.createFile.value = '' 
                this.fileToken=res.data.result.fileToken            
            } else {
                this.$message({ message: res.data.respMsg, type: 'error'});
            } 
        })
    }
}

ps: 这种上传比较麻烦,可以参考网上的 post form 表单的方式。

后端配置

配置类

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

import javax.servlet.MultipartConfigElement;

/**
 * 文件请求配置
 *
 * https://blog.csdn.net/luyongguo/article/details/86010145
 *
 * @author binbin.hou
 * @since 1.0.0
 */
@Configuration
public class FileRequestConfig {

    @Value("${file.request.maxFileSize:100MB}")
    private String maxFileSize;

    @Value("${file.request.maxRequestSize:100MB}")
    private String maxRequestSize;

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        // 单个数据大小
        // KB,MB
        factory.setMaxFileSize(maxFileSize);
        // 总上传数据大小
        factory.setMaxRequestSize(maxRequestSize);

        return factory.createMultipartConfig();
    }

    @Bean
    public CommonsMultipartResolver commonsMultipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();

        resolver.setDefaultEncoding(Constants.UTF_8);
        resolver.setMaxUploadSize(500 * 1024 * 1024);
        resolver.setMaxUploadSizePerFile(100 * 1024* 1024);
        return resolver;
    }
}

主要指定文件的上传大小,编码等。

后端实现

注意:@RequestParam("file") MultipartFile multipartFile 这里是必须的,如果使用 ServeletHttpRequest 中转换获取是没有值的。

/**
 * 文件上传
 * https://www.cnblogs.com/charlypage/p/9858676.html
 * @param multipartFile 上传请求
 * @return 上传结果
 */
@RequestMapping("/upload")
public BaseInfoResp<FileUploadResp> upload(@RequestParam("file") MultipartFile multipartFile) {
    UploadFileDto uploadFileDto = HttpUtils.getUploadFileInfo(multipartFile);
    File file = uploadFileDto.getFile();
    try {
        ValidateUtils.validate(uploadFileDto);
        this.fileSizeCheck(uploadFileDto);
        String fileToken = fileService.uploadJFile(file);
        // 入库
        String fileName = uploadFileDto.getOriginalFilename();
        customerFileService.addFile(fileToken, fileName);
        FileUploadResp resp = new FileUploadResp();
        resp.setFileToken(fileToken);
        return RespUtil.of(resp);
    } finally {
        FileUtil.deleteFile(file);
    }
}

其中文件构建的部分:

/**
 * 获取上传的文件信息
 * @param multipartFile 请求
 * @return 结果
 */
public static UploadFileDto getUploadFileInfo(MultipartFile multipartFile) {
    try {
        String filename = multipartFile.getOriginalFilename();
        long fileSize = multipartFile.getSize();
        log.info("原始的文件名称 {}, 文件大小 {}", filename, fileSize);
        //1. 创建文件
        String targetFullPath = FileUtil.buildFullPath(filename);
        File file = FileUtil.createFile(targetFullPath);
        // 写入文件
        try (FileOutputStream fos = new FileOutputStream(targetFullPath);
             BufferedOutputStream bos = new BufferedOutputStream(fos);) {
            bos.write(multipartFile.getBytes());
        } catch (Exception e) {
            log.error("异常", e);
            throw new BizException(RespCode.FILE_UPLOAD_ERROR);
        }
        UploadFileDto dto = new UploadFileDto();
        dto.setFile(file);
        dto.setOriginalFilename(filename);
        dto.setFileSize(fileSize);
        return dto;
    } catch (Exception e) {
        log.error("文件上传异常", e);
        throw new BizException(RespCode.FILE_UPLOAD_REQUEST_ERROR);
    }
}

file 信息缺失

测试了很多遍,都发现 multipartFile 的信息为 null。

虽然以前踩过类似的坑,直到被 spring 转换掉了。

但是配置下面之后,依然无效。

  • springboot.properties
spring.servlet.multipart.enabled=false

后来发现,需要排除 springboot 的自动配置类:

@SpringBootApplication(exclude = {MultipartAutoConfiguration.class})
@PropertySource("classpath:springboot.properties")
public class BootApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(BootApplication.class);
    }

}

参考资料

commons-fileupload

Spring Boot 使用 ServletFileUpload上传文件失败,upload.parseRequest(request)为空

Apache Commons FileUpload

SpringMVC用MultipartFile上传文件及文件名中文乱码

SpringBoot结合commons-fileupload上传文件

  • 上传文件缺失问题

https://www.cnblogs.com/charlypage/p/9858676.html

https://blog.csdn.net/weixin_42733631/article/details/112600786

https://www.cnblogs.com/charlypage/p/9858676.html

https://blog.csdn.net/qq_36907589/article/details/108516431