前言

文件下载,常用的一个功能,比较简单,但是有可能会遇见一些问题

  • 文件太大,若写法上有问题,可能会导致内存溢出
  • 文件中文名称乱码问题

方式1:HttpServletResponse.write

直接将文件一次性读取到内存中,然后通过response.write()写入到客户端,这种方式适合小文件,若文件比较大,将文件一次性到内存中可能导致OOM,需要注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping(value = "/download1")
public void download(HttpServletResponse response) throws IOException {
// 指定要下载的文件
File file = ResourceUtils.getFile("classpath:测试文件1.txt");
// 文件转成字节数组
byte[] fileBytes = Files.readAllBytes(file.toPath());
// 文件名编码,防止中文乱码
String fileName = URLEncoder.encode(file.getName(), "UTF-8");
// 设置响应头信息
response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\"");
// 内容类型为通用类型,表示二进制数据流
response.setContentType("application/octet-stream");
// 输出文件内容
try(OutputStream os = response.getOutputStream()){
os.write(fileBytes);
}
}

方式2:ResponseEntity

方法需要返回ResponseEntity类型的对象,这个类是SpringBoot中自带的,是对http相应结果的一种封装,可以用来构建http响应结果:包含响应状态码、响应头、响应体等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/download2")
public ResponseEntity<byte[]> download2() throws IOException {
// 指定要下载的文件
File file = ResourceUtils.getFile("classpath:测试文件1.txt");
// 文件转成字节数组
byte[] fileBytes = Files.readAllBytes(file.toPath());
// 文件名编码,防止中文乱码
String fileName = URLEncoder.encode(file.getName(), "UTF-8");
// 构建响应实体:ResponseEntity, 包含了http请求的响应信息,比如状态码、响应头、响应体等信息
ResponseEntity<byte[]> responseEntity = ResponseEntity.ok()
// 设置响应头信息
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + fileName + "\"")
// 内容类型为通用类型,表示二进制数据流
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
// 响应体
.body(fileBytes);
return responseEntity;
}

上面两种方式,都是直接将文件读取到内存字节数组中,然后输出到客户端,这种方式适合小文件,若文件比较大,将文件一次性到内存中导致OOM

方式3:通用方案(适合任意文件大小),建议采用

Resource是spring中的一个资源接口,是对资源的一种抽象,常见的几个实现类

  1. ClassPathResource:表示类路径下的资源,即src/main/resources目录下的资源
    • Resource resource = new ClassPathResource(“测试文件1.txt”);
  2. FileSystemResource:表示文件系统下的资源,即磁盘上的文件
    • Resource resource = new FileSystemResource(“D:\测试文件1.txt”);
  3. UrlResource:表示网络资源,即http://www.baidu.com
  4. InputStreamResource:表示输入流资源,即通过输入流获取的资源
  5. ByteArrayResource:表示字节数组资源,即通过字节数组获取的资源

边读边写,边读边写,避免OOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@GetMapping("/download3")
public void download3(HttpServletResponse response) throws IOException {
Resource resource = new ClassPathResource("测试文件1.txt");
if (!resource.exists()) {
response.sendError(404, "文件未找到");
return;
}
// 文件名编码,防止中文乱码
String fileName = URLEncoder.encode(resource.getFilename(), StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
// 设置响应头信息
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);
// 内容类型为通用类型,表示二进制数据流
response.setContentType(Files.probeContentType(Paths.get(resource.getURI())));
// 边读边写
try (InputStream is = resource.getInputStream();
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024 * 8]; // 8KB缓存区
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
}

返回ResponseEntity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/download4")
public ResponseEntity<Resource> download4() throws IOException {
// 指定要下载的文件
Resource resource = new ClassPathResource("测试文件1.txt");

// 文件名编码,防止中文乱码
String fileName = URLEncoder.encode(resource.getFilename(), StandardCharsets.UTF_8);
// 构建响应实体:ResponseEntity, 包含了http请求的响应信息,比如状态码、响应头、响应体等信息
ResponseEntity<Resource> responseEntity = ResponseEntity.ok()
// 设置响应头信息
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + fileName + "\"")
// 内容类型为通用类型,表示二进制数据流
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
// 响应体
.body(resource);
return responseEntity;
}

中文乱码问题

  1. 文件名中如果有中文,下载下来后文件名称是乱码,解决代码如下,需要对文件名称进行编码

    1
    String fileName = URLEncoder.encode(file.getName(), "UTF-8");
  2. 但是使用URLEncoder.encode()可能不符合HTTP标准(RFC 5987),部分浏览器可能仍会乱码。

    • 改进方案:使用RFC 5987规范编码文件名
      1
      2
      3
      String encodedFileName = "filename*=UTF-8''" + 
      URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
      response.setHeader("Content-Disposition", "attachment; " + encodedFileName);

核心响应头分类及作用

Content-Type

  • 作用:告诉客户端响应体的数据格式(MIME 类型)
  • 常见场景
    • 返回 JSON 数据application/json
    • 返回 HTML 页面text/html
    • 文件下载application/octet-stream(通用二进制流)或具体类型(如 image/png/jpeg/pdf等)
  • 设置方法
    1
    2
    //response.setHeader("Content-Type", "application/json");
    response.setContentType("application/json; charset=UTF-8");
    • 注意事项:
    • 必须包含字符编码(如 charset=UTF-8),否则可能乱码。
    • 若未设置,浏览器可能根据内容猜测类型(MIME 嗅探),存在安全风险。

Content-Disposition

  • 作用:控制客户端如何处理响应内容(如直接展示或下载为文件)
  • 常见场景
    • 强制文件下载attachment; filename="file.txt"
    • 内联显示inline(如直接在浏览器中显示图片)
      • 注意:如果在请求头中设置了response.setHeader("Content-Type", "image/png");会默认以内联方式显示,而不需要显式设置inline
  • 设置方法
    1
    2
    //response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
    response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);

Cache-Control

  • 作用:控制客户端和代理服务器的缓存行为。
  • 常见场景
    • 禁止缓存no-store
    • 缓存但需验证no-cache
    • 缓存有效期max-age=3600(缓存 1 小时)
  • 设置方法
    1
    response.setHeader("Cache-Control", "no-store");

Location

  • 作用:重定向客户端到新的 URL(需配合 3xx 状态码)。
  • 常见场景
    • 用户登录后跳转到主页
    • 旧 URL 迁移到新地址
  • 设置方法
    1
    2
    response.setStatus(HttpServletResponse.SC_FOUND); // 302
    response.setHeader("Location", "/new-url");
  • 作用:向客户端设置 Cookie。
  • 常见场景
    • 用户会话管理(如 JSESSIONID)
    • 记住用户偏好设置
  • 设置方法
    1
    2
    3
    Cookie cookie = new Cookie("theme", "dark");
    cookie.setMaxAge(86400); // 有效期 1 天
    response.addCookie(cookie);

X-Content-Type-Options

  • 作用:向客户端设置 Cookie。
  • 设置方法
    1
    response.setHeader("X-Content-Type-Options", "nosniff");

其他实用响应头

  1. CORS(跨域资源共享)
    1
    2
    3
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "GET, POST");
    response.setHeader("Access-Control-Allow-Headers", "Content-Type");
  2. 安全头
    1
    2
    3
    4
    5
    6
    // 防止点击劫持
    response.setHeader("X-Frame-Options", "DENY");
    // 防止 XSS 攻击
    response.setHeader("X-XSS-Protection", "1; mode=block");
    // 禁止嗅探 MIME 类型
    response.setHeader("X-Content-Type-Options", "nosniff");

经典使用场景

返回JSON数据

1
2
3
4
5
6
7
@GetMapping("/api/user")
public ResponseEntity<User> getUser() {
User user = userService.getUser();
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(user);
}

关键头:

  • Content-Type: application/json; charset=UTF-8
  • X-Content-Type-Options: nosniff(可选,增强安全)

文件下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/download")
public void downloadFile(HttpServletResponse response) throws IOException {
Resource resource = new ClassPathResource("file.pdf");
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode("文件.pdf", "UTF-8"));

try (InputStream is = resource.getInputStream();
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
}

关键头:

  • Content-Type: application/pdf(明确文件类型)
  • Content-Disposition: attachment; filename*=UTF-8’’%E6%96%87%E4%BB%B6.pdf(强制下载并解决中文乱码)

重定向

1
2
3
4
5
@GetMapping("/old-page")
public void redirect(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301
response.setHeader("Location", "/new-page");
}

关键头:

  • Location: /new-page
  • HTTP Status: 301 或 302

禁用缓存(敏感数据)

1
2
3
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");

关键头:

  • Cache-Control: no-store
  • Pragma: no-cache(兼容旧浏览器)
  • Expires: 0(立即过期)