在日常开发中,文件下载是一个常见的功能,虽然在项目中出现的频率可能不算太高,但几乎每个项目都会涉及。而有些下载需求相对复杂,虽然不是难点,但实现起来却十分繁琐。
因此,为了简化这一过程,有一个工具库,使得下载功能的实现变得更加简单快捷。
https://github.com/Linyuzai/concept/wiki/Concept-Download
一键下载任意对象
如果告诉你,现在仅需一个注解就能轻松下载任意对象,你会不会觉得很方便?
import com.icoderoad.download.annotation.Download;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
@RestController
public class DownloadController {
@Download(source = "classpath:/download/README.txt")
@GetMapping("/classpath")
public void downloadFromClasspath() {
}
@Download
@GetMapping("/file")
public File downloadFile() {
return new File("/Users/Shared/README.txt");
}
@Download
@GetMapping("/http")
public String downloadFromHttp() {
return "http://127.0.0.1:8080/icoderoad-download/image.jpg";
}
}
看起来似乎没有太大变化?那让我们看看一个实际场景。
真实业务中的应用
在一个设备管理平台中,每个设备都会有一个二维码图片,其地址存储在数据库的一个字段中。现需导出所有设备的二维码图片,并以设备名称命名,最终打包成 ZIP 文件。
实现这一需求,需要:
- 查询设备列表。
- 根据二维码 URL 下载图片并存入本地缓存。
- 处理缓存判断,避免重复下载。
- 并发下载以提升性能。
- 下载完成后生成 ZIP 文件。
- 将 ZIP 文件写入响应流。
整个实现过程大约需要 200 行代码,显得十分冗长繁琐。于是我思考是否有更简单的方法。
其实,我们只需要提供待下载的数据,比如文件路径、文件对象、文本内容、HTTP 地址,甚至是一个自定义对象,而无需关注下载逻辑。
于是,我们可以这样简化实现:
import com.icoderoad.download.annotation.Download;
import com.icoderoad.download.annotation.SourceName;
import com.icoderoad.download.annotation.SourceObject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class DeviceDownloadController {
private final DeviceService deviceService;
public DeviceDownloadController(DeviceService deviceService) {
this.deviceService = deviceService;
}
@Download(filename = "二维码.zip")
@GetMapping("/download")
public List<Device> downloadDevices() {
return deviceService.all();
}
}
class Device {
private String name;
@SourceObject
private String qrCodeUrl;
@SourceName
public String getQrCodeName() {
return name + ".png";
}
}
只需标注注解,系统会自动处理文件名称、下载内容、打包等逻辑,无需手动编写大量代码。
设计思路
这一功能的核心思想是基于 AOP 拦截下载请求,并结合 Spring WebFlux 进行异步处理。
@Download 注解说明
参数 | 说明 |
| 需要下载的内容,但是优先级低于返回值 如果方法返回值不为 |
| 如果为 |
| 指定下载时浏览器上显示的名称 如果不指定则会获取下载内容的名称,如文件则使用文件名 |
| 如果未指定,会尝试获取 如果尝试获取失败,则默认 |
| 压缩格式,默认 |
| 强制压缩 如果为 |
| 如果下载包含中文的文本文件出现乱码,可以尝试指定编码 |
| 统一的响应头,每2个为一组 |
| 额外的数据,当需要自行编写额外流程业务时可能会用到 |
整体流程
图片
响应式支持
为了兼容 Spring WebFlux,我们需要获取 ServerHttpResponse,但不能直接使用 RequestContextHolder,因此可以通过 WebFilter 进行注入:
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
public class ReactiveDownloadFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put(ServerHttpResponse.class, exchange.getResponse()));
}
}
然后在需要的地方通过 ReactiveDownloadHolder 获取响应对象。
import org.springframework.web.server.ServerHttpResponse;
import reactor.core.publisher.Mono;
public class ReactiveDownloadHolder {
public static Mono<ServerHttpResponse> getResponse() {
return Mono.deferContextual(ctx -> Mono.just(ctx.get(ServerHttpResponse.class)));
}
}
处理下载任务
下载任务分为多个步骤,例如:
- 获取文件路径或 File 对象。
- 如果是多个文件,则先进行压缩处理。
- 将最终文件写入响应流。
因此,我们采用类似 Spring Cloud Gateway 过滤链的方式,设计了 DownloadHandler:
import reactor.core.publisher.Mono;
public interface DownloadHandler {
Mono<Void> handle(DownloadContext context, DownloadHandlerChain chain);
}
每个 DownloadHandler 处理特定任务,如下载、压缩、写入响应流等。
适配多种数据源
不同类型的下载对象需要不同的处理方式,例如文件、HTTP 地址、自定义对象等,因此我们抽象出 Source 接口,并通过 SourceFactory 进行匹配。
public interface SourceFactory {
boolean support(Object source, DownloadContext context);
Source create(Object source, DownloadContext context);
}
例如:
public class FileSourceFactory implements SourceFactory {
@Override
public boolean support(Object source, DownloadContext context) {
return source instanceof File;
}
@Override
public Source create(Object source, DownloadContext context) {
return new FileSource((File) source);
}
}
结语
这个工具库极大简化了文件下载功能,尤其是针对复杂的批量下载需求,只需简单的注解即可完成。如果你正在开发 SpringBoot 3.4 版本的项目,并需要实现高效的下载功能,不妨试试这个方案!