下载功能应该是比较常见的功能了,虽然一个项目里面可能出现的不多,但是基本上每个项目都会有,而且有些下载功能其实还是比较繁杂的,倒不是难,而是麻烦。
所以结合之前的下载需求,我写了一个库来简化下载功能的实现
传送门:
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拿到响应的入参了,但是总觉得这样写有点多余