本文目录导读:

我来为您提供一个完整的Spring Boot文件上传案例,包括单文件上传、多文件上传和文件下载功能。
项目依赖配置 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>file-upload-demo</artifactId>
<version>1.0.0</version>
<name>file-upload-demo</name>
<description>文件上传下载示例</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
配置文件 (application.yml)
server:
port: 8080
spring:
servlet:
multipart:
# 单个文件最大大小
max-file-size: 10MB
# 单个请求最大大小
max-request-size: 100MB
# 文件上传临时目录
location: ${java.io.tmpdir}
file:
# 文件存储路径
upload-path: D:/uploads/
# 允许的文件类型
allowed-types: jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,txt
# 单个文件最大大小(MB)
max-size: 10
文件上传控制器
package com.example.fileupload.controller;
import com.example.fileupload.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${file.upload-path}")
private String uploadPath;
@Value("${file.allowed-types}")
private String allowedTypes;
/**
* 单文件上传
*/
@PostMapping("/upload")
public Map<String, Object> uploadFile(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
// 检查文件是否为空
if (file.isEmpty()) {
result.put("code", 400);
result.put("message", "文件不能为空");
return result;
}
// 检查文件大小
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
result.put("code", 400);
result.put("message", "文件大小不能超过10MB");
return result;
}
// 检查文件类型
String originalFilename = file.getOriginalFilename();
String extension = StringUtils.getFilenameExtension(originalFilename);
if (!isAllowedType(extension)) {
result.put("code", 400);
result.put("message", "不支持的文件类型: " + extension);
return result;
}
// 生成唯一文件名
String newFileName = generateFileName(originalFilename);
// 创建目录(如果不存在)
Path uploadDir = Paths.get(uploadPath);
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
// 保存文件
Path filePath = uploadDir.resolve(newFileName);
file.transferTo(filePath.toFile());
result.put("code", 200);
result.put("message", "上传成功");
result.put("data", new HashMap<String, String>() {{
put("fileName", originalFilename);
put("newFileName", newFileName);
put("fileSize", FileUtil.formatFileSize(file.getSize()));
put("fileUrl", "/file/download/" + newFileName);
}});
} catch (Exception e) {
log.error("文件上传失败", e);
result.put("code", 500);
result.put("message", "文件上传失败: " + e.getMessage());
}
return result;
}
/**
* 多文件上传
*/
@PostMapping("/batch/upload")
public Map<String, Object> batchUpload(@RequestParam("files") MultipartFile[] files) {
Map<String, Object> result = new HashMap<>();
if (files.length == 0) {
result.put("code", 400);
result.put("message", "请选择要上传的文件");
return result;
}
java.util.List<Map<String, Object>> fileList = new java.util.ArrayList<>();
int successCount = 0;
for (MultipartFile file : files) {
Map<String, Object> fileResult = new HashMap<>();
try {
if (file.isEmpty()) {
fileResult.put("status", "failed");
fileResult.put("message", "文件为空");
} else {
String originalFilename = file.getOriginalFilename();
String extension = StringUtils.getFilenameExtension(originalFilename);
if (!isAllowedType(extension)) {
fileResult.put("status", "failed");
fileResult.put("message", "不支持的文件类型: " + extension);
} else {
// 生成唯一文件名
String newFileName = generateFileName(originalFilename);
// 保存文件
Path uploadDir = Paths.get(uploadPath);
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
Path filePath = uploadDir.resolve(newFileName);
file.transferTo(filePath.toFile());
fileResult.put("status", "success");
fileResult.put("fileName", originalFilename);
fileResult.put("newFileName", newFileName);
fileResult.put("fileUrl", "/file/download/" + newFileName);
successCount++;
}
}
} catch (Exception e) {
log.error("文件上传失败: {}", file.getOriginalFilename(), e);
fileResult.put("status", "failed");
fileResult.put("message", e.getMessage());
}
fileList.add(fileResult);
}
result.put("code", 200);
result.put("message", "上传完成");
result.put("total", files.length);
result.put("successCount", successCount);
result.put("failedCount", files.length - successCount);
result.put("data", fileList);
return result;
}
/**
* 文件下载
*/
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
Path filePath = Paths.get(uploadPath).resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
// 提取原始文件名(如果有的话)
String originalFileName = getOriginalFileName(fileName);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(originalFileName, "UTF-8") + "\"")
.body(resource);
} catch (Exception e) {
log.error("文件下载失败", e);
return ResponseEntity.status(500).build();
}
}
/**
* 在线预览文件
*/
@GetMapping("/preview/{fileName}")
public ResponseEntity<Resource> previewFile(@PathVariable String fileName) {
try {
Path filePath = Paths.get(uploadPath).resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
// 根据扩展名判断MIME类型
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.body(resource);
} catch (Exception e) {
log.error("文件预览失败", e);
return ResponseEntity.status(500).build();
}
}
/**
* 删除文件
*/
@DeleteMapping("/delete/{fileName}")
public Map<String, Object> deleteFile(@PathVariable String fileName) {
Map<String, Object> result = new HashMap<>();
try {
Path filePath = Paths.get(uploadPath).resolve(fileName).normalize();
boolean deleted = Files.deleteIfExists(filePath);
if (deleted) {
result.put("code", 200);
result.put("message", "文件删除成功");
} else {
result.put("code", 404);
result.put("message", "文件不存在");
}
} catch (Exception e) {
log.error("文件删除失败", e);
result.put("code", 500);
result.put("message", "文件删除失败: " + e.getMessage());
}
return result;
}
/**
* 获取文件列表
*/
@GetMapping("/list")
public Map<String, Object> getFileList() {
Map<String, Object> result = new HashMap<>();
try {
Path uploadDir = Paths.get(uploadPath);
if (!Files.exists(uploadDir)) {
result.put("code", 200);
result.put("data", new java.util.ArrayList<>());
return result;
}
java.util.List<Map<String, Object>> fileList = new java.util.ArrayList<>();
Files.list(uploadDir).forEach(path -> {
if (Files.isRegularFile(path)) {
try {
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("fileName", path.getFileName().toString());
fileInfo.put("fileSize", FileUtil.formatFileSize(Files.size(path)));
fileInfo.put("createTime", Files.getLastModifiedTime(path).toString());
fileInfo.put("downloadUrl", "/file/download/" + path.getFileName());
fileList.add(fileInfo);
} catch (IOException e) {
log.error("获取文件信息失败", e);
}
}
});
result.put("code", 200);
result.put("data", fileList);
} catch (Exception e) {
log.error("获取文件列表失败", e);
result.put("code", 500);
result.put("message", "获取文件列表失败: " + e.getMessage());
}
return result;
}
/**
* 检查文件类型是否允许
*/
private boolean isAllowedType(String extension) {
if (StringUtils.isEmpty(extension)) {
return false;
}
String[] allowedTypesArray = allowedTypes.split(",");
for (String type : allowedTypesArray) {
if (type.trim().equalsIgnoreCase(extension)) {
return true;
}
}
return false;
}
/**
* 生成唯一文件名
*/
private String generateFileName(String originalFilename) {
String extension = StringUtils.getFilenameExtension(originalFilename);
return UUID.randomUUID().toString().replace("-", "") + "." + extension;
}
/**
* 获取原始文件名(从文件名映射表中获取)
*/
private String getOriginalFileName(String fileName) {
// 这里可以查询数据库获取原始文件名
// 简化版本直接使用文件名
return fileName;
}
}
文件工具类
package com.example.fileupload.util;
import java.text.DecimalFormat;
public class FileUtil {
/**
* 格式化文件大小
*/
public static String formatFileSize(long size) {
if (size <= 0) {
return "0 B";
}
String[] units = {"B", "KB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
/**
* 检查文件扩展名
*/
public static boolean checkFileType(String fileName, String[] allowedTypes) {
String extension = getFileExtension(fileName);
if (extension == null || extension.isEmpty()) {
return false;
}
for (String type : allowedTypes) {
if (type.equalsIgnoreCase(extension)) {
return true;
}
}
return false;
}
/**
* 获取文件扩展名
*/
public static String getFileExtension(String fileName) {
if (fileName == null || fileName.isEmpty()) {
return null;
}
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex >= 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex + 1);
}
return null;
}
/**
* 生成唯一文件名
*/
public static String generateUniqueFileName(String originalFilename) {
String extension = getFileExtension(originalFilename);
String timestamp = String.valueOf(System.currentTimeMillis());
String randomStr = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
if (extension != null && !extension.isEmpty()) {
return timestamp + "_" + randomStr + "." + extension;
} else {
return timestamp + "_" + randomStr;
}
}
}
上传页面 (HTML)
创建 src/main/resources/templates/upload.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">文件上传下载示例</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: auto;
background-color: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1, h2 {
color: #333;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
input[type="file"] {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
button:hover {
background-color: #45a049;
}
button.delete {
background-color: #f44336;
}
button.delete:hover {
background-color: #da190b;
}
.message {
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
.success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
#fileList {
margin-top: 15px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #eee;
margin: 5px 0;
border-radius: 4px;
}
.file-info {
flex: 1;
}
.file-actions a, .file-actions button {
margin-left: 10px;
}
.file-actions a {
color: #007bff;
text-decoration: none;
padding: 5px 10px;
border: 1px solid #007bff;
border-radius: 3px;
}
.file-actions a:hover {
background-color: #007bff;
color: white;
}
progress {
width: 100%;
height: 20px;
margin-top: 10px;
}
.progress-container {
display: none;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>文件上传下载示例</h1>
<!-- 单文件上传 -->
<div class="section">
<h2>单文件上传</h2>
<div class="form-group">
<input type="file" id="singleFile">
</div>
<button onclick="uploadSingleFile()">上传文件</button>
<div class="progress-container" id="singleProgressContainer">
<progress id="singleProgress" value="0" max="100"></progress>
</div>
<div id="singleMessage"></div>
</div>
<!-- 多文件上传 -->
<div class="section">
<h2>多文件上传</h2>
<div class="form-group">
<input type="file" id="multiFiles" multiple>
</div>
<button onclick="uploadMultiFiles()">上传文件</button>
<div class="progress-container" id="multiProgressContainer">
<progress id="multiProgress" value="0" max="100"></progress>
</div>
<div id="multiMessage"></div>
</div>
<!-- 文件列表 -->
<div class="section">
<h2>已上传文件</h2>
<button onclick="loadFileList()">刷新列表</button>
<div id="fileList"></div>
</div>
</div>
<script>
// 单文件上传
async function uploadSingleFile() {
const fileInput = document.getElementById('singleFile');
const file = fileInput.files[0];
if (!file) {
showMessage('single', '请先选择文件', 'error');
return;
}
const formData = new FormData();
formData.append('file', file);
showProgress('single', 0);
try {
const response = await fetch('/file/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
showProgress('single', 100);
if (result.code === 200) {
showMessage('single', '上传成功: ' + result.data.fileName, 'success');
loadFileList(); // 刷新列表
} else {
showMessage('single', '上传失败: ' + result.message, 'error');
}
} catch (error) {
showMessage('single', '上传失败: ' + error.message, 'error');
}
// 重置文件输入
fileInput.value = '';
}
// 多文件上传
async function uploadMultiFiles() {
const fileInput = document.getElementById('multiFiles');
const files = fileInput.files;
if (files.length === 0) {
showMessage('multi', '请先选择文件', 'error');
return;
}
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
showProgress('multi', 0);
try {
const response = await fetch('/file/batch/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
showProgress('multi', 100);
if (result.code === 200) {
showMessage('multi', '上传完成: 成功' + result.successCount + '个, 失败' + result.failedCount + '个', 'success');
loadFileList(); // 刷新列表
} else {
showMessage('multi', '上传失败: ' + result.message, 'error');
}
} catch (error) {
showMessage('multi', '上传失败: ' + error.message, 'error');
}
// 重置文件输入
fileInput.value = '';
}
// 加载文件列表
async function loadFileList() {
const fileListDiv = document.getElementById('fileList');
try {
const response = await fetch('/file/list');
const result = await response.json();
if (result.code === 200) {
const files = result.data;
if (files.length === 0) {
fileListDiv.innerHTML = '<p>暂无文件</p>';
return;
}
let html = '';
files.forEach(file => {
html += `
<div class="file-item">
<div class="file-info">
<strong>${file.fileName}</strong><br>
<small>大小: ${file.fileSize} | 上传时间: ${formatDate(file.createTime)}</small>
</div>
<div class="file-actions">
<a href="${file.downloadUrl}">下载</a>
<a href="/file/preview/${file.fileName}" target="_blank">预览</a>
<button class="delete" onclick="deleteFile('${file.fileName}')">删除</button>
</div>
</div>
`;
});
fileListDiv.innerHTML = html;
} else {
fileListDiv.innerHTML = '<p>加载文件列表失败</p>';
}
} catch (error) {
fileListDiv.innerHTML = '<p>加载文件列表失败: ' + error.message + '</p>';
}
}
// 删除文件
async function deleteFile(fileName) {
if (!confirm('确定要删除文件: ' + fileName + ' 吗?')) {
return;
}
try {
const response = await fetch('/file/delete/' + encodeURIComponent(fileName), {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 200) {
alert('文件删除成功');
loadFileList();
} else {
alert('删除失败: ' + result.message);
}
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 显示消息
function showMessage(type, message, status) {
const div = document.getElementById(type + 'Message');
div.className = 'message ' + (status === 'success' ? 'success' : 'error');
div.textContent = message;
}
// 显示进度条
function showProgress(type, percent) {
const container = document.getElementById(type + 'ProgressContainer');
const progress = document.getElementById(type + 'Progress');
if (percent === 100) {
setTimeout(() => {
container.style.display = 'none';
progress.value = 0;
}, 1000);
} else {
container.style.display = 'block';
progress.value = percent;
}
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
}
// 页面加载时获取文件列表
window.onload = function() {
loadFileList();
};
</script>
</body>
</html>
主页控制器
package com.example.fileupload.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PageController {
@GetMapping("/")
public String index() {
return "upload";
}
}
启动类
package com.example.fileupload;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FileUploadApplication {
public static void main(String[] args) {
SpringApplication.run(FileUploadApplication.class, args);
}
}
测试说明
启动应用后,访问 http://localhost:8080 即可看到上传页面。
主要功能:
- 单文件上传:支持单个文件的上传
- 多文件上传:支持多个文件同时上传
- 文件下载:支持文件下载
- 文件预览:支持图片、PDF等文件在线预览
- 文件删除:支持删除已上传的文件
- 文件列表:显示所有已上传的文件
API接口:
POST /file/upload- 单文件上传POST /file/batch/upload- 多文件上传GET /file/list- 获取文件列表GET /file/download/{fileName}- 下载文件GET /file/preview/{fileName}- 预览文件DELETE /file/delete/{fileName}- 删除文件
配置说明:
在 application.yml 中可以配置:
- 文件上传路径
- 允许的文件类型
- 单个文件大小限制
- 请求总大小限制
这样就完成了Spring Boot的文件上传下载功能,包含了完整的CRUD操作,您可以根据实际需求进行修改和扩展。