SpringBoot 骚操作:一个注解秒杀所有类型的文件下载!

下载功能应该是比较常见的功能了,虽然一个项目里面可能出现的不多,但是基本上每个项目都会有,而且有些下载功能其实还是比较繁杂的,倒不是难,而是麻烦。

所以结合之前的下载需求,我写了一个库来简化下载功能的实现

传送门:https://github.com/Linyuzai/concept/wiki/Concept-Download

如果我说现在只需要一个注解就能帮你下载任意的对象,是不是觉得非常的方便

@Download(source = "classpath:/download/README.txt")  
@GetMapping("/classpath")  
public void classpath() {  

}  

@Download
@GetMapping("/file")  
public File file() {  
    returnnew File("/Users/Shared/README.txt");  
}  

@Download
@GetMapping("/http")  
public String http() {  
    return"http://127.0.0.1:8080/concept-download/image.jpg";  
}  

感觉差别不大?那就听听我遇到的一个下载需求

我们有一个平台是管理设备的,然后每个设备都会有一个二维码图片,用一个字段存储的 http 地址

现在需要导出所有设备二维码图片的压缩包,图片名称需要用设备名称加 .png 后缀,需求上来说并不难,但是着实有点麻烦

  • 首先我需要将设备列表查出来

  • 然后使用二维码地址下载图片并写到本地缓存文件

  • 在下载之前需要先判断是否已经存在缓存

  • 下载时需要并发下载提升性能

  • 等所有图片下载结束后

  • 再生成一个压缩文件

  • 然后再操作输入输出流写到响应中

看着我实现了将近 200 行的代码,真是又臭又长,一个下载功能咋能那么麻烦呢,于是我就想有没有更简单的方式

我当时的需求很简单,我想着我只要提供需要下载的数据,比如一个文件路径,一个文件对象,一段字符串文本,一个http地址,或者混搭了前面所有类型的一个集合,甚至是我们自定义的某个类的实例,后面的事情我就不用管了

文件路径是一个文件还是一个目录?字符串文本需要先写入一个文本文件中?http资源如何下载到本地?多个文件怎么压缩?最后怎么写到响应中?我才不想花时间管这些

比如就像我现在这个需求,我只要返回设备列表就行了,其他的事情我都不用管

@Download(filename = "二维码.zip")  
@GetMapping("/download")  
public List<Device> download() {  
    return deviceService.all();  
}  

publicclass Device {  

    //设备名称  
    private String name;  

    //设备二维码  
    //注解表示该http地址是需要下载的数据  
    @SourceObject
    private String qrCodeUrl;  

    //注解表示文件名称  
    @SourceName
    public String getQrCodeName() {  
        return name + ".png";  
    }  
    //省略其他属性方法  
}  

通过在 Device 的字段上标注某些注解(或是实现某个接口)来指定文件名称和文件地址

如果能这样实现,省时省心省力,又多了写 199 行代码的摸鱼时间难道不香么

思路

下面来讲讲这个库的主要设计思路,以及中间遇到的坑,大家有兴趣可以继续往下看

其实基于一开始的设想,我觉得功能并没有多复杂,于是就决定开肝

只是万万没想到实现起来比我想象的更复杂(这是后话了)

基础

首先整个库基于响应式编程,但却并不是完全意义上的响应式,只能说是Mono<InputStream>这样的。。。奇怪组合?

为什么会这样呢,很大的一个原因是由于需要兼容webmvc和webflux,导致我仅仅是将之前实现的InputStream方式重构成了响应式,所以就出现了这样的组合

这也是我遇到的最大的一个坑,我先前已经基本调通了基于Servlet的整个下载流程,然后就想着支持一下webflux

大家都知道webmvc中,我们可以通过RequestContextHolder来获得请求和响应对象,但是在webflux中就不行了,当然我们可以在方法参数中注入

@Download(source = "classpath:/download/README.txt")  
@GetMapping("/classpath")  
public void classpath(ServerHttpResponse response) {  
  
}  

结合Spring自带的注入功能,我们就可以通过AOP拿到响应的入参了,但是总觉得这样写有点多余࿰