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

推荐订阅源

Project Zero
Project Zero
Security Archives - TechRepublic
Security Archives - TechRepublic
C
Cyber Attacks, Cyber Crime and Cyber Security
Security Latest
Security Latest
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
S
Schneier on Security
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
Intezer
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
F
Full Disclosure
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
MyScale Blog
MyScale Blog
Hacker News: Ask HN
Hacker News: Ask HN
G
Google Developers Blog
H
Heimdal Security Blog
O
OpenAI News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LangChain Blog
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
IT之家
IT之家
Cyberwarzone
Cyberwarzone
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Know Your Adversary
Know Your Adversary
博客园 - 聂微东
The Cloudflare Blog
C
Check Point Blog
K
Kaspersky official blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Tor Project blog
T
Threat Research - Cisco Blogs
T
Tailwind CSS Blog
P
Proofpoint News Feed
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
A
About on SuperTechFans
小众软件
小众软件
Cloudbric
Cloudbric
A
Arctic Wolf

小松鼠的博客

记录一次线上k8s工作节点无法创建容器的问题排查思路与解决办法 记一次线上GoLang项目OOM排查过程 从LastPass转向拥抱开源KeePass的心路历程 故障定位与 AI 结合前后端编码实践 FileBeat收集nginx-ingress-controller日志 K8s云原生环境下文件描述符占用过高查询思路 2024年最新关闭火绒安全工具的开机自启方法 Kubernetes任务调度实践-Go语言实现Job和CronJob对比分析 离线更新k8s环境下的trivy漏洞库方法 使用Go语言接入Choerodon实现基于OAuth2的统一身份认证登录 在Vue2中自定义Switch组件并实现父子组件双向数据绑定 关于docker jdk1.8镜像中的GB18030-2022标准支持及验证 Go框架gin中的session存储gin-contrib-sessions和go-session 关于修改node_module中的源码问题记录 docker-compose网络和内网服务IP冲突问题 慎用存储过程:一条语句引发的数据库存储100%占用 避坑-不能将specific类型的gitlab-runner改变为share类型 Docker compose中的MySQL主从复制模式和percona-toolkit工具使用 在minio中开启https访问以及使用rclone备份minio桶 在多机Docker环境下部署Choerodon的解决方案 Prometheus中Monitor添加对SpringBoot Actuator的Basic认证 在Nginx的容器镜像中隐藏Nginx的Server响应头 K8s中的两种nginx-ingress-controller及其区别 两个docker工具:runlike和whaler Grafana中的邮件报警和截图插件grafana-image-enderer K8s中externalName-service和services-without-selectors maven配置文件settings.xml中的一些概念总结 K8s中flexvolume插件驱动的安装 K8s中的coredns无法解析svc问题排查 K8s中使用Ingress访问请求体过大问题解决 关于k8s中对于SpringBoot应用TCP类型的就绪探针不准确的问题发现 K8s中的环境变量与应用程序的对应关系与操作 SpringMVC4升级为SpringBoot2实战 在Vmware中Ubuntu22.04的vm-tools和网络问题 修改k8s节点主机名并重新加入集群 离线安装Grafana插件 Spring Data Jpa 中使用CriteriaBuilder动态拼接SQL 在SpringBoot项目配置Liquibase数据库版本管理 记录Vue中父子组件传值的实战应用 实现单例模式的8种方法 三种常用的生产者消费者模式实现 使用两个线程交替打印0-100的奇偶数 关于部署于JBoss5中的Spring应用获取项目真实部署路径的问题 获取下一个完全对称日 通过短信验证码验证修改密码的解决方案 在Win10中使用Win+R快速启动软件 使用RSA加解密时注意Cipher.getInstance(String var0,Provider var1)提供的Provider是否正确 在RestEasy2.x中解决接口重复提交问题 几道简单的CTF题目思路 重温Spring---Spring事务控制与基于XML和注解的配置方法 重温Spring---Spring AOP基于XML和注解的配置 重温Spring---AOP动态代理和Spring AOP及其基本原理 重温Spring---Spring IOC基于XML和注解的配置和比较 在Windows10中安装MySQL5.7 Zip版本及常用配置 重温Spring---使用Spring IOC解决程序耦合 策略模式与责任链模式实战应用 Linux上直接打开war包修改文件 在Windows上运行两个微信的简单脚本 ThreadPoolExecutor的使用方法与分页查询数据实例 IDEA中Shelve Changes 和 Git Stash 通过resteasy发布RESTful接口 解决前端请求后台接口,后台报错Can not deserialize instance of java.util.ArrayList out of START_OBJECT token 使用VBA脚本汇总Excel文档 使用Jenkins+GitLab实现自动部署vue项目 Kubernetes:使用hostPath挂载nginx集群的配置文件和html 彻底搞定VirtualBox虚拟机的网络设定 在Docker中安装MySQL5.7并开启远程访问(附授权和修改密码方式) 利用git命令和java文件流 获取自己改动过的文件 浅谈Spring定时任务的使用(Scheduled注解) 在Spring项目简单配置Flyway(V4.2版本)数据库版本管理 解决Spring单元测试中因外键关联导致的失败integrity constraint violation:foreign key no action Redis安装与哨兵模式配置入门 关于Vue中使用Element-UI样式row-class-name失效的问题 Element-UI中实现可动态增加行列和可编辑单元格的表格 Windows系统查看端口占用、结束进程方法和命令 层次分析法(AHP)分析步骤与计算方法 源码分析之解决layui框架重载表格时额外参数不清空的问题 Spring Data Jpa 返回自定义对象(实体部分属性、多表联查) 如何将一个jar放到本地maven仓库中 关于SSM项目停止Tomcat时Log4j出现java.lang.NoClassDefFoundError: 获取el-table单元格值并根据该值对元素自定义样式渲染 解决Git每次push都要重新输入账号密码和HttpRequestException encountered的问题 解决前后端分离项目中Vue不带cookies的问题 SSM集成Shiro自定义权限过滤器不执行解决方案 SSM集成Shiro不进入自定义Realm的doGetAuthorizationInfo的解决方案 Vue+SSM中使用Token验证登录 Git拉代码推送代码提示密码错误如何修改 Git配置SSH Key(Git配置多个账户) 安装Tomcat服务器以及错误汇总(tomcat8.0、jdk8) 关于我
Spring Boot中4种文件下载方法的实现
ycyin · 2023-05-20 · via 小松鼠的博客

2023年5月19日大约 5 分钟SpringSpring Boot


本篇文章介绍Spring Boot中分别返回ResponseEntity<InputStreamResource>ResponseEntity<Resource>ResponseEntity<StreamingResponseBody>HttpServletResponse.getOutputStream()4种文件下载方法的实现。

先看看Controller比较直观的展示效果:

@RestController
@RequestMapping("v1/api/file")
public class FileController {
    // spring 注入泛型接口实现
    private final DownLoadFileService<InputStreamResource> isrDownLoadFileService;
    private final DownLoadFileService<Resource> rDownLoadFileService;
    private final DownLoadFileService<StreamingResponseBody> srbDownLoadFileService;
    private final DownLoadFileService srDownLoadFileService;

    public FileController(DownLoadFileService<InputStreamResource> isrDownLoadFileService, DownLoadFileService<Resource> rDownLoadFileService, DownLoadFileService<StreamingResponseBody> srbDownLoadFileService, DownLoadFileService srDownLoadFileService) {
        this.isrDownLoadFileService = isrDownLoadFileService;
        this.rDownLoadFileService = rDownLoadFileService;
        this.srbDownLoadFileService = srbDownLoadFileService;
        this.srDownLoadFileService = srDownLoadFileService;
    }
    
    @RequestMapping(value = "/download1", method = RequestMethod.GET,produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE})
    @SneakyThrows
    public ResponseEntity<InputStreamResource> downloadFile1(String id){
        return isrDownLoadFileService.downloadFile(id);
    }

    @RequestMapping(value = "/download2", method = RequestMethod.GET, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    @SneakyThrows
    public ResponseEntity<Resource> downloadFile2(String id) {
        return rDownLoadFileService.downloadFile(id);
    }

    @GetMapping(value = "/download3", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<StreamingResponseBody> downloadFile3(@RequestParam(name = "id") String id) {
        return srbDownLoadFileService.downloadFile(id);
    }

    @GetMapping(value = "/download4")
    public void downloadFile4(@RequestParam(name = "id") String id) {
        srDownLoadFileService.downloadFile(id);
    }

}

前言

springboot:2.3.9.RELEASE

mybatis-plus: 3.5.2

本文的文件访问依赖使用minioClient,所有的InputStream都来自minioTemplate.get(thirdFile.getName());,篇幅有限,访问minio具体实现也比较简单就不在本文中介绍,也可以使用其它方式比如本地文件获取文件输入流InputStream。

其实本文介绍的这几种方式原理都是一样的,最终都是往HttpServletResponse输出流,由此还可以做出更多的变通,比如还可以返回ResponseEntity<ByteArrayResource> 、ResponseEntity<byte[]>等,这里可以去看看ResponseEntity的原理:

ResponseEntity对应一个http请求或者响应,可以用在controller的返回值里,方便处理header及status。而通常使用的@ResponseBody注解,只能处理body部分。这也是为什么通常在下载场景中会使用ResponseEntity,因为下载需要设置header里的content-type以及特殊的status(比如206)。

ResponseEntity类型的返回值由一个特殊的HttpEntityMethodProcessor类型的returnTypeHandler来处理,主要是将ResponseEntity里设置的header和status写入到httpResponse中,body部分调用的是父类的模板方法。

必需的配置

  1. 添加HttpMessageConverter转换器配置

    不添加会报错:springboot no converter for [class org.springframework.core.io.inputstreamresource] with preset content-type '.....']

    报这个错误的原因大概就是我们在Controller层使用@RestController注解,这个注解包含@ResponseBody注解,AbstractMessageConverterMethodProcessor类的子类RequestResponseBodyMethodProcessor是用来处理@Responsebody注解的,而HttpEntityMethodProcessorAbstractMessageConverterMethodProcessor的另一个子类,该processor专门处理返回值类型是ResponseEntity类型的controller返回值。

    所以按照原理来说,不使用@RestController@Responsebody注解就可以不用添加这个HttpMessageConverter转换器配置,但是我这里不知道为啥即使不使用@RestController注解还是要必须添加这个转换器配置才行。有懂的老铁欢迎评论。

    @Configuration
    public class WebMVCConfig implements WebMvcConfigurer {
    
        public void    configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            ResourceHttpMessageConverter resHttpMessageConverter = new ResourceHttpMessageConverter();
            final List<MediaType> list = new ArrayList<>();
            list.add(MediaType.IMAGE_JPEG);
            list.add(MediaType.IMAGE_PNG);
            list.add(MediaType.APPLICATION_OCTET_STREAM);
            resHttpMessageConverter.setSupportedMediaTypes(list);
            converters.add(resHttpMessageConverter);
        }
    }
    
  2. 使用到的实体和Mapper

    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface FileMapper extends BaseMapper<ThirdFile> {
    }
    
    import com.alibaba.fastjson.annotation.JSONField;
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Builder;
    import lombok.Data;
    
    @TableName("third_file")
    @Data
    @Builder
    public class ThirdFile {
        @TableId(value = "id",type = IdType.INPUT)
        private String id;
    
        @TableField("name")
        private String name;
    
        @TableField("size")
        private Long size;
    
        @TableField("version")
        private Integer version;
    
        @TableField("create_time")
        @JSONField(name = "create_time")
        private Long createTime;
    
        @TableField("update_time")
        @JSONField(name = "update_time")
        private Long updateTime;
    }
    

定义泛型接口

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public interface DownLoadFileService<T> {

    ResponseEntity<T> downloadFile(String id);

    default HttpHeaders getHttpHeaders(String filename) throws UnsupportedEncodingException {
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", URLEncoder.encode(filename, StandardCharsets.UTF_8.name())));
        headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
        headers.add(HttpHeaders.PRAGMA, "no-cache");
        headers.add(HttpHeaders.EXPIRES, "0");
        return headers;
    }
}

具体实现

第一种:返回InputStreamResource

import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import javax.servlet.ServletContext;
import java.io.InputStream;

@Log4j2
@Service
public class InputStreamResourceDownService implements DownLoadFileService<InputStreamResource> {


    private final MinioTemplate minioTemplate;
    private final FileMapper fileMapper;
    private final ServletContext servletContext;

    public InputStreamResourceDownService(MinioTemplate minioTemplate, FileMapper fileMapper, ServletContext servletContext) {
        this.minioTemplate = minioTemplate;
        this.fileMapper = fileMapper;
        this.servletContext = servletContext;
    }

    @SneakyThrows
    @Override
    public ResponseEntity<InputStreamResource> downloadFile(String id) {
        ThirdFile thirdFile = fileMapper.selectById(id);
        InputStream inputStream = minioTemplate.get(thirdFile.getName());
        InputStreamResource inputStreamResource = new InputStreamResource(inputStream);

        String fileName = thirdFile.getName();
        MediaType mediaType = MediaTypeUtils.getMediaTypeForFileName(this.servletContext, fileName);
        log.info("fileName: " + fileName);
        log.info("mediaType: " + mediaType);

        return ResponseEntity
                .ok()
                .headers(getHttpHeaders(fileName))
                .contentLength(thirdFile.getSize())
                .contentType(mediaType)
                .body(inputStreamResource);
    }
}

第二种:返回Resource

import lombok.SneakyThrows;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;

@Service
public class ResourceDownLoadService implements DownLoadFileService<Resource> {

    private final MinioTemplate minioTemplate;
    private final FileMapper fileMapper;

    public ResourceDownLoadService(MinioTemplate minioTemplate, FileMapper fileMapper) {
        this.minioTemplate = minioTemplate;
        this.fileMapper = fileMapper;
    }

    @SneakyThrows
    @Override
    public ResponseEntity<Resource> downloadFile(String id) {
        ThirdFile thirdFile = fileMapper.selectById(id);
        InputStream inputStream = minioTemplate.get(thirdFile.getName());
        // 读取 InputStream 的数据到字节数组中
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        byte[] fileBytes = outputStream.toByteArray();

        return ResponseEntity.ok()
                .headers(getHttpHeaders(thirdFile.getName()))
                .contentLength(fileBytes.length)
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(new ByteArrayResource(fileBytes));
    }
}

第三种:返回StreamingResponseBody

import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.IOException;
import java.io.InputStream;

@Service
public class StreamingResponseBodyDownLoadService implements DownLoadFileService<StreamingResponseBody> {
    private final MinioTemplate minioTemplate;
    private final FileMapper fileMapper;

    public StreamingResponseBodyDownLoadService(MinioTemplate minioTemplate, FileMapper fileMapper) {
        this.minioTemplate = minioTemplate;
        this.fileMapper = fileMapper;
    }

    @SneakyThrows
    @Override
    public ResponseEntity<StreamingResponseBody> downloadFile(String id) {

        ThirdFile thirdFile = fileMapper.selectById(id);
        InputStream inputStream = minioTemplate.get(thirdFile.getName());

        return ResponseEntity.ok()
                .headers(getHttpHeaders(thirdFile.getName()))
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(outputStream -> {
                    try (InputStream inputStream2 = inputStream) {
                        StreamUtils.copy(inputStream2, outputStream);
                    } catch (IOException ignored) {

                    }
                });
    }
}

第四种:返回HttpServletResponse输出流

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;


@Log4j2
@Service("srDownLoadFileService")
public class ServletResponseDownService implements DownLoadFileService {

    private final MinioTemplate minioTemplate;
    private final FileMapper fileMapper;
    private final ServletContext servletContext;
    private final HttpServletResponse response;

    public ServletResponseDownService(MinioTemplate minioTemplate, FileMapper fileMapper, ServletContext servletContext, HttpServletResponse response) {
        this.minioTemplate = minioTemplate;
        this.fileMapper = fileMapper;
        this.servletContext = servletContext;
        this.response = response;
    }

    @SneakyThrows
    @Override
    public ResponseEntity downloadFile(String id) {
        ThirdFile thirdFile = fileMapper.selectById(id);
        InputStream inputStream = minioTemplate.get(thirdFile.getName());
        String fileName = thirdFile.getName();
        MediaType mediaType = MediaTypeUtils.getMediaTypeForFileName(this.servletContext, fileName);
        log.info("fileName: " + fileName);
        log.info("mediaType: " + mediaType);

        // Content-Type, eg: application/pdf
        response.setContentType(mediaType.getType());

        // Content-Disposition
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));

        // Content-Length
        response.setContentLength(Math.toIntExact(thirdFile.getSize()));

        BufferedInputStream inStream = new BufferedInputStream(inputStream);
        BufferedOutputStream outStream = new BufferedOutputStream(response.getOutputStream());

        byte[] buffer = new byte[1024];
        int bytesRead = 0;
        while ((bytesRead = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, bytesRead);
        }
        outStream.flush();
        inStream.close();
        return null;
    }
}

Spring注入泛型接口使用

@Log4j2
@RestController
@RequestMapping("v1/api/file")
public class FileController {

    // 泛型接口注入
    private final DownLoadFileService<InputStreamResource> isrDownLoadFileService;
    private final DownLoadFileService<Resource> rDownLoadFileService;
    private final DownLoadFileService<StreamingResponseBody> srbDownLoadFileService;
    private final DownLoadFileService srDownLoadFileService;

    public FileController(DownLoadFileService<InputStreamResource> isrDownLoadFileService, DownLoadFileService<Resource> rDownLoadFileService, DownLoadFileService<StreamingResponseBody> srbDownLoadFileService, DownLoadFileService srDownLoadFileService) {
        this.isrDownLoadFileService = isrDownLoadFileService;
        this.rDownLoadFileService = rDownLoadFileService;
        this.srbDownLoadFileService = srbDownLoadFileService;
        this.srDownLoadFileService = srDownLoadFileService;
    }

    @RequestMapping(value = "/download1", method = RequestMethod.GET,produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE})
    @SneakyThrows
    public ResponseEntity<InputStreamResource> downloadFile1(String id){
        return isrDownLoadFileService.downloadFile(id);
    }

    @RequestMapping(value = "/download2", method = RequestMethod.GET, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    @SneakyThrows
    public ResponseEntity<Resource> downloadFile2(String id) {
        return rDownLoadFileService.downloadFile(id);
    }

    @GetMapping(value = "/download3", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<StreamingResponseBody> downloadFile3(@RequestParam(name = "id") String id) {
        return srbDownLoadFileService.downloadFile(id);
    }


    @GetMapping(value = "/download4")
    public void downloadFile4(@RequestParam(name = "id") String id) {
        srDownLoadFileService.downloadFile(id);
    }


}

参考:

some ways for creating the file downloading function

How to solve "No converter for class B with preset Content-Type 'image/png'" error in Spring when trying to serve an image?

ResponseEntity 使用 及 原理

springboot 使用ResponseEntity实现文件流下载

SpringBoot中文件二进制流下载功能Api

Java- 泛型机制详解

java 泛型详解

Spring 框架:泛型接口的自动注入