⭐表示重要。
第一章:表单(⭐)
- 要求:
- ① 请求方式必须是 POST。
- ② 请求体的编码方式必须是 multipart/form-data (通过 form 标签的 enctype 属性设置)。
③ 使用 input 标签,type 属性设置为 file 来生成文件上传域。
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<!-- 上传文件的表单 -->
<!-- method 属性:用来上传的表单,请求方式必须是 POST -->
<!--
enctype 属性:用来指定请求体的编码类型 ,默认值是 application/x-www-form-urlencoded,如果是上传文件,需要设置为 multipart/form-data。
-->
<!--
如果 enctype 设置为 multipart/form-data ,那么整个请求体都是按照二进制方式来编码,所以非上传文件的普通组件也不能按照原来的方式(request.getParameter(..))进行解码了。
-->
<form enctype="multipart/form-data" method="post" th:action="@{/upload}">
用户名:<input name="userName" type="text"><br/>
上传文件:<input name="file" type="file"><br/>
<button>提交</button>
</form>
</body>
</html>
第二章:SpringMVC 环境要求(⭐)
2.1 导入依赖
- pom.xml
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
- 完整的pom.xml
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.12</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
<!-- 文件上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
2.2 配置
- 在 SpringMVC 的配置文件中加入 multipart 类型数据的解析器:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
- 完整的 springmvc.xml :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 自动扫描包 -->
<context:component-scan base-package="com.github.fairy.era.mvc"></context:component-scan>
<!-- 配置视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<!-- 物理视图:视图前缀+逻辑视图+视图后缀 -->
<!-- 视图前缀 -->
<property name="prefix" value="/WEB-INF/templates/"/>
<!-- 视图后缀 -->
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML5"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<mvc:view-controller path="/" view-name="portal"/>
<!-- 配置CommonsMultipartResolver -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
</beans>
第三章:handler方法接收数据(⭐)
- 表单提交的数据如果是请求参数,依然可以使用
@RequestParam
注解接收。 表单提交的数据如果是上传文件,使用
MultipartFile
类型接收。示例:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class UploadHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PostMapping("/upload")
public String upload(
// 表单提交的数据仍然是请求参数,所以使用 @RequestParam 注解接收
@RequestParam("userName") String userName,
// 对于上传的文件使用 MultipartFile 类型接收其相关数据
@RequestParam("file") MultipartFile file) throws IOException {
logger.info("UploadHandler.upload的请求参数中的是userName :{}", userName);
String name = file.getName();
logger.info("文件上传表单项中的 name 属性值:{}", name);
String originalFilename = file.getOriginalFilename();
logger.info("文件在用户本地原始的文件名:{}", originalFilename);
String contentType = file.getContentType();
logger.info("文件的内容类型:{}", contentType);
boolean empty = file.isEmpty();
logger.info("文件是否为空:{}", empty);
long size = file.getSize();
logger.info("文件大小:{}", size);
byte[] bytes = file.getBytes();
logger.info("文件二进制数据的字节数组:{}", Arrays.asList(bytes));
InputStream inputStream = file.getInputStream();
logger.info("读取文件数据的输入流对象:{}", inputStream);
Resource resource = file.getResource();
logger.info("代表当前 MultiPartFile 对象的资源对象:{}", resource);
return "target";
}
}
第四章:MultipartFile 接口(⭐)
- 文件上传表单项中的 name 属性值:
String getName();
- 文件在用户本地原始的文件名:
String getOriginalFilename();
- 文件的内容类型:
String getContentType();
- 文件是否为空:
boolean isEmpty();
- 文件大小:
long getSize();
- 文件二进制数据的字节数组:
byte[] getBytes() throws IOException;
- 读取文件数据的输入流对象:
InputStream getInputStream() throws IOException;
- 代表当前 MultiPartFile 对象的资源对象:
default Resource getResource()
- 文件转存:
void transferTo(File dest) throws IOException, IllegalStateException;
- 文件转存:
default void transferTo(Path dest) throws IOException, IllegalStateException
第五章:上传多个文件(⭐)
5.1 请求参树名不同
- 表单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<!-- 上传文件的表单 -->
<!-- method 属性:用来上传的表单,请求方式必须是 POST -->
<!--
enctype 属性:用来指定请求体的编码类型 ,默认值是 application/x-www-form-urlencoded,如果是上传文件,需要设置为 multipart/form-data。
-->
<!--
如果 enctype 设置为 multipart/form-data ,那么整个请求体都是按照二进制方式来编码,所以非上传文件的普通组件也不能按照原来的方式(request.getParameter(..))进行解码了。
-->
<form enctype="multipart/form-data" method="post" th:action="@{/upload}">
昵称:<input name="userName" type="text"><br/>
头像:<input name="avatar" type="file"><br/>
背景:<input name="background" type="file"><br/>
<button>提交</button>
</form>
</body>
</html>
- handler 方法:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class UploadHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PostMapping("/upload")
public String upload(
// 表单提交的数据仍然是请求参数,所以使用 @RequestParam 注解接收
@RequestParam("userName") String userName,
// 对于上传的文件使用 MultipartFile 类型接收其相关数据
// 浏览器端的表单用一个名字携带一个文件:使用单个 MultipartFile 类型变量接收
@RequestParam("avatar") MultipartFile avatar,
// 如果有另外一个名字携带了另外一个文件,那就用另外一个 MultipartFile 接收
@RequestParam("background") MultipartFile background) throws IOException {
logger.info("[普通表单项] userName = " + userName);
logger.info("[文件表单项] 请求参数名 = " + avatar.getName());
logger.info("[文件表单项] 原始文件名 = " + avatar.getOriginalFilename());
logger.info("[文件表单项] 判断当前上传文件是否为空 = " + (avatar.isEmpty() ? "空" : "非空"));
logger.info("[文件表单项] 当前上传文件的大小 = " + avatar.getSize());
logger.info("[文件表单项] 当前上传文件的二进制内容组成的字节数组 = " + avatar.getBytes());
logger.info("[文件表单项] 能够读取当前上传文件的输入流 = " + avatar.getInputStream());
logger.info("[另一个文件] 原始文件名 = " + background.getOriginalFilename());
return "target";
}
}
5.2 请求参数名相同
- 表单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<!-- 上传文件的表单 -->
<!-- method 属性:用来上传的表单,请求方式必须是 POST -->
<!--
enctype 属性:用来指定请求体的编码类型 ,默认值是 application/x-www-form-urlencoded,如果是上传文件,需要设置为 multipart/form-data。
-->
<!--
如果 enctype 设置为 multipart/form-data ,那么整个请求体都是按照二进制方式来编码,所以非上传文件的普通组件也不能按照原来的方式(request.getParameter(..))进行解码了。
-->
<form enctype="multipart/form-data" method="post" th:action="@{/upload}">
文件1:<input name="fileList" type="file"><br>
文件2:<input name="fileList" type="file"><br>
文件3:<input name="fileList" type="file"><br>
<button>提交</button>
</form>
</body>
</html>
- handler 方法:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class UploadHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PostMapping("/upload")
public String upload(
// 浏览器端的表单用一个名字携带多个文件:使用 List<MultipartFile> 类型变量接收
@RequestParam("fileList") List<MultipartFile> fileList) throws IOException {
fileList.forEach(file -> logger.info("file = {}", file));
return "target";
}
}
第六章:文件转存
6.1 底层机制
6.2 三种去向
6.2.1 本地转存
- ① 创建保存文件的目录:
- ② 编写转存代码:
package com.github.fairy.era.mvc.handler;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class UploadHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private ServletContext servletContext;
@PostMapping("/upload")
public String upload(
MultipartFile file) throws IOException {
// 1、准备好保存文件的目标目录
// ① File 对象要求目标路径是一个物理路径(在硬盘空间里能够直接找到文件的路径)
// ② 项目在不同系统平台上运行,要求能够自动兼容、适配不同系统平台的路径格式
// 例如:Window系统平台的路径是 D:/aaa/bbb 格式
// 例如:Linux系统平台的路径是 /ttt/uuu/vvv 格式
// 所以我们需要根据『不会变的虚拟路径』作为基准动态获取『跨平台的物理路径』
// ③ 虚拟路径:浏览器通过 Tomcat 服务器访问 Web 应用中的资源时使用的路径
String virtualPath = "/upload";
// ④调用 ServletContext 对象的方法将虚拟路径转换为真实物理路径
String realPath = servletContext.getRealPath(virtualPath);
// 2、生成保存文件的文件名
// ①为了避免同名的文件覆盖已有文件,不使用 originalFilename,所以需要我们生成文件名
// ②我们生成文件名包含两部分:文件名本身和扩展名
// ③声明变量生成文件名本身
// 获取文件的源名称
String originalFilename = file.getOriginalFilename();
// 获取文件的扩展名
String extension = FilenameUtils.getExtension(originalFilename);
logger.info("获取文件的扩展名:" + extension);
// 获取文件名(不包括扩展名)
String baseName = FilenameUtils.getBaseName(originalFilename);
logger.info("文件的基名(不包括扩展名):" + baseName);
String generatedFileName = UUID.randomUUID().toString().replace("-", "");
// ⑤拼装起来就是我们生成的整体文件名
String destFileName = generatedFileName + "." + extension;
// 3、拼接保存文件的路径,由两部分组成
String destFilePath = realPath + "/" + destFileName;
// 4、创建 File 对象,对应文件具体保存的位置
File dstFile = new File(destFilePath);
// 执行转存
file.transferTo(dstFile);
return "target";
}
}
- 缺点:
- ① Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
- ② 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
- ③ 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。
- ④ 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。
6.2.2 文件服务器(⭐)
- 总体机制:
- 好处:
- ① 不受 Web 应用重新部署影响。
- ② 在应用服务器集群环境下不会导致数据不一致。
- ③ 针对文件读写进行专门的优化,性能有保障。
- ④ 能够实现动态扩容。
- 文件服务器的类型:
- ① 第三方平台:
- 阿里的 OSS 对象存储服务。
- 七牛云。
- ② 自己搭建服务器:FastDFS 等。
6.2.3 上传到其它模块
- 这种情况肯定出现在分布式架构中,常规业务中不会这么做,采用这个方案一定是特殊情况。
- 在 MultipartFile 接口中有一个对应的方法:
public interface MultipartFile extends InputStreamSource {
...
/**
* Return a Resource representation of this MultipartFile. This can be used
* as input to the {@code RestTemplate} or the {@code WebClient} to expose
* content length and the filename along with the InputStream.
* @return this MultipartFile adapted to the Resource contract
* @since 5.1
*/
default Resource getResource() {
return new MultipartFileResource(this);
}
}
- 注释的意思:这个 Resource 对象代表当前 MultipartFile 对象,输入给 RestTemplate 或 WebClient。而 RestTemplate 或 WebClient 就是用来在 Java 程序中向服务器端发出请求的组件。