【SpringMVC笔记】 - 8 - 文件上传与下载
一、文件上传
1. 核心概述
文件上传是 Web 开发中常见的功能,SpringMVC 针对文件上传提供了便捷的支持。在 SpringMVC 6 版本中,文件上传的配置方式与 Spring5 有显著区别:Spring6 移除了CommonsMultipartResolver类,不再依赖commons-fileupload依赖包,而是通过在web.xml中为 DispatcherServlet 配置multipart-config节点实现文件上传的参数控制。
2. 环境准备与核心依赖
SpringMVC 6 实现文件上传无需额外添加如下依赖(Spring5 需要):
xml
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
仅需保证 SpringMVC 6 核心依赖正常引入即可。
3. 前端页面编写
文件上传的前端表单需满足三个核心要求:
- 请求方式为 POST
- 表单编码类型
enctype="multipart/form-data"(默认application/x-www-form-urlencoded无法传输文件二进制数据) - 文件选择组件
type="file",并指定name属性(后端通过该属性接收文件)
示例代码:
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<!--文件上传表单-->
<form th:action="@{/file/up}" method="post" enctype="multipart/form-data">
文件:<input type="file" name="fileName"/><br>
<input type="submit" value="上传">
</form>
</body>
</html>
4. web.xml 配置(核心)
在 SpringMVC 6 中,需在 DispatcherServlet 的配置中添加multipart-config节点,用于控制文件上传的大小限制:
xml
<!--前端控制器-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 服务器启动时初始化DispatcherServlet,提升首次请求效率 -->
<load-on-startup>1</load-on-startup>
<!-- 文件上传配置 -->
<multipart-config>
<!-- 单个文件最大大小:单位字节(示例为100KB) -->
<max-file-size>102400</max-file-size>
<!-- 整个表单所有文件总大小:单位字节(示例为100KB) -->
<max-request-size>102400</max-request-size>
<!-- 文件大小阈值,超过该值将写入临时文件,0表示始终写入磁盘 -->
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
注意:Spring5 中需在 springmvc.xml 中配置
CommonsMultipartResolver,Spring6 已废弃该类,统一使用multipart-config配置。
5. SpringMVC 核心配置(springmvc.xml)
基础的 SpringMVC 配置需保证组件扫描、视图解析器、注解驱动等核心配置生效,示例:
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/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"
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">
<!-- 组件扫描:扫描Controller所在包 -->
<context:component-scan base-package="com.zzz.controller"/>
<!-- 配置Thymeleaf视图解析器 -->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="characterEncoding" value="UTF-8"/>
<property name="order" value="1"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<!-- 视图控制器:访问/直接跳转index页面 -->
<mvc:view-controller path="/" view-name="index"/>
<!-- 静态资源处理 -->
<mvc:default-servlet-handler/>
<!-- 开启注解驱动 -->
<mvc:annotation-driven/>
</beans>
6. Controller 编写(核心业务逻辑)
文件上传的核心逻辑是接收前端传递的MultipartFile对象,将文件流写入服务器指定目录,关键步骤:
- 接收
MultipartFile参数(通过@RequestParam绑定前端name属性); - 获取文件原始名称,生成唯一文件名(避免文件覆盖);
- 获取服务器存储目录,不存在则创建;
- 通过输入流读取文件、输出流写入服务器目录;
- 关闭流资源,返回跳转页面。
示例代码:
java
package com.zzz.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;
@Controller
public class FileController {
/**
* 文件上传接口
* @param multipartFile 前端传递的文件对象(name="fileName")
* @param request 请求对象,用于获取服务器真实路径
* @return 跳转成功页面
* @throws IOException 流操作异常
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String upload(@RequestParam("filename") MultipartFile multipartFile,
HttpServletRequest request) throws IOException {
// 1. 获取请求参数名(前端input的name属性值)
String paramName = multipartFile.getName();
System.out.println("请求参数名:" + paramName);
// 2. 获取文件原始名称(如:test.png)
String originalFilename = multipartFile.getOriginalFilename();
System.out.println("文件原始名称:" + originalFilename);
// 3. 生成唯一文件名:UUID + 文件后缀(避免文件覆盖)
String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String uniqueFileName = UUID.randomUUID().toString() + fileSuffix;
// 4. 获取服务器存储目录(WEB-INF/upload,不存在则创建)
String realPath = request.getServletContext().getRealPath("/upload");
File uploadDir = new File(realPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs(); // 多级目录创建
}
// 5. 构建目标文件对象
File destFile = new File(uploadDir.getAbsolutePath() + "/" + uniqueFileName);
// 6. 读取文件流并写入服务器
// 输入流(读取前端上传的文件)
InputStream inputStream = multipartFile.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
// 输出流(写入服务器目录)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
// 7. 缓冲区读写(提高IO效率)
byte[] buffer = new byte[1024]; // 1KB缓冲区
int len; // 每次读取的字节数
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
// 8. 资源释放
bos.flush(); // 刷新缓冲区
bos.close();
bis.close();
inputStream.close();
// 9. 跳转成功页面
return "ok";
}
}
7. 关键注意事项
- 文件名唯一性:必须使用 UUID / 时间戳等生成唯一文件名,否则同名文件会被覆盖;
- 流资源释放:必须关闭输入 / 输出流,避免资源泄漏;
- 文件大小限制 :
multipart-config中的大小单位为字节,需根据业务调整; - 编码问题:配置字符编码过滤器,避免文件名中文乱码:
xml
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
二、文件下载
1. 核心概述
文件下载的核心是通过ResponseEntity<byte[]>封装文件字节数组、响应头信息,SpringMVC 会自动将该对象转换为下载响应,关键是设置正确的响应头(文件类型、下载文件名)。
2. 前端页面编写
通过超链接触发下载请求,示例:
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h1>文件上传与下载</h1>
<hr>
<!--文件上传表单-->
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
文件 :<input type="file" name="filename">
<input type="submit" value="上传">
</form>
<hr>
<!--文件下载链接-->
<a th:href="@{/download}">下载</a>
</body>
</html>
3. Controller 编写(下载核心逻辑)
下载逻辑关键步骤:
- 定位服务器上的目标文件;
- 创建
HttpHeaders,设置响应类型为二进制流(APPLICATION_OCTET_STREAM); - 设置
Content-Disposition响应头,指定下载文件名; - 读取文件字节数组,封装为
ResponseEntity返回。
示例代码:
java
package com.zzz.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@Controller
public class FileController {
/**
* 文件下载接口
* @param request 请求对象,用于获取服务器文件路径
* @return ResponseEntity封装的文件字节数组与响应头
* @throws IOException 文件读取异常
*/
@GetMapping("/download")
public ResponseEntity<byte[]> download(HttpServletRequest request) throws IOException {
// 1. 定位服务器上的目标文件(示例:upload目录下的OIP.png)
String filePath = request.getServletContext().getRealPath("/upload/OIP.png");
File file = new File(filePath);
if (!file.exists()) {
// 可选:文件不存在时返回404
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
// 2. 构建响应头
HttpHeaders headers = new HttpHeaders();
// 设置响应内容类型:二进制流(通用文件类型)
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 设置下载文件名(attachment表示附件下载,inline表示在线预览)
headers.setContentDispositionFormData("attachment", file.getName());
// 3. 读取文件字节数组,封装ResponseEntity返回
byte[] fileBytes = Files.readAllBytes(file.toPath());
return new ResponseEntity<>(fileBytes, headers, HttpStatus.OK);
}
}
4. 关键注意事项
- 响应头配置:
ContentType设置为APPLICATION_OCTET_STREAM,表示二进制流,兼容所有文件类型;Content-Disposition的attachment表示以附件形式下载,若改为inline则在线预览(如图片、文本);
- 文件路径校验 :必须校验文件是否存在,避免
FileNotFoundException; - 中文文件名乱码:若文件名包含中文,需对文件名进行 URL 编码:
java
// 解决中文文件名乱码
String encodedFileName = java.net.URLEncoder.encode(file.getName(), "UTF-8");
headers.setContentDispositionFormData("attachment", encodedFileName);
- 大文件下载优化 :若下载大文件,不建议使用
Files.readAllBytes()(一次性加载到内存),可通过StreamingResponseBody实现流式下载:
java
@GetMapping("/download/large")
public ResponseEntity<StreamingResponseBody> downloadLargeFile(HttpServletRequest request) throws FileNotFoundException {
String filePath = request.getServletContext().getRealPath("/upload/largeFile.zip");
File file = new File(filePath);
if (!file.exists()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", java.net.URLEncoder.encode(file.getName(), "UTF-8"));
// 流式下载
StreamingResponseBody responseBody = outputStream -> {
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] buffer = new byte[1024 * 1024]; // 1MB缓冲区
int len;
while ((len = bis.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
};
return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);
}
三、完整测试流程
1. 上传测试
- 启动 Web 服务器(如 Tomcat);
- 访问首页
http://localhost:8080/,选择本地文件点击 "上传"; - 查看服务器
WEB-INF/upload目录,确认文件已上传且文件名是 UUID 格式; - 页面跳转至ok.html,显示 "OK" 表示上传成功。
2. 下载测试
- 点击页面中的 "下载" 链接;
- 浏览器弹出文件下载弹窗,确认文件名和大小正确;
- 下载完成后,校验文件完整性(如打开图片、解压压缩包等)。
四、常见问题排查
1. 上传文件为空
- 检查前端表单
enctype="multipart/form-data"是否配置; - 检查
MultipartFile参数的@RequestParam名称与前端name属性是否一致; - 检查
multipart-config的文件大小限制是否过小,导致文件被拦截。
2. 上传文件目录不存在
- 确认使用
mkdirs()(创建多级目录)而非mkdir()(仅创建单级目录); - 检查服务器目录权限,确保应用有写入权限。
3. 下载文件乱码
- 对下载文件名进行 URL 编码;
- 检查字符编码过滤器是否配置生效。
4. Spring6 与 Spring5 兼容问题
- Spring5 需配置
CommonsMultipartResolver,Spring6 需配置multipart-config; - Spring6 使用 Jakarta EE API(
jakarta.servlet),Spring5 使用 Java EE API(javax.servlet),注意包导入。
五、总结
- SpringMVC 6 文件上传无需依赖
commons-fileupload,通过web.xml的multipart-config配置文件大小限制; - 文件上传核心是处理
MultipartFile对象,通过 IO 流将文件写入服务器,需保证文件名唯一; - 文件下载核心是通过
ResponseEntity<byte[]>封装文件字节和响应头,支持附件下载 / 在线预览; - 开发中需注意资源释放、编码问题、文件路径校验,大文件下载建议使用流式传输。