Appearance
文件上传与下载
在Web应用中,文件上传和下载是常见的功能需求。Spring MVC提供了优雅且灵活的方式来处理这些功能。本文将详细介绍如何在Spring MVC中实现文件上传与下载。
文件上传
配置MultipartResolver
在Spring MVC中,文件上传功能需要配置MultipartResolver
。Spring提供了两个实现类:
- CommonsMultipartResolver:基于Apache Commons FileUpload的实现
- StandardServletMultipartResolver:基于Servlet 3.0+的实现(推荐)
使用StandardServletMultipartResolver(Servlet 3.0+)
java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
}
在Servlet配置中(如web.xml
或Java配置):
xml
<!-- web.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>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<multipart-config>
<location>/tmp</location>
<max-file-size>5242880</max-file-size>
<max-request-size>26214400</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
Java配置:
java
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...其他配置...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp",
5 * 1024 * 1024, // 最大文件大小 (5MB)
25 * 1024 * 1024, // 最大请求大小 (25MB)
0) // 不缓存文件到内存
);
}
}
使用CommonsMultipartResolver
java
@Bean
public CommonsMultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSizePerFile(5 * 1024 * 1024); // 5MB
resolver.setMaxUploadSize(25 * 1024 * 1024); // 25MB
resolver.setDefaultEncoding("UTF-8");
return resolver;
}
Spring Boot中的配置
在Spring Boot中,文件上传功能开箱即用,只需在application.properties
或application.yml
中配置:
properties
# 应用属性配置
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=25MB
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=${java.io.tmpdir}
创建文件上传控制器
单文件上传
java
@Controller
public class FileUploadController {
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "请选择一个文件上传");
return "redirect:/uploadForm";
}
try {
// 获取文件名
String originalFilename = file.getOriginalFilename();
// 创建保存路径
String uploadDir = "uploads/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 创建目标文件
File dest = new File(dir.getAbsolutePath() + File.separator + originalFilename);
// 保存文件
file.transferTo(dest);
redirectAttributes.addFlashAttribute("message",
"文件上传成功: " + originalFilename);
} catch (IOException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("message",
"文件上传失败: " + e.getMessage());
}
return "redirect:/uploadForm";
}
@GetMapping("/uploadForm")
public String showUploadForm() {
return "uploadForm";
}
}
多文件上传
java
@PostMapping("/uploadMultiple")
public String handleMultipleFileUpload(@RequestParam("files") MultipartFile[] files,
RedirectAttributes redirectAttributes) {
if (files.length == 0) {
redirectAttributes.addFlashAttribute("message", "请选择至少一个文件上传");
return "redirect:/uploadForm";
}
StringBuilder fileNames = new StringBuilder();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
try {
String originalFilename = file.getOriginalFilename();
String uploadDir = "uploads/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
File dest = new File(dir.getAbsolutePath() + File.separator + originalFilename);
file.transferTo(dest);
fileNames.append(originalFilename).append(", ");
} catch (IOException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("message",
"部分文件上传失败: " + e.getMessage());
return "redirect:/uploadForm";
}
}
}
redirectAttributes.addFlashAttribute("message",
"文件上传成功: " + fileNames.toString());
return "redirect:/uploadForm";
}
上传表单页面
Thymeleaf模板:
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>文件上传</title>
</head>
<body>
<div th:if="${message}">
<h2 th:text="${message}"></h2>
</div>
<div>
<h3>单文件上传</h3>
<form method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="file" />
<button type="submit">上传</button>
</form>
</div>
<div>
<h3>多文件上传</h3>
<form method="POST" enctype="multipart/form-data" action="/uploadMultiple">
<input type="file" name="files" multiple />
<button type="submit">上传多文件</button>
</form>
</div>
</body>
</html>
JSP页面:
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
</head>
<body>
<c:if test="${not empty message}">
<h2>${message}</h2>
</c:if>
<div>
<h3>单文件上传</h3>
<form method="POST" enctype="multipart/form-data" action="<c:url value='/upload'/>">
<input type="file" name="file" />
<button type="submit">上传</button>
</form>
</div>
<div>
<h3>多文件上传</h3>
<form method="POST" enctype="multipart/form-data" action="<c:url value='/uploadMultiple'/>">
<input type="file" name="files" multiple />
<button type="submit">上传多文件</button>
</form>
</div>
</body>
</html>
处理大文件上传
对于大文件上传,可以使用流式处理:
java
@PostMapping("/uploadLarge")
public ResponseEntity<String> handleLargeFileUpload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择一个文件上传");
}
try {
String originalFilename = file.getOriginalFilename();
String uploadDir = "uploads/large/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
File dest = new File(dir.getAbsolutePath() + File.separator + originalFilename);
// 使用流式处理大文件
try (InputStream inputStream = file.getInputStream();
FileOutputStream outputStream = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
return ResponseEntity.ok("文件上传成功: " + originalFilename);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文件上传失败: " + e.getMessage());
}
}
文件下载
基本文件下载
java
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName,
HttpServletRequest request) {
// 加载文件作为资源
String uploadDir = "uploads/";
Path path = Paths.get(uploadDir + fileName);
Resource resource;
try {
resource = new UrlResource(path.toUri());
} catch (MalformedURLException e) {
return ResponseEntity.badRequest().build();
}
// 检查文件是否存在
if (!resource.exists() || !resource.isReadable()) {
return ResponseEntity.notFound().build();
}
// 确定文件的内容类型
String contentType = null;
try {
contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException e) {
// 无法确定内容类型
}
// 如果无法确定内容类型,则使用默认类型
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
流式下载大文件
对于大文件,可以使用流式下载:
java
@GetMapping("/downloadLarge/{fileName:.+}")
public void downloadLargeFile(@PathVariable String fileName,
HttpServletResponse response) {
String uploadDir = "uploads/large/";
File file = new File(uploadDir + fileName);
if (!file.exists()) {
try {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到");
return;
} catch (IOException e) {
e.printStackTrace();
}
}
// 设置内容类型和头信息
response.setContentType("application/octet-stream");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"");
response.setContentLengthLong(file.length());
// 使用流传输文件
try (FileInputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
带进度反馈的下载
对于需要显示下载进度的情况,可以使用如下方式:
java
@GetMapping("/downloadWithProgress/{fileName:.+}")
public ResponseEntity<StreamingResponseBody> downloadWithProgress(@PathVariable String fileName) {
String uploadDir = "uploads/";
File file = new File(uploadDir + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
StreamingResponseBody responseBody = outputStream -> {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.body(responseBody);
}
安全注意事项
在实现文件上传和下载功能时,需要注意以下安全问题:
文件上传安全
- 验证文件类型:限制允许上传的文件类型
java
private boolean isValidFileType(MultipartFile file) {
String fileName = file.getOriginalFilename();
return fileName != null && (
fileName.endsWith(".jpg") ||
fileName.endsWith(".png") ||
fileName.endsWith(".pdf"));
}
- 限制文件大小:防止DOS攻击
- 使用安全的文件名:防止路径遍历攻击
java
private String getSafeFileName(String fileName) {
return fileName.replaceAll("[^a-zA-Z0-9.-]", "_")
.replace("..", "_");
}
- 将上传目录放在Web根目录之外:防止直接访问上传的文件
- 使用病毒扫描API:检测上传文件中的恶意代码
文件下载安全
- 验证用户权限:确保用户有权下载请求的文件
- 防止路径遍历:验证文件路径
java
private boolean isValidFilePath(String fileName) {
Path requestedPath = Paths.get(fileName).normalize();
Path baseDir = Paths.get("uploads").normalize().toAbsolutePath();
Path resolvedPath = baseDir.resolve(requestedPath).normalize().toAbsolutePath();
return resolvedPath.startsWith(baseDir);
}
- 设置适当的内容类型:防止浏览器将脚本作为HTML执行
实用技巧
断点续传
java
@GetMapping("/resumable/{fileName:.+}")
public ResponseEntity<Resource> resumableDownload(
@PathVariable String fileName,
@RequestHeader(value = "Range", required = false) String rangeHeader) {
String uploadDir = "uploads/";
Path path = Paths.get(uploadDir + fileName);
Resource resource;
try {
resource = new UrlResource(path.toUri());
} catch (MalformedURLException e) {
return ResponseEntity.badRequest().build();
}
if (!resource.exists() || !resource.isReadable()) {
return ResponseEntity.notFound().build();
}
long fileLength;
try {
fileLength = resource.contentLength();
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"");
headers.add(HttpHeaders.ACCEPT_RANGES, "bytes");
// 处理范围请求
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
String[] ranges = rangeHeader.substring(6).split("-");
long start = Long.parseLong(ranges[0]);
long end = ranges.length > 1 ? Long.parseLong(ranges[1]) : fileLength - 1;
if (start >= fileLength) {
return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + fileLength)
.build();
}
if (end >= fileLength) {
end = fileLength - 1;
}
long contentLength = end - start + 1;
try {
InputStream inputStream = resource.getInputStream();
inputStream.skip(start);
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(contentLength)
.header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileLength)
.body(new InputStreamResource(new BoundedInputStream(inputStream, contentLength)));
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// 正常下载
return ResponseEntity.ok()
.headers(headers)
.contentLength(fileLength)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
// 辅助类,限制输入流大小
class BoundedInputStream extends InputStream {
private final InputStream delegate;
private long remaining;
public BoundedInputStream(InputStream delegate, long limit) {
this.delegate = delegate;
this.remaining = limit;
}
@Override
public int read() throws IOException {
if (remaining <= 0) {
return -1;
}
int result = delegate.read();
if (result != -1) {
remaining--;
}
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (remaining <= 0) {
return -1;
}
int maxLen = (int) Math.min(len, remaining);
int result = delegate.read(b, off, maxLen);
if (result != -1) {
remaining -= result;
}
return result;
}
@Override
public void close() throws IOException {
delegate.close();
}
}
异步文件上传与下载
java
@PostMapping("/uploadAsync")
public DeferredResult<ResponseEntity<String>> uploadAsync(@RequestParam("file") MultipartFile file) {
DeferredResult<ResponseEntity<String>> deferredResult = new DeferredResult<>();
CompletableFuture.runAsync(() -> {
try {
// 文件保存逻辑
String originalFilename = file.getOriginalFilename();
String uploadDir = "uploads/";
Path path = Paths.get(uploadDir);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
Files.copy(file.getInputStream(),
path.resolve(originalFilename),
StandardCopyOption.REPLACE_EXISTING);
deferredResult.setResult(
ResponseEntity.ok("文件上传成功: " + originalFilename));
} catch (Exception e) {
deferredResult.setResult(
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文件上传失败: " + e.getMessage()));
}
});
return deferredResult;
}
总结
Spring MVC提供了完善的文件上传和下载支持,可以满足各种复杂场景的需求。在实现这些功能时,应注意以下几点:
- 适当配置
MultipartResolver
以支持文件上传 - 根据需求选择合适的文件上传和下载策略
- 对于大文件传输,使用流式处理
- 注意文件上传和下载的安全隐患
- 通过异步处理和断点续传等技术提高用户体验