SpringMVC使用Resource实现二进制传输(下载)

在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);
}
posted on 2026-04-05 12:20  wastonl  阅读(1)  评论(0)    收藏  举报