Concept-Download: A Backend Library for Simplified File Download in Spring
This article introduces the Concept-Download library, explains how a single @Download annotation can handle various download sources—including files, HTTP URLs, and custom objects—by leveraging reactive programming, handler chains, source factories, concurrent loading, compression, and unified response writing for both Spring MVC and WebFlux.
Download functionality is common but often cumbersome; the author created a library to simplify its implementation in Spring-based backend projects.
@Download(source = "classpath:/download/README.txt")
@GetMapping("/classpath")
public void classpath() {}
@Download
@GetMapping("/file")
public File file() {
return new File("/Users/Shared/README.txt");
}
@Download
@GetMapping("/http")
public String http() {
return "http://127.0.0.1:8080/concept-download/image.jpg";
}For a real scenario—exporting QR‑code images of devices as a zip file—the author needed to fetch device lists, download images concurrently, check caches, compress files, and stream the result, which originally required ~200 lines of code.
@Download(filename = "二维码.zip")
@GetMapping("/download")
public List
download() {
return deviceService.all();
}
public class Device {
// 设备名称
private String name;
// 设备二维码 (http 地址需要下载)
@SourceObject
private String qrCodeUrl;
// 文件名称
@SourceName
public String getQrCodeName() {
return name + ".png";
}
// 省略其他属性方法
}The library is built on reactive programming, using Mono<InputStream> to bridge WebMVC and WebFlux. A custom ReactiveDownloadFilter captures the response object, storing it in a context accessible via ReactiveDownloadHolder .
public class ReactiveDownloadFilter implements WebFilter {
@Override
public Mono
filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put(ServerHttpRequest.class, request))
.contextWrite(ctx -> ctx.put(ServerHttpResponse.class, response));
}
}
public class ReactiveDownloadHolder {
public static Mono
getRequest() {
return Mono.deferContextual(c -> Mono.just(c.get(ServerHttpRequest.class)));
}
public static Mono
getResponse() {
return Mono.deferContextual(c -> Mono.just(c.get(ServerHttpResponse.class)));
}
}Processing is organized as a chain of DownloadHandler objects, each implementing a single step (e.g., loading sources, compressing, writing response). The chain can be extended arbitrarily, similar to Spring Cloud Gateway.
public interface DownloadHandler extends OrderProvider {
Mono
handle(DownloadContext context, DownloadHandlerChain chain);
}
public interface DownloadHandlerChain {
Mono
next(DownloadContext context);
}A DownloadContext travels through the chain, sharing intermediate results. Different SourceFactory implementations create Source objects for files, HTTP URLs, or custom classes.
public interface SourceFactory extends OrderProvider {
boolean support(Object source, DownloadContext context);
Source create(Object source, DownloadContext context);
}Concurrent loading of remote resources is handled by SourceLoader , allowing custom thread‑pool or coroutine strategies.
public interface SourceLoader {
Mono
load(Source source, DownloadContext context);
}Compression is abstracted via SourceCompressor , with a default ZIP implementation but extensible to other formats.
public interface SourceCompressor extends OrderProvider {
String getFormat();
default boolean support(String format, DownloadContext context) { return format.equalsIgnoreCase(getFormat()); }
Compression compress(Source source, DownloadWriter writer, DownloadContext context);
}Response writing unifies HttpServletResponse and ServerHttpResponse through DownloadResponse . For WebFlux, a FluxSinkOutputStream adapts an OutputStream to a reactive DataBuffer stream.
public class ReactiveDownloadResponse implements DownloadResponse {
private final ServerHttpResponse response;
private OutputStream os;
private Mono
mono;
// write method creates FluxSinkOutputStream and writes bytes
}
public static class FluxSinkOutputStream extends OutputStream {
private final FluxSink
fluxSink;
private final ServerHttpResponse response;
@Override public void write(byte[] b) { writeSink(b); }
// other write overloads delegate to writeSink
private void writeSink(byte... bytes) {
DataBuffer buffer = response.bufferFactory().wrap(bytes);
fluxSink.next(buffer);
DataBufferUtils.release(buffer);
}
@Override public void flush() { fluxSink.complete(); }
}The DownloadWriter handles the actual byte transfer, supporting range requests, charset conversion, and progress callbacks.
public interface DownloadWriter extends OrderProvider {
boolean support(Resource resource, Range range, DownloadContext context);
void write(InputStream is, OutputStream os, Range range, Charset charset, Long length, Callback callback);
interface Callback { void onWrite(long current, long increase); }
}Event publishing via DownloadEventPublisher and listeners provides flexible logging of each step, progress updates, and total time spent, helping to discover bugs.
Additional pitfalls include the need to trigger context destruction after response writing, which required moving the destroy step into a doAfterTerminate hook because the reactive chain ends with Mono.empty() .
In conclusion, the library demonstrates advanced backend techniques—reactive programming, modular handler chains, custom source handling, and unified response writing—while drastically reducing boilerplate for complex download scenarios.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.