文件下载/上传

文件流式下载

描述

文件流式下载不区分文件类型,只需要文件路径

响应头

语句 响应头名称 作用 示例值
setContentType("application/octet-stream") Content-Type 告诉浏览器:是二进制流文件 application/octet-stream
setHeader("Content-Disposition", "attachment; filename=...") Content-Disposition 告诉浏览器:是下载附件,并指定下载名 attachment; filename="test.pdf"
setHeader("Content-Length", String.valueOf(Files.size(filePath))) Content-Length 告诉浏览器:文件大小(可显示进度) 1048576
setCharacterEncoding("UTF-8") 响应字符集 保证文件名和文本内容不乱码 UTF-8

后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
public class FileDownloadController {

private final Path baseDir = Paths.get("D:\\Desktop\\测试").toAbsolutePath().normalize();

@GetMapping("/download3")
public void downloadFile(@RequestParam("name") String name, HttpServletResponse response) throws IOException {

// 1️⃣ 防止路径遍历攻击
Path filePath = baseDir.resolve(name).normalize();
if (!filePath.startsWith(baseDir) || !Files.exists(filePath) || Files.isDirectory(filePath)) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().write("File not found");
return;
}

// 2️⃣ 处理文件名编码
String fileName = filePath.getFileName().toString();
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");

// 3️⃣ 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
response.setHeader("Content-Length", String.valueOf(Files.size(filePath)));
response.setCharacterEncoding("UTF-8");

// 4️⃣ 流式读取文件并写出
try (InputStream in = Files.newInputStream(filePath);
OutputStream out = response.getOutputStream()) {

byte[] buffer = new byte[8192]; // 8KB 缓冲区
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush(); // 确保及时发送到客户端
}
}
}
}

文件流式上传

后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class FileUploadController {

@Value("${file.upload-dir}") // 默认路径:D:\Desktop\test
private String uploadDir;

@PostMapping("/upload")
public ResponseEntity<?> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "customPath", required = false) String customPath // 接收前端传入的子路径
) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "请选择文件上传"));
}

try {
// 拼接完整路径:D:\Desktop\test + customPath
String fullPath = uploadDir;
if (customPath != null && !customPath.trim().isEmpty()) {
fullPath += (customPath.endsWith("/") || customPath.endsWith("\\"))
? customPath
: customPath + "\\";
}

// 确保目录存在
Path uploadPath = Paths.get(fullPath);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}

// 获取原始文件名(不做修改)
String originalFilename = file.getOriginalFilename();
Path filePath = uploadPath.resolve(originalFilename);

// 如果文件已经存在,先删除
if (Files.exists(filePath)){
Files.delete(filePath);
}

// // 保存文件——不会自动关闭流
// Files.copy(file.getInputStream(), filePath);

// ===== 保存文件(自动关闭流)=====
try (InputStream in = file.getInputStream()) {
Files.copy(in, filePath, StandardCopyOption.REPLACE_EXISTING);
}

// 返回响应
Map<String, Object> response = new HashMap<>();
response.put("message", "文件上传成功");
response.put("originalFilename", originalFilename);
response.put("savedPath", filePath.toString());
response.put("fileSize", file.getSize());
response.put("contentType", file.getContentType());

return ResponseEntity.ok(response);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.internalServerError().body(Map.of("message", "文件上传失败: " + e.getMessage()));
}
}
}

配置文件

1
2
3
4
# 基础路径(Windows)
file.upload-dir=D:\\Desktop\\test\\
# 基础路径(Linux)
file.upload-dir=/Users/username/Desktop/test/

数据导出

描述

数据导出常以Excel表格的样式导出,有 Apache POIEasyExcel 两种方式

区别

特性 Apache POI EasyExcel
项目背景 Apache 软件基金会的顶级项目,历史悠久,是 Java 操作 Office 文档的事实标准。 阿里巴巴开源的项目,旨在解决 POI 在处理大数据量时的内存溢出问题。
内存模型 基于内存的模型。用户模式会将整个文件(所有工作表、行、单元格)一次性加载到内存中,形成对象树。 基于事件的流式模型。逐行读取和解析,读取时不会将整个文件加载到内存中。
内存消耗 。处理大文件(如几十MB以上)时,很容易导致 OOM(内存溢出) 极低。理论上只会在内存中保留一行的数据,非常适合处理超大文件(如百万行)。
性能 对于小文件,性能很好。对于大文件,性能急剧下降,甚至无法完成。 对于大文件,性能非常出色且稳定。对于小文件,性能与 POI 相当或略慢(因为涉及模型转换)。
API 与易用性 功能强大但繁琐。API 非常底层和灵活,需要编写大量样板代码(如创建行、单元格、设置样式等)。 简单易用。提供了高度封装的 API,支持注解驱动模型(通过注解映射Java对象和Excel列),并内置了监听器用于读。
功能完整性 非常全面。支持 Excel 的所有特性,包括 .xls (HSSF) 和 .xlsx (XSSF/SXSSF),以及复杂的图表、样式、公式、宏等。 覆盖常用场景。专注于数据的导入导出,支持基本样式和格式。对于非常复杂的 Excel 操作(如图表、合并复杂单元格),功能不如 POI。

Apache POI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@PostMapping("/excel/poi")
public void exportWithPoi(HttpServletResponse response) throws IOException {
// ===== 1️⃣ 查询数据 =====
List<Paper> list = paperService.list();

// ===== 2️⃣ 创建 Workbook & Sheet =====
try (Workbook wb = new XSSFWorkbook()) {
Sheet sheet = wb.createSheet("数据导出");
CreationHelper helper = wb.getCreationHelper();

// ===== 3️⃣ 样式设置 =====
// 基础样式:居中 + 边框
CellStyle baseStyle = wb.createCellStyle();
baseStyle.setAlignment(HorizontalAlignment.CENTER);
baseStyle.setVerticalAlignment(VerticalAlignment.CENTER);
baseStyle.setBorderTop(BorderStyle.THIN);
baseStyle.setBorderBottom(BorderStyle.THIN);
baseStyle.setBorderLeft(BorderStyle.THIN);
baseStyle.setBorderRight(BorderStyle.THIN);

// 表头样式:继承基础 + 灰底 + 加粗
CellStyle headerStyle = wb.createCellStyle();
headerStyle.cloneStyleFrom(baseStyle);
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font boldFont = wb.createFont();
boldFont.setBold(true);
headerStyle.setFont(boldFont);

// 日期样式:继承基础 + 日期格式
CellStyle dateStyle = wb.createCellStyle();
dateStyle.cloneStyleFrom(baseStyle);
dateStyle.setDataFormat(helper.createDataFormat().getFormat("yyyy-MM-dd"));

// ===== 4️⃣ 写表头 =====
String[] headers = {"ID", "文章名", "创建时间", "浏览数"};
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}

// ===== 5️⃣ 写数据 =====
int rowNum = 1;
for (Paper p : list) {
Row row = sheet.createRow(rowNum++);
Object[] vals = {
p.getPaperId(),
p.getPaperTitle(),
p.getPublishDate(),
p.getViewCount()
};
for (int i = 0; i < vals.length; i++) {
Cell cell = row.createCell(i);
if (vals[i] instanceof Date) {
cell.setCellValue((Date) vals[i]);
cell.setCellStyle(dateStyle);
} else {
cell.setCellValue(vals[i] == null ? "" : vals[i].toString());
cell.setCellStyle(baseStyle);
}
}
}

// ===== 6️⃣ 自动列宽 =====
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}

// ===== 7️⃣ 响应输出 =====
String fileName = URLEncoder.encode("export_data.xlsx", StandardCharsets.UTF_8);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
wb.write(response.getOutputStream());
}
}

EasyExcel

使用方式相较于poi更为简洁

EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;

@RestController
@RequestMapping("/api")
public class EasyExcelExportController {

private final PaperService paperService;

public EasyExcelExportController(PaperService paperService) {
this.paperService = paperService;
}

@GetMapping("/excel/easy")
public void exportWithEasyExcel(HttpServletResponse response) throws IOException {
// ===== 1️⃣ 查询数据 =====
List<PaperExportDTO> list = paperService.list().stream().map(p -> new PaperExportDTO(
p.getPaperId(),
p.getPaperTitle(),
p.getPublishDate(),
p.getViewCount()
)).toList();

// ===== 2️⃣ 设置响应头 =====
String fileName = URLEncoder.encode("export_data.xlsx", StandardCharsets.UTF_8);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);

// ===== 3️⃣ 表头样式和内容样式 =====
WriteCellStyle headStyle = new WriteCellStyle();
WriteFont headFont = new WriteFont();
headFont.setBold(true);
headFont.setFontHeightInPoints((short) 12);
headStyle.setWriteFont(headFont);

WriteCellStyle contentStyle = new WriteCellStyle();
WriteFont contentFont = new WriteFont();
contentFont.setFontHeightInPoints((short) 11);
contentStyle.setWriteFont(contentFont);

HorizontalCellStyleStrategy styleStrategy =
new HorizontalCellStyleStrategy(headStyle, contentStyle);

// ===== 4️⃣ 写出 Excel =====
EasyExcel.write(response.getOutputStream(), PaperExportDTO.class)
.sheet("数据导出")
.registerWriteHandler(styleStrategy)
.doWrite(list);
}
}