惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Martin Fowler
Martin Fowler
aimingoo的专栏
aimingoo的专栏
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
S
Securelist
博客园 - Franky
P
Proofpoint News Feed
量子位
雷峰网
雷峰网
Security Latest
Security Latest
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Latest news
Latest news
L
Lohrmann on Cybersecurity
W
WeLiveSecurity
月光博客
月光博客
Hacker News: Ask HN
Hacker News: Ask HN
宝玉的分享
宝玉的分享
GbyAI
GbyAI
小众软件
小众软件
M
MIT News - Artificial intelligence
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Secure Thoughts
The Cloudflare Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
IT之家
IT之家
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Threat Research - Cisco Blogs
Stack Overflow Blog
Stack Overflow Blog
有赞技术团队
有赞技术团队
Attack and Defense Labs
Attack and Defense Labs
Y
Y Combinator Blog
Scott Helme
Scott Helme
O
OpenAI News
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
阮一峰的网络日志
阮一峰的网络日志
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
博客园 - 【当耐特】
K
Kaspersky official blog
Microsoft Azure Blog
Microsoft Azure Blog
S
SegmentFault 最新的问题
Forbes - Security
Forbes - Security
腾讯CDC
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Tailwind CSS Blog

博客园 - wastonl

Spring异步任务和定时任务 Spring ResolvableType说明 Jvm内存以及垃圾回收相关知识 RocketMQ如何保证消息可靠性 RocketMQ整体架构 mybatis-plus易忘点笔记 Spring事件异步执行设计与实现 Spring Bean销毁机制 Spring Lifecycle组件 Zipkin Brave使用 Spring Boot日志系统简要介绍 spring cloud sleuth基本使用 Spring懒加载与@Lazy注解 tomcat自动刷新响应输出流缓冲区 https碎碎念 ES脚本使用 SpringMVC静态资源处理 Maven插件运行方式 如何使用Maven将项目中的依赖打进jar包 时区以及时区对于Java时间类格式化的影响 SpringMVC处理请求头、响应头、编码行为 tomcat连接处理机制和线程模型 树组件实现 JWT示例与原理 方法句柄API使用
SpringMVC使用Resource实现二进制传输(下载)
wastonl · 2026-04-05 · via 博客园 - wastonl

在Spring MVC中,可以不直接使用HttpServletResponse对象实现下载行为,可以通过返回Resource对象来实现。

传统方式

@RequestMapping("/download")
public void download(HttpServletResponse response) throws IOException {
    ClassPathResource resource = new ClassPathResource("assert/in.txt");
    try (InputStream is = resource.getInputStream()) {
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        String contentDisposition = ContentDisposition.attachment()
            .filename("in.txt", StandardCharsets.UTF_8)
            .build()
            .toString();
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
        IOUtils.copy(is, response.getOutputStream());
    }
}

自己操作HttpServletResponse进行写入。

注意: 这里有一个容易出错的写法

@RequestMapping("/download")
public void download(HttpServletResponse response) throws IOException {
    ClassPathResource resource = new ClassPathResource("assert/in.txt");
    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        String contentDisposition = ContentDisposition.attachment()
            .filename("in.txt", StandardCharsets.UTF_8)
            .build()
            .toString();
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
    // 习惯性关闭流
    try (OutputStream os = response.getOutputStream()) {
        /*
         * 这里获取输出流逻辑发生错误时, 将会导致SpringMVC统一异常处理无效
         * 第一:由于设置了Content-Type为APPLICATION_OCTET_STREAM,前后端分类项目统一异常处理一般返回的是
         * JSON对象,这样子SpringMVC无法找到支持Content-Type为APPLICATION_OCTET_STREAM并且响应对象是一个
         * JSON对象的HttpMessageConverter, 统一异常处理会报错
         * 第二:因为响应输出流被手动关闭了, 即便统一异常处理没有错误,这后续的写入也没有任何效果,前端拿不到错误信息
         */
        try (InputStream is = getInputStrean()) {
            IOUtils.copy(is, os);
        }
    }
}

tomcat容器处理请求时会对HttpServletResponse的响应输出流进行关闭,因此无需手动关闭,必要时可以使用flush方法。

HttpServletResponse的响应输出流close后,后续再操作响应输出流,也不会报错,只是没有效果。

Resouce方式

Spring提供了很多Resource的实现,比如

  • ClassPathResource,将类路径下的资源转成Resource对象
  • FileSystemResource,将File对象转成Resource对象
  • ByteArrayResource,将字节数组转成Resource对象,这个不适用于大对象,因为不是流式的,全部加载到内存了
  • InputStreamResource,将InputStream输入流转成Resource对象

所以无论是字节数组、输入流、文件,都可以将其包装成Resource对象。

@RequestMapping("/downloadByResource")
public ResponseEntity<Resource> downloadByResource() {
    String contentDisposition = ContentDisposition.attachment()
        .filename("in.txt", StandardCharsets.UTF_8)
        .build()
        .toString();
    return ResponseEntity.ok()
        .contentType( MediaType.APPLICATION_OCTET_STREAM)
        .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
        .body(new ClassPathResource("assert/in.txt"));
}

如果流式控制要求更高,可以用这个,自己控制一次写多少到输出流中

@GetMapping("/download-stream")
public ResponseEntity<StreamingResponseBody> downloadStream() {

    StreamingResponseBody stream = outputStream -> {
        try (InputStream in = new FileInputStream("in.txt")) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = in.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
                outputStream.flush();
            }
        }
    };
    
    String contentDisposition = ContentDisposition.attachment()
        .filename("in.txt", StandardCharsets.UTF_8)
        .build()
        .toString();
    return ResponseEntity.ok()
            .contentType( MediaType.APPLICATION_OCTET_STREAM)
            .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
            .body(stream);
}