Spring Boot 3 文件上传下载与 Excel 导入导出完整指南
1. 项目依赖配置
首先,我们需要在 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>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>file-upload-download-demo</artifactId>
<version>1.0.0</version>
<name>file-upload-download-demo</name>
<properties>
<java.version>17</java.version>
<hutool.version>5.8.22</hutool.version>
<easyexcel.version>3.3.2</easyexcel.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Validation Starter (用于文件上传验证) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Hutool 工具包 (简化文件操作) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- EasyExcel (处理Excel导入导出) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<!-- Apache POI (EasyExcel底层依赖,通常不需要单独引入) -->
<!-- 如果需要直接使用POI,可以引入以下依赖 -->
<!--
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.4</version>
</dependency>
-->
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 应用配置文件
创建 application.yml 配置文件:
# application.yml
server:
port: 8080
spring:
servlet:
multipart:
# 启用文件上传
enabled: true
# 单个文件最大大小 (10MB)
max-file-size: 10MB
# 单次请求最大大小 (50MB)
max-request-size: 50MB
# 文件大小阈值,超过此值会写入磁盘
file-size-threshold: 2KB
# 临时文件位置
location: ${java.io.tmpdir}
# 文件存储配置
file:
upload:
# 上传文件存储目录
dir: ${user.home}/uploads
# 允许的文件类型
allowed-extensions: jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,txt,csv
# 最大文件大小 (字节)
max-size: 10485760 # 10MB
# 日志配置
logging:
level:
com.example: debug
3. 公共响应封装类
创建统一的响应结果封装类:
package com.example.common;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 统一响应结果封装类
* @param <T> 响应数据类型
*/
@Data
@Accessors(chain = true)
public class Result<T> {
/**
* 响应状态码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 成功响应构造方法
*/
public static <T> Result<T> success() {
return new Result<T>().setCode(200).setMessage("操作成功");
}
/**
* 成功响应构造方法(带数据)
*/
public static <T> Result<T> success(T data) {
return new Result<T>().setCode(200).setMessage("操作成功").setData(data);
}
/**
* 成功响应构造方法(带消息和数据)
*/
public static <T> Result<T> success(String message, T data) {
return new Result<T>().setCode(200).setMessage(message).setData(data);
}
/**
* 失败响应构造方法
*/
public static <T> Result<T> error(String message) {
return new Result<T>().setCode(500).setMessage(message);
}
/**
* 失败响应构造方法(带状态码和消息)
*/
public static <T> Result<T> error(Integer code, String message) {
return new Result<T>().setCode(code).setMessage(message);
}
}
4. 文件上传下载控制器
4.1 基础文件上传下载控制器
package com.example.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.example.common.Result;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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 java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 文件上传下载控制器
* 提供基础的文件上传、下载、删除功能
*/
@Slf4j
@RestController
@RequestMapping("/api/file")
public class FileController {
/**
* 上传文件存储目录路径
* 从配置文件中读取
*/
@Value("${spring.file.upload.dir}")
private String uploadDir;
/**
* 允许的文件扩展名列表
* 从配置文件中读取并转换为List
*/
@Value("${spring.file.upload.allowed-extensions}")
private String allowedExtensions;
/**
* 最大文件大小(字节)
* 从配置文件中读取
*/
@Value("${spring.file.upload.max-size}")
private long maxSize;
/**
* 初始化上传目录
*/
@PostConstruct
public void init() {
// 创建上传目录(如果不存在)
File uploadDirectory = new File(uploadDir);
if (!uploadDirectory.exists()) {
boolean created = uploadDirectory.mkdirs();
if (created) {
log.info("创建上传目录: {}", uploadDir);
}
}
log.info("文件上传目录: {}", uploadDir);
}
/**
* 单文件上传接口
*
* @param file 上传的文件
* @return 上传结果,包含文件访问路径
*/
@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 验证文件是否为空
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
// 验证文件大小
if (file.getSize() > maxSize) {
return Result.error("文件大小不能超过 " + (maxSize / 1024 / 1024) + "MB");
}
// 验证文件扩展名
String originalFilename = file.getOriginalFilename();
if (!isValidFileType(originalFilename)) {
return Result.error("不支持的文件类型,请上传以下类型文件: " + allowedExtensions);
}
// 生成唯一的文件名(避免文件名冲突)
String uniqueFilename = generateUniqueFilename(originalFilename);
// 构建完整的文件路径
Path filePath = Paths.get(uploadDir, uniqueFilename);
// 保存文件到磁盘
file.transferTo(filePath.toFile());
// 返回文件访问路径(相对路径)
String fileUrl = "/api/file/download/" + uniqueFilename;
log.info("文件上传成功: 原始文件名={}, 保存路径={}", originalFilename, filePath);
return Result.success("文件上传成功", fileUrl);
} catch (IOException e) {
log.error("文件上传失败", e);
return Result.error("文件上传失败: " + e.getMessage());
} catch (Exception e) {
log.error("文件上传异常", e);
return Result.error("文件上传异常: " + e.getMessage());
}
}
/**
* 多文件上传接口
*
* @param files 上传的文件数组
* @return 上传结果列表,包含每个文件的访问路径
*/
@PostMapping("/upload/batch")
public Result<List<String>> uploadFiles(@RequestParam("files") MultipartFile[] files) {
if (files == null || files.length == 0) {
return Result.error("上传文件不能为空");
}
// 限制一次上传的文件数量(可选)
if (files.length > 10) {
return Result.error("一次最多上传10个文件");
}
try {
List<String> fileUrls = Arrays.stream(files)
.filter(file -> !file.isEmpty()) // 过滤空文件
.map(this::processSingleFile) // 处理单个文件
.filter(result -> result.getCode() == 200) // 过滤成功的结果
.map(Result::getData) // 提取文件URL
.collect(Collectors.toList());
if (fileUrls.isEmpty()) {
return Result.error("没有有效的文件被上传");
}
log.info("批量文件上传成功,共上传 {} 个文件", fileUrls.size());
return Result.success("批量文件上传成功", fileUrls);
} catch (Exception e) {
log.error("批量文件上传失败", e);
return Result.error("批量文件上传失败: " + e.getMessage());
}
}
/**
* 处理单个文件上传的私有方法
*
* @param file 单个文件
* @return 上传结果
*/
private Result<String> processSingleFile(MultipartFile file) {
try {
// 验证文件大小
if (file.getSize() > maxSize) {
return Result.error("文件 " + file.getOriginalFilename() + " 大小超过限制");
}
// 验证文件扩展名
String originalFilename = file.getOriginalFilename();
if (!isValidFileType(originalFilename)) {
return Result.error("文件 " + originalFilename + " 类型不支持");
}
// 生成唯一文件名
String uniqueFilename = generateUniqueFilename(originalFilename);
Path filePath = Paths.get(uploadDir, uniqueFilename);
// 保存文件
file.transferTo(filePath.toFile());
return Result.success("/api/file/download/" + uniqueFilename);
} catch (IOException e) {
log.error("处理文件 {} 失败", file.getOriginalFilename(), e);
return Result.error("处理文件失败: " + e.getMessage());
}
}
/**
* 文件下载接口
*
* @param filename 要下载的文件名
* @param response HTTP响应对象
* @throws IOException IO异常
*/
@GetMapping("/download/{filename:.+}")
public void downloadFile(@PathVariable String filename, HttpServletResponse response) throws IOException {
// 验证文件名安全性(防止路径遍历攻击)
if (!isValidFilename(filename)) {
response.sendError(HttpStatus.BAD_REQUEST.value(), "无效的文件名");
return;
}
// 构建文件完整路径
Path filePath = Paths.get(uploadDir, filename);
// 检查文件是否存在
if (!Files.exists(filePath)) {
response.sendError(HttpStatus.NOT_FOUND.value(), "文件不存在");
return;
}
// 设置响应头
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setContentLengthLong(Files.size(filePath));
// 设置文件名(处理中文文件名)
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"); // 处理空格
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename);
// 写入文件内容到响应
Files.copy(filePath, response.getOutputStream());
response.getOutputStream().flush();
}
/**
* 通过文件名直接下载(返回ResponseEntity方式)
* 这种方式更适合需要返回JSON错误信息的场景
*
* @param filename 要下载的文件名
* @return 文件流响应
*/
@GetMapping("/download2/{filename:.+}")
public ResponseEntity<byte[]> downloadFile2(@PathVariable String filename) {
try {
// 验证文件名安全性
if (!isValidFilename(filename)) {
return ResponseEntity.badRequest().build();
}
// 构建文件路径
Path filePath = Paths.get(uploadDir, filename);
// 检查文件是否存在
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
// 读取文件内容
byte[] fileContent = Files.readAllBytes(filePath);
// 构建响应头
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentLength(fileContent.length);
headers.setContentDispositionFormData("attachment", encodedFilename);
return ResponseEntity.ok()
.headers(headers)
.body(fileContent);
} catch (IOException e) {
log.error("文件下载失败: {}", filename, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 删除文件接口
*
* @param filename 要删除的文件名
* @return 删除结果
*/
@DeleteMapping("/delete/{filename:.+}")
public Result<String> deleteFile(@PathVariable String filename) {
try {
// 验证文件名安全性
if (!isValidFilename(filename)) {
return Result.error("无效的文件名");
}
// 构建文件路径
Path filePath = Paths.get(uploadDir, filename);
// 检查文件是否存在
if (!Files.exists(filePath)) {
return Result.error("文件不存在");
}
// 删除文件
boolean deleted = Files.deleteIfExists(filePath);
if (deleted) {
log.info("文件删除成功: {}", filename);
return Result.success("文件删除成功");
} else {
return Result.error("文件删除失败");
}
} catch (IOException e) {
log.error("文件删除失败: {}", filename, e);
return Result.error("文件删除失败: " + e.getMessage());
}
}
/**
* 验证文件扩展名是否合法
*
* @param filename 文件名
* @return 是否合法
*/
private boolean isValidFileType(String filename) {
if (!StringUtils.hasText(filename)) {
return false;
}
// 获取文件扩展名(转换为小写)
String extension = FileUtil.extName(filename).toLowerCase();
// 检查扩展名是否在允许列表中
List<String> allowedList = Arrays.asList(allowedExtensions.split(","));
return allowedList.contains(extension);
}
/**
* 验证文件名安全性(防止路径遍历攻击)
*
* @param filename 文件名
* @return 是否安全
*/
private boolean isValidFilename(String filename) {
if (!StringUtils.hasText(filename)) {
return false;
}
// 检查是否包含路径分隔符(防止路径遍历)
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
return false;
}
// 检查文件名长度
if (filename.length() > 255) {
return false;
}
return true;
}
/**
* 生成唯一的文件名
* 使用UUID + 原始文件扩展名
*
* @param originalFilename 原始文件名
* @return 唯一文件名
*/
private String generateUniqueFilename(String originalFilename) {
// 获取文件扩展名
String extension = FileUtil.extName(originalFilename);
// 生成UUID(使用Hutool的IdUtil)
String uuid = IdUtil.fastSimpleUUID();
// 如果原始文件没有扩展名,则不添加扩展名
if (StrUtil.isBlank(extension)) {
return uuid;
} else {
return uuid + "." + extension;
}
}
}
5. Excel 数据模型
创建用于 Excel 导入导出的数据模型:
package com.example.model;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 用户数据模型
* 用于Excel导入导出
*/
@Data
@HeadRowHeight(25) // 表头行高
@ContentRowHeight(20) // 内容行高
@ColumnWidth(20) // 默认列宽
public class UserExcelDTO {
/**
* 用户ID
* Excel列名:用户ID
* 列索引:0(第一列)
*/
@ExcelProperty(value = "用户ID", index = 0)
@ColumnWidth(15)
private Long id;
/**
* 用户名
* Excel列名:用户名
*/
@ExcelProperty("用户名")
@ColumnWidth(25)
private String username;
/**
* 真实姓名
* Excel列名:真实姓名
*/
@ExcelProperty("真实姓名")
@ColumnWidth(20)
private String realName;
/**
* 邮箱
* Excel列名:邮箱
*/
@ExcelProperty("邮箱")
@ColumnWidth(30)
private String email;
/**
* 手机号
* Excel列名:手机号
*/
@ExcelProperty("手机号")
@ColumnWidth(18)
private String phone;
/**
* 年龄
* Excel列名:年龄
*/
@ExcelProperty("年龄")
@ColumnWidth(10)
private Integer age;
/**
* 工资
* Excel列名:工资
*/
@ExcelProperty("工资")
@ColumnWidth(15)
private BigDecimal salary;
/**
* 入职日期
* Excel列名:入职日期
* 日期格式:yyyy-MM-dd HH:mm:ss
*/
@ExcelProperty("入职日期")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ColumnWidth(22)
private LocalDateTime hireDate;
/**
* 状态
* Excel列名:状态
* 0-禁用, 1-启用
*/
@ExcelProperty("状态")
@ColumnWidth(12)
private Integer status;
}
6. Excel 导入导出控制器
package com.example.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.common.Result;
import com.example.model.UserExcelDTO;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Excel导入导出控制器
* 使用EasyExcel实现Excel文件的导入导出功能
*/
@Slf4j
@RestController
@RequestMapping("/api/excel")
public class ExcelController {
/**
* 模拟数据库数据(实际项目中应从数据库查询)
*/
private List<UserExcelDTO> getMockUserData() {
List<UserExcelDTO> users = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
UserExcelDTO user = new UserExcelDTO();
user.setId((long) i);
user.setUsername("user" + i);
user.setRealName("用户" + i);
user.setEmail("user" + i + "@example.com");
user.setPhone("1380013800" + (i % 10));
user.setAge(20 + (i % 50));
user.setSalary(java.math.BigDecimal.valueOf(5000 + (i * 100)));
user.setHireDate(LocalDateTime.now().minusDays(i));
user.setStatus(i % 2 == 0 ? 1 : 0);
users.add(user);
}
return users;
}
/**
* 导出Excel文件(全量导出)
*
* @param response HTTP响应对象
* @throws IOException IO异常
*/
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
try {
// 获取要导出的数据
List<UserExcelDTO> userData = getMockUserData();
// 设置响应头
String fileName = "用户数据_" + DateUtil.format(DateUtil.date(), "yyyyMMdd_HHmmss") + ".xlsx";
setExcelResponseHeader(response, fileName);
// 使用EasyExcel写入数据
EasyExcel.write(response.getOutputStream(), UserExcelDTO.class)
.sheet("用户数据")
.doWrite(userData);
log.info("Excel导出成功,共导出 {} 条数据", userData.size());
} catch (Exception e) {
log.error("Excel导出失败", e);
// 导出失败时返回错误信息
response.reset();
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
response.getWriter().write("{\"code\":500,\"message\":\"导出失败: " + e.getMessage() + "\"}");
}
}
/**
* 分页导出Excel文件
* 适用于大数据量导出,避免内存溢出
*
* @param response HTTP响应对象
* @throws IOException IO异常
*/
@GetMapping("/export/paginated")
public void exportExcelPaginated(HttpServletResponse response) throws IOException {
try {
// 设置响应头
String fileName = "用户数据_分页_" + DateUtil.format(DateUtil.date(), "yyyyMMdd_HHmmss") + ".xlsx";
setExcelResponseHeader(response, fileName);
// 创建ExcelWriter
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), UserExcelDTO.class)
.build();
// 创建工作表
WriteSheet writeSheet = EasyExcel.writerSheet("用户数据").build();
// 分页查询并写入数据(模拟分页)
int pageSize = 1000; // 每页1000条
int currentPage = 0;
List<UserExcelDTO> pageData;
do {
// 模拟分页查询(实际项目中应调用数据库分页查询)
pageData = getMockPageData(currentPage, pageSize);
if (!pageData.isEmpty()) {
// 写入当前页数据
excelWriter.write(pageData, writeSheet);
currentPage++;
}
} while (!pageData.isEmpty());
// 关闭ExcelWriter
excelWriter.finish();
log.info("分页Excel导出成功,共导出 {} 页", currentPage);
} catch (Exception e) {
log.error("分页Excel导出失败", e);
response.reset();
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
response.getWriter().write("{\"code\":500,\"message\":\"导出失败: " + e.getMessage() + "\"}");
}
}
/**
* 模拟分页数据查询
*
* @param page 页码(从0开始)
* @param size 每页大小
* @return 分页数据
*/
private List<UserExcelDTO> getMockPageData(int page, int size) {
// 模拟总共10000条数据
int totalRecords = 10000;
int start = page * size;
int end = Math.min(start + size, totalRecords);
if (start >= totalRecords) {
return CollUtil.newArrayList();
}
List<UserExcelDTO> pageData = new ArrayList<>();
for (int i = start; i < end; i++) {
UserExcelDTO user = new UserExcelDTO();
user.setId((long) i + 1);
user.setUsername("user" + (i + 1));
user.setRealName("用户" + (i + 1));
user.setEmail("user" + (i + 1) + "@example.com");
user.setPhone("1380013800" + ((i + 1) % 10));
user.setAge(20 + ((i + 1) % 50));
user.setSalary(java.math.BigDecimal.valueOf(5000 + ((i + 1) * 100)));
user.setHireDate(LocalDateTime.now().minusDays(i + 1));
user.setStatus((i + 1) % 2 == 0 ? 1 : 0);
pageData.add(user);
}
return pageData;
}
/**
* Excel文件导入
*
* @param file 上传的Excel文件
* @return 导入结果
*/
@PostMapping("/import")
public Result<String> importExcel(@RequestParam("file") MultipartFile file) {
try {
// 验证文件
if (file.isEmpty()) {
return Result.error("请选择要导入的Excel文件");
}
String originalFilename = file.getOriginalFilename();
if (!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls")) {
return Result.error("请上传Excel文件(.xlsx或.xls格式)");
}
// 读取Excel文件数据
List<UserExcelDTO> importedData = new ArrayList<>();
// 使用EasyExcel读取数据
EasyExcel.read(file.getInputStream(), UserExcelDTO.class,
new com.alibaba.excel.read.listener.PageReadListener<UserExcelDTO>(dataList -> {
// 处理每一批数据(默认100条一批)
importedData.addAll(dataList);
// 这里可以进行数据验证、转换、保存到数据库等操作
// 为了示例简单,这里只做简单处理
log.info("读取到 {} 条数据", dataList.size());
}))
.sheet()
.doRead();
// 验证导入的数据
if (importedData.isEmpty()) {
return Result.error("Excel文件中没有有效数据");
}
// 实际项目中,这里应该将数据保存到数据库
// validateAndSaveData(importedData);
log.info("Excel导入成功,共导入 {} 条数据", importedData.size());
return Result.success("Excel导入成功,共导入 " + importedData.size() + " 条数据");
} catch (Exception e) {
log.error("Excel导入失败", e);
return Result.error("Excel导入失败: " + e.getMessage());
}
}
/**
* 设置Excel文件下载的响应头
*
* @param response HTTP响应对象
* @param fileName 文件名
* @throws IOException IO异常
*/
private void setExcelResponseHeader(HttpServletResponse response, String fileName) throws IOException {
// 设置内容类型
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 编码文件名(处理中文)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
// 设置Content-Disposition头
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
}
}
7. Excel 导入数据验证监听器
创建专门的数据验证监听器,用于处理导入过程中的数据验证:
package com.example.listener;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.example.model.UserExcelDTO;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* Excel导入数据验证监听器
* 在读取Excel数据时进行数据验证
*/
@Slf4j
public class UserExcelDataListener implements ReadListener<UserExcelDTO> {
/**
* 每批处理的数据量
*/
private static final int BATCH_COUNT = 100;
/**
* 临时存储数据的列表
*/
private final List<UserExcelDTO> cachedData = new ArrayList<>();
/**
* 存储验证错误信息
*/
private final List<String> errorMessages = new ArrayList<>();
/**
* 行号计数器(从1开始,包含表头)
*/
private int currentRow = 1;
@Override
public void invoke(UserExcelDTO data, AnalysisContext context) {
currentRow++;
// 验证数据
String validationError = validateData(data, currentRow);
if (validationError != null) {
errorMessages.add("第" + currentRow + "行: " + validationError);
return; // 跳过无效数据
}
// 添加到缓存
cachedData.add(data);
// 达到批次数量时处理数据
if (cachedData.size() >= BATCH_COUNT) {
saveData();
cachedData.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余数据
saveData();
cachedData.clear();
// 输出验证结果
if (!errorMessages.isEmpty()) {
log.warn("Excel导入完成,发现 {} 个验证错误", errorMessages.size());
errorMessages.forEach(log::warn);
} else {
log.info("Excel导入完成,所有数据验证通过");
}
}
/**
* 保存数据到数据库(实际项目中实现)
*/
private void saveData() {
if (!cachedData.isEmpty()) {
log.info("保存 {} 条数据到数据库", cachedData.size());
// 实际项目中这里应该调用Service层保存数据
// userService.saveBatch(cachedData);
}
}
/**
* 验证单行数据
*
* @param data 要验证的数据
* @param rowNumber 行号
* @return 验证错误信息,null表示验证通过
*/
private String validateData(UserExcelDTO data, int rowNumber) {
// 验证用户名
if (StrUtil.isBlank(data.getUsername())) {
return "用户名不能为空";
}
if (data.getUsername().length() > 50) {
return "用户名长度不能超过50个字符";
}
// 验证真实姓名
if (StrUtil.isBlank(data.getRealName())) {
return "真实姓名不能为空";
}
// 验证邮箱格式
if (StrUtil.isNotBlank(data.getEmail())) {
if (!data.getEmail().matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
return "邮箱格式不正确";
}
}
// 验证手机号格式
if (StrUtil.isNotBlank(data.getPhone())) {
if (!data.getPhone().matches("^1[3-9]\\d{9}$")) {
return "手机号格式不正确";
}
}
// 验证年龄范围
if (data.getAge() != null) {
if (data.getAge() < 0 || data.getAge() > 150) {
return "年龄必须在0-150之间";
}
}
// 验证工资
if (data.getSalary() != null) {
if (data.getSalary().compareTo(java.math.BigDecimal.ZERO) < 0) {
return "工资不能为负数";
}
}
// 验证状态
if (data.getStatus() != null) {
if (data.getStatus() != 0 && data.getStatus() != 1) {
return "状态只能为0(禁用)或1(启用)";
}
}
return null; // 验证通过
}
/**
* 获取验证错误信息
*
* @return 错误信息列表
*/
public List<String> getErrorMessages() {
return new ArrayList<>(errorMessages);
}
/**
* 是否存在验证错误
*
* @return true表示存在错误
*/
public boolean hasErrors() {
return !errorMessages.isEmpty();
}
}
8. 使用验证监听器的导入控制器
package com.example.controller;
import com.alibaba.excel.EasyExcel;
import com.example.common.Result;
import com.example.listener.UserExcelDataListener;
import com.example.model.UserExcelDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 使用验证监听器的Excel导入控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/excel/validated")
public class ValidatedExcelController {
/**
* 使用验证监听器导入Excel
*
* @param file 上传的Excel文件
* @return 导入结果
*/
@PostMapping("/import")
public Result<String> importExcelWithValidation(@RequestParam("file") MultipartFile file) {
try {
// 验证文件
if (file.isEmpty()) {
return Result.error("请选择要导入的Excel文件");
}
String originalFilename = file.getOriginalFilename();
if (!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls")) {
return Result.error("请上传Excel文件(.xlsx或.xls格式)");
}
// 创建验证监听器
UserExcelDataListener listener = new UserExcelDataListener();
// 读取Excel文件
EasyExcel.read(file.getInputStream(), UserExcelDTO.class, listener)
.sheet()
.doRead();
// 检查是否有验证错误
if (listener.hasErrors()) {
StringBuilder errorMessage = new StringBuilder("Excel导入完成,但存在以下问题:\n");
listener.getErrorMessages().forEach(msg -> errorMessage.append(msg).append("\n"));
return Result.error(errorMessage.toString());
}
return Result.success("Excel导入成功");
} catch (Exception e) {
log.error("Excel导入失败", e);
return Result.error("Excel导入失败: " + e.getMessage());
}
}
}
9. 前端HTML测试页面
创建一个简单的HTML页面用于测试:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传下载测试</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
.button { padding: 10px 20px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; }
.button:hover { background: #0056b3; }
.result { margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 3px; }
input[type="file"] { margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<h1>Spring Boot 3 文件上传下载与Excel导入导出测试</h1>
<!-- 文件上传测试 -->
<div class="section">
<h2>1. 文件上传测试</h2>
<input type="file" id="fileInput" multiple>
<button class="button" onclick="uploadFile()">上传文件</button>
<button class="button" onclick="uploadFiles()">批量上传</button>
<div id="uploadResult" class="result"></div>
</div>
<!-- 文件下载测试 -->
<div class="section">
<h2>2. 文件下载测试</h2>
<input type="text" id="filenameInput" placeholder="输入文件名(如:abc123.txt)">
<button class="button" onclick="downloadFile()">下载文件</button>
<div id="downloadResult" class="result"></div>
</div>
<!-- Excel导出测试 -->
<div class="section">
<h2>3. Excel导出测试</h2>
<button class="button" onclick="exportExcel()">导出Excel</button>
<button class="button" onclick="exportExcelPaginated()">分页导出Excel</button>
</div>
<!-- Excel导入测试 -->
<div class="section">
<h2>4. Excel导入测试</h2>
<input type="file" id="excelFileInput" accept=".xlsx,.xls">
<button class="button" onclick="importExcel()">导入Excel</button>
<div id="importResult" class="result"></div>
</div>
</div>
<script>
// 基础URL
const BASE_URL = 'http://localhost:8080/api';
// 上传单个文件
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择文件');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch(`${BASE_URL}/file/upload`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.getElementById('uploadResult').innerHTML =
`<strong>结果:</strong> ${JSON.stringify(data, null, 2)}`;
})
.catch(error => {
console.error('上传失败:', error);
document.getElementById('uploadResult').innerHTML =
`<strong>错误:</strong> ${error.message}`;
});
}
// 批量上传文件
function uploadFiles() {
const fileInput = document.getElementById('fileInput');
const files = fileInput.files;
if (files.length === 0) {
alert('请选择文件');
return;
}
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
fetch(`${BASE_URL}/file/upload/batch`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.getElementById('uploadResult').innerHTML =
`<strong>结果:</strong> ${JSON.stringify(data, null, 2)}`;
})
.catch(error => {
console.error('批量上传失败:', error);
document.getElementById('uploadResult').innerHTML =
`<strong>错误:</strong> ${error.message}`;
});
}
// 下载文件
function downloadFile() {
const filename = document.getElementById('filenameInput').value;
if (!filename) {
alert('请输入文件名');
return;
}
window.location.href = `${BASE_URL}/file/download/${filename}`;
}
// 导出Excel
function exportExcel() {
window.location.href = `${BASE_URL}/excel/export`;
}
// 分页导出Excel
function exportExcelPaginated() {
window.location.href = `${BASE_URL}/excel/export/paginated`;
}
// 导入Excel
function importExcel() {
const fileInput = document.getElementById('excelFileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择Excel文件');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch(`${BASE_URL}/excel/import`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.getElementById('importResult').innerHTML =
`<strong>结果:</strong> ${JSON.stringify(data, null, 2)}`;
})
.catch(error => {
console.error('导入失败:', error);
document.getElementById('importResult').innerHTML =
`<strong>错误:</strong> ${error.message}`;
});
}
</script>
</body>
</html>
10. 启动类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot 应用启动类
*/
@SpringBootApplication
public class FileUploadDownloadApplication {
public static void main(String[] args) {
SpringApplication.run(FileUploadDownloadApplication.class, args);
}
}
11. 最佳实践总结
11.1 文件上传最佳实践
-
安全性验证:
- 验证文件扩展名
- 验证文件大小
- 防止路径遍历攻击
- 验证文件内容类型(可选)
-
文件存储:
- 使用唯一文件名避免冲突
- 考虑使用云存储(如阿里云OSS、AWS S3)
- 定期清理临时文件
-
性能优化:
- 限制单次上传文件数量
- 使用异步处理大文件
- 考虑分片上传
11.2 文件下载最佳实践
-
响应头设置:
- 正确设置Content-Type
- 处理中文文件名编码
- 设置合适的缓存策略
-
大文件处理:
- 使用流式传输避免内存溢出
- 支持断点续传(可选)
11.3 Excel导入导出最佳实践
-
导出优化:
- 大数据量使用分页导出
- 使用EasyExcel避免内存溢出
- 添加进度提示(前端)
-
导入验证:
- 使用监听器进行逐行验证
- 提供详细的错误信息
- 支持部分成功导入
-
用户体验:
- 提供模板下载
- 显示导入进度
- 支持预览功能
11.4 异常处理
-
统一异常处理:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MultipartException.class) public ResponseEntity<Result<String>> handleMultipartException(MultipartException e) { return ResponseEntity.badRequest().body(Result.error("文件上传异常: " + e.getMessage())); } @ExceptionHandler(IOException.class) public ResponseEntity<Result<String>> handleIOException(IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Result.error("IO异常: " + e.getMessage())); } } -
日志记录:
- 记录关键操作日志
- 记录错误详细信息
- 考虑记录操作用户信息
这个完整的实现涵盖了Spring Boot 3中文件上传下载和Excel导入导出的所有标准用法,包含了安全性、性能、用户体验等多个方面的最佳实践。
544

被折叠的 条评论
为什么被折叠?



