⭐表示重要。

第一章:表单(⭐)

  • 要求:
  • ① 请求方式必须是 POST。
  • ② 请求体的编码方式必须是 multipart/form-data (通过 form 标签的 enctype 属性设置)。
  • ③ 使用 input 标签,type 属性设置为 file 来生成文件上传域。

  • 示例:

  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <!-- 上传文件的表单 -->
  9. <!-- method 属性:用来上传的表单,请求方式必须是 POST -->
  10. <!--
  11. enctype 属性:用来指定请求体的编码类型 ,默认值是 application/x-www-form-urlencoded,如果是上传文件,需要设置为 multipart/form-data。
  12. -->
  13. <!--
  14. 如果 enctype 设置为 multipart/form-data ,那么整个请求体都是按照二进制方式来编码,所以非上传文件的普通组件也不能按照原来的方式(request.getParameter(..))进行解码了。
  15. -->
  16. <form enctype="multipart/form-data" method="post" th:action="@{/upload}">
  17. 用户名:<input name="userName" type="text"><br/>
  18. 上传文件:<input name="file" type="file"><br/>
  19. <button>提交</button>
  20. </form>
  21. </body>
  22. </html>

第二章:SpringMVC 环境要求(⭐)

2.1 导入依赖

  • pom.xml
  1. <dependency>
  2. <groupId>commons-fileupload</groupId>
  3. <artifactId>commons-fileupload</artifactId>
  4. <version>1.4</version>
  5. </dependency>
  • 完整的pom.xml
  1. <!-- SpringMVC -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-webmvc</artifactId>
  5. <version>5.3.12</version>
  6. </dependency>
  7. <!-- 日志 -->
  8. <dependency>
  9. <groupId>ch.qos.logback</groupId>
  10. <artifactId>logback-classic</artifactId>
  11. <version>1.2.6</version>
  12. </dependency>
  13. <!-- ServletAPI -->
  14. <dependency>
  15. <groupId>javax.servlet</groupId>
  16. <artifactId>javax.servlet-api</artifactId>
  17. <version>4.0.1</version>
  18. <scope>provided</scope>
  19. </dependency>
  20. <!-- Spring5和Thymeleaf整合包 -->
  21. <dependency>
  22. <groupId>org.thymeleaf</groupId>
  23. <artifactId>thymeleaf-spring5</artifactId>
  24. <version>3.0.12.RELEASE</version>
  25. </dependency>
  26. <dependency>
  27. <groupId>com.fasterxml.jackson.core</groupId>
  28. <artifactId>jackson-databind</artifactId>
  29. <version>2.13.0</version>
  30. </dependency>
  31. <!-- 文件上传 -->
  32. <dependency>
  33. <groupId>commons-fileupload</groupId>
  34. <artifactId>commons-fileupload</artifactId>
  35. <version>1.4</version>
  36. </dependency>

2.2 配置

  • 在 SpringMVC 的配置文件中加入 multipart 类型数据的解析器:
  1. <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  2. <!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
  3. <property name="defaultEncoding" value="UTF-8"/>
  4. </bean>
  • 完整的 springmvc.xml :
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"
  5. 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">
  6. <!-- 自动扫描包 -->
  7. <context:component-scan base-package="com.github.fairy.era.mvc"></context:component-scan>
  8. <!-- 配置视图解析器 -->
  9. <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
  10. <property name="order" value="1"/>
  11. <property name="characterEncoding" value="UTF-8"/>
  12. <property name="templateEngine">
  13. <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
  14. <property name="templateResolver">
  15. <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
  16. <!-- 物理视图:视图前缀+逻辑视图+视图后缀 -->
  17. <!-- 视图前缀 -->
  18. <property name="prefix" value="/WEB-INF/templates/"/>
  19. <!-- 视图后缀 -->
  20. <property name="suffix" value=".html"/>
  21. <property name="templateMode" value="HTML5"/>
  22. <property name="characterEncoding" value="UTF-8"/>
  23. </bean>
  24. </property>
  25. </bean>
  26. </property>
  27. </bean>
  28. <mvc:annotation-driven/>
  29. <mvc:default-servlet-handler/>
  30. <mvc:view-controller path="/" view-name="portal"/>
  31. <!-- 配置CommonsMultipartResolver -->
  32. <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  33. <!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
  34. <property name="defaultEncoding" value="UTF-8"/>
  35. </bean>
  36. </beans>

第三章:handler方法接收数据(⭐)

  • 表单提交的数据如果是请求参数,依然可以使用 @RequestParam 注解接收。
  • 表单提交的数据如果是上传文件,使用 MultipartFile 类型接收。

  • 示例:

  1. package com.github.fairy.era.mvc.handler;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.core.io.Resource;
  5. import org.springframework.stereotype.Controller;
  6. import org.springframework.web.bind.annotation.PostMapping;
  7. import org.springframework.web.bind.annotation.RequestParam;
  8. import org.springframework.web.multipart.MultipartFile;
  9. import java.io.IOException;
  10. import java.io.InputStream;
  11. import java.util.Arrays;
  12. /**
  13. * @author 许大仙
  14. * @version 1.0
  15. * @since 2021-11-11 14:58
  16. */
  17. @Controller
  18. public class UploadHandler {
  19. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  20. @PostMapping("/upload")
  21. public String upload(
  22. // 表单提交的数据仍然是请求参数,所以使用 @RequestParam 注解接收
  23. @RequestParam("userName") String userName,
  24. // 对于上传的文件使用 MultipartFile 类型接收其相关数据
  25. @RequestParam("file") MultipartFile file) throws IOException {
  26. logger.info("UploadHandler.upload的请求参数中的是userName :{}", userName);
  27. String name = file.getName();
  28. logger.info("文件上传表单项中的 name 属性值:{}", name);
  29. String originalFilename = file.getOriginalFilename();
  30. logger.info("文件在用户本地原始的文件名:{}", originalFilename);
  31. String contentType = file.getContentType();
  32. logger.info("文件的内容类型:{}", contentType);
  33. boolean empty = file.isEmpty();
  34. logger.info("文件是否为空:{}", empty);
  35. long size = file.getSize();
  36. logger.info("文件大小:{}", size);
  37. byte[] bytes = file.getBytes();
  38. logger.info("文件二进制数据的字节数组:{}", Arrays.asList(bytes));
  39. InputStream inputStream = file.getInputStream();
  40. logger.info("读取文件数据的输入流对象:{}", inputStream);
  41. Resource resource = file.getResource();
  42. logger.info("代表当前 MultiPartFile 对象的资源对象:{}", resource);
  43. return "target";
  44. }
  45. }

第四章:MultipartFile 接口(⭐)

MultipartFile 接口.png

  • 文件上传表单项中的 name 属性值:
  1. String getName();
  • 文件在用户本地原始的文件名:
  1. String getOriginalFilename();
  • 文件的内容类型:
  1. String getContentType();
  • 文件是否为空:
  1. boolean isEmpty();
  • 文件大小:
  1. long getSize();
  • 文件二进制数据的字节数组:
  1. byte[] getBytes() throws IOException;
  • 读取文件数据的输入流对象:
  1. InputStream getInputStream() throws IOException;
  • 代表当前 MultiPartFile 对象的资源对象:
  1. default Resource getResource()
  • 文件转存:
  1. void transferTo(File dest) throws IOException, IllegalStateException;
  • 文件转存:
  1. default void transferTo(Path dest) throws IOException, IllegalStateException

第五章:上传多个文件(⭐)

5.1 请求参树名不同

  • 表单:
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <!-- 上传文件的表单 -->
  9. <!-- method 属性:用来上传的表单,请求方式必须是 POST -->
  10. <!--
  11. enctype 属性:用来指定请求体的编码类型 ,默认值是 application/x-www-form-urlencoded,如果是上传文件,需要设置为 multipart/form-data。
  12. -->
  13. <!--
  14. 如果 enctype 设置为 multipart/form-data ,那么整个请求体都是按照二进制方式来编码,所以非上传文件的普通组件也不能按照原来的方式(request.getParameter(..))进行解码了。
  15. -->
  16. <form enctype="multipart/form-data" method="post" th:action="@{/upload}">
  17. 昵称:<input name="userName" type="text"><br/>
  18. 头像:<input name="avatar" type="file"><br/>
  19. 背景:<input name="background" type="file"><br/>
  20. <button>提交</button>
  21. </form>
  22. </body>
  23. </html>
  • handler 方法:
  1. package com.github.fairy.era.mvc.handler;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.stereotype.Controller;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestParam;
  7. import org.springframework.web.multipart.MultipartFile;
  8. import java.io.IOException;
  9. /**
  10. * @author 许大仙
  11. * @version 1.0
  12. * @since 2021-11-11 14:58
  13. */
  14. @Controller
  15. public class UploadHandler {
  16. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  17. @PostMapping("/upload")
  18. public String upload(
  19. // 表单提交的数据仍然是请求参数,所以使用 @RequestParam 注解接收
  20. @RequestParam("userName") String userName,
  21. // 对于上传的文件使用 MultipartFile 类型接收其相关数据
  22. // 浏览器端的表单用一个名字携带一个文件:使用单个 MultipartFile 类型变量接收
  23. @RequestParam("avatar") MultipartFile avatar,
  24. // 如果有另外一个名字携带了另外一个文件,那就用另外一个 MultipartFile 接收
  25. @RequestParam("background") MultipartFile background) throws IOException {
  26. logger.info("[普通表单项] userName = " + userName);
  27. logger.info("[文件表单项] 请求参数名 = " + avatar.getName());
  28. logger.info("[文件表单项] 原始文件名 = " + avatar.getOriginalFilename());
  29. logger.info("[文件表单项] 判断当前上传文件是否为空 = " + (avatar.isEmpty() ? "空" : "非空"));
  30. logger.info("[文件表单项] 当前上传文件的大小 = " + avatar.getSize());
  31. logger.info("[文件表单项] 当前上传文件的二进制内容组成的字节数组 = " + avatar.getBytes());
  32. logger.info("[文件表单项] 能够读取当前上传文件的输入流 = " + avatar.getInputStream());
  33. logger.info("[另一个文件] 原始文件名 = " + background.getOriginalFilename());
  34. return "target";
  35. }
  36. }

5.2 请求参数名相同

  • 表单:
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <!-- 上传文件的表单 -->
  9. <!-- method 属性:用来上传的表单,请求方式必须是 POST -->
  10. <!--
  11. enctype 属性:用来指定请求体的编码类型 ,默认值是 application/x-www-form-urlencoded,如果是上传文件,需要设置为 multipart/form-data。
  12. -->
  13. <!--
  14. 如果 enctype 设置为 multipart/form-data ,那么整个请求体都是按照二进制方式来编码,所以非上传文件的普通组件也不能按照原来的方式(request.getParameter(..))进行解码了。
  15. -->
  16. <form enctype="multipart/form-data" method="post" th:action="@{/upload}">
  17. 文件1:<input name="fileList" type="file"><br>
  18. 文件2:<input name="fileList" type="file"><br>
  19. 文件3:<input name="fileList" type="file"><br>
  20. <button>提交</button>
  21. </form>
  22. </body>
  23. </html>
  • handler 方法:
  1. package com.github.fairy.era.mvc.handler;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.stereotype.Controller;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestParam;
  7. import org.springframework.web.multipart.MultipartFile;
  8. import java.io.IOException;
  9. import java.util.List;
  10. /**
  11. * @author 许大仙
  12. * @version 1.0
  13. * @since 2021-11-11 14:58
  14. */
  15. @Controller
  16. public class UploadHandler {
  17. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  18. @PostMapping("/upload")
  19. public String upload(
  20. // 浏览器端的表单用一个名字携带多个文件:使用 List<MultipartFile> 类型变量接收
  21. @RequestParam("fileList") List<MultipartFile> fileList) throws IOException {
  22. fileList.forEach(file -> logger.info("file = {}", file));
  23. return "target";
  24. }
  25. }

第六章:文件转存

6.1 底层机制

文件转存的底层机制.png

6.2 三种去向

6.2.1 本地转存

本地转存.png

  • ① 创建保存文件的目录:

创建保存文件的目录.png

  • ② 编写转存代码:
  1. package com.github.fairy.era.mvc.handler;
  2. import org.apache.commons.io.FilenameUtils;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Controller;
  7. import org.springframework.web.bind.annotation.PostMapping;
  8. import org.springframework.web.multipart.MultipartFile;
  9. import javax.servlet.ServletContext;
  10. import java.io.File;
  11. import java.io.IOException;
  12. import java.util.UUID;
  13. /**
  14. * @author 许大仙
  15. * @version 1.0
  16. * @since 2021-11-11 14:58
  17. */
  18. @Controller
  19. public class UploadHandler {
  20. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  21. @Autowired
  22. private ServletContext servletContext;
  23. @PostMapping("/upload")
  24. public String upload(
  25. MultipartFile file) throws IOException {
  26. // 1、准备好保存文件的目标目录
  27. // ① File 对象要求目标路径是一个物理路径(在硬盘空间里能够直接找到文件的路径)
  28. // ② 项目在不同系统平台上运行,要求能够自动兼容、适配不同系统平台的路径格式
  29. // 例如:Window系统平台的路径是 D:/aaa/bbb 格式
  30. // 例如:Linux系统平台的路径是 /ttt/uuu/vvv 格式
  31. // 所以我们需要根据『不会变的虚拟路径』作为基准动态获取『跨平台的物理路径』
  32. // ③ 虚拟路径:浏览器通过 Tomcat 服务器访问 Web 应用中的资源时使用的路径
  33. String virtualPath = "/upload";
  34. // ④调用 ServletContext 对象的方法将虚拟路径转换为真实物理路径
  35. String realPath = servletContext.getRealPath(virtualPath);
  36. // 2、生成保存文件的文件名
  37. // ①为了避免同名的文件覆盖已有文件,不使用 originalFilename,所以需要我们生成文件名
  38. // ②我们生成文件名包含两部分:文件名本身和扩展名
  39. // ③声明变量生成文件名本身
  40. // 获取文件的源名称
  41. String originalFilename = file.getOriginalFilename();
  42. // 获取文件的扩展名
  43. String extension = FilenameUtils.getExtension(originalFilename);
  44. logger.info("获取文件的扩展名:" + extension);
  45. // 获取文件名(不包括扩展名)
  46. String baseName = FilenameUtils.getBaseName(originalFilename);
  47. logger.info("文件的基名(不包括扩展名):" + baseName);
  48. String generatedFileName = UUID.randomUUID().toString().replace("-", "");
  49. // ⑤拼装起来就是我们生成的整体文件名
  50. String destFileName = generatedFileName + "." + extension;
  51. // 3、拼接保存文件的路径,由两部分组成
  52. String destFilePath = realPath + "/" + destFileName;
  53. // 4、创建 File 对象,对应文件具体保存的位置
  54. File dstFile = new File(destFilePath);
  55. // 执行转存
  56. file.transferTo(dstFile);
  57. return "target";
  58. }
  59. }
  • 缺点:
  • ① Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
  • ② 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
  • ③ 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。
  • ④ 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。

本地转存的缺点.png

6.2.2 文件服务器(⭐)

  • 总体机制:

文件服务器.png

  • 好处:
  • ① 不受 Web 应用重新部署影响。
  • ② 在应用服务器集群环境下不会导致数据不一致。
  • ③ 针对文件读写进行专门的优化,性能有保障。
  • ④ 能够实现动态扩容。

文件服务器的好处.png

  • 文件服务器的类型:
  • ① 第三方平台:
    • 阿里的 OSS 对象存储服务。
    • 七牛云。
  • ② 自己搭建服务器:FastDFS 等。

6.2.3 上传到其它模块

  • 这种情况肯定出现在分布式架构中,常规业务中不会这么做,采用这个方案一定是特殊情况。

上传到其他模块.png

  • 在 MultipartFile 接口中有一个对应的方法:
  1. public interface MultipartFile extends InputStreamSource {
  2. ...
  3. /**
  4. * Return a Resource representation of this MultipartFile. This can be used
  5. * as input to the {@code RestTemplate} or the {@code WebClient} to expose
  6. * content length and the filename along with the InputStream.
  7. * @return this MultipartFile adapted to the Resource contract
  8. * @since 5.1
  9. */
  10. default Resource getResource() {
  11. return new MultipartFileResource(this);
  12. }
  13. }
  • 注释的意思:这个 Resource 对象代表当前 MultipartFile 对象,输入给 RestTemplate 或 WebClient。而 RestTemplate 或 WebClient 就是用来在 Java 程序中向服务器端发出请求的组件。