环境:SpringBoot3.2.5
1. 复现坑
这是一个关于使用Resource下载文件引发的问题。示例代码如下:
("/download")public class StreamDownloadController {("")public ResponseEntity<Resource> download() throws Exception {// 故意写错String contentType = "textplain" ;// 确保文件是存在的InputStream is = new FileInputStream(new File("d:\\1.txt")) ;Resource resource = new InputStreamResource(is) ;return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, contentType).body(resource) ;}
上面代码通过ResponseEntity实现文件的下载,设置的Content-Type header故意写错,正确应该是格式 "*/*"。调用上面接口后,程序报错:

错误信息就是我们这是的Content-Type错误。仅这个错误还好,关键出现错误后会引发资源泄漏问题,也就是说我们的程序还会继续占用这个1.txt文件资源,通过删除该文件可以看到提示:

试想如果我们这个接口是在高并发环境下,那么定会引起操作系统中的文件句柄被耗尽。引起原因是没有关闭文件流。
2. 解决方案
2.1 错误的解决方案
可能你会说,用try-with-resources,下面将代码改为这种方式
@GetMapping("")public ResponseEntity<Resource> download() throws Exception {try (InputStream is = new FileInputStream(new File("d:\\1.txt"))) {Resource resource = new InputStreamResource(is) ;return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, "text/plain").body(resource) ;}}
注意:上面代码处理没有问题,Content-Type也是正确的。

报了其它的错误“Stream Closed”。这需要你对SpringMVC底层原理有了解,简单说下Controller接口调用:
调用接口download方法,获取到返回值
这时候这个download方法已经执行完了,try-with执行完文件流当然要关闭了。
得到上一步的返回值,通过结果类型找到对应的HandlerMethodReturnValueHandler处理器,输出结果。这时候才真正的去读取文件流,但是文件流已经在上一步中关闭。所以就出现了上面的错误。
2.2 正确的解决方案
方法1
这种方法必须放弃使用ResponseEntity返回值方式,如下代码:
@GetMapping("")public void download(HttpServletResponse response) throws Exception {try (InputStream is = new FileInputStream(new File("d:\\1.txt"))) {response.setContentType("xxxooo") ;is.transferTo(response.getOutputStream()) ;}}
这种方法流打开及读取发送客户端都在try中,不会出现上面的问题。
方法2
如果你的返回值必须是ResponseEntity那么,只能通过如下方式实现:
("")public ResponseEntity<Resource> download3() throws Exception {Resource resource = new AbstractResource() {public InputStream getInputStream() throws IOException {// 只有真正的在进行读取的时候才打开文件流InputStream is = new FileInputStream(new File("d:\\1.txt")) ;return is ;}public String getDescription() {return "下载文件" ;}};return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, "xxxooo").body(resource) ;}
但是这种方式编码稍有点麻烦,需要自定义Resource实现。
方法3
该方法需要升级Spring版本,才发布没几天的6.1.7,在该版本中对InputStreamResource类增加了一个构造方法,如下:
public InputStreamResource(InputStreamSource inputStreamSource)
该参数可以作为lambda表达式提供,该表达式可以延迟检索实际的InputStream。
接口改造如下
@GetMapping("")public ResponseEntity<Resource> download() throws Exception {Resource resource = new InputStreamResource(new FileSystemResource(new File("d:\\1.txt"))) ;return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, "xxxooo").body(resource) ;}
该方法的原理与上面一样。
注意:我使用的SpringBoot版本是3.2.5对应Spring版本是6.1.6,所以你进行如下配置:
<spring-framework.version>6.1.7</spring-framework.version>
这样能替换默认的版本。
以上是本篇文章的全部内容,如对你有帮助就请作者吃个棒棒糖🍭。
推荐文章
实战案例SpringBoot整合Seata AT模式实现分布式事务【超详细】
Spring6.2震撼来袭,多线程实例化Bean应用启动速度飙升!
SpringBoot整合Flink CDC,实时追踪数据变动,无缝同步至Redis
【高效开发】使用Spring Data JPA的QBE功能,轻松构建查询条件



