Simplified Download Library for Spring Backend: Design, Implementation, and Usage
This article introduces a Spring‑based download library that simplifies file, HTTP resource, and custom object downloads by using a single @Download annotation, explains its reactive architecture, source handling, concurrent loading, compression, response writing, and event‑driven logging, and provides extensive code examples.
Downloading files is a common yet often cumbersome feature in many projects; this article presents a library that abstracts the download process into a single annotation, allowing developers to return any data type—file paths, HTTP URLs, strings, or custom objects—without handling the underlying details.
Portal: https://github.com/Linyuzai/concept/wiki/Concept-Download
For a typical requirement of exporting device QR‑code images as a zip file, the library handles steps such as retrieving device lists, downloading images, checking caches, concurrent loading, compression, and writing the result to the HTTP response.
@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"; }The library is built on reactive programming (Mono<InputStream>) to support both WebMvc and WebFlux, using a custom ReactiveDownloadFilter to capture request/response objects in the reactive context.
public class ReactiveDownloadFilter implements WebFilter {
@Override
public Mono<Void> 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<ServerHttpRequest> getRequest() {
return Mono.deferContextual(c -> Mono.just(c.get(ServerHttpRequest.class)));
}
public static Mono<ServerHttpResponse> getResponse() {
return Mono.deferContextual(c -> Mono.just(c.get(ServerHttpResponse.class)));
}
}Download sources are abstracted as Source implementations (e.g., FileSource, HttpSource) created by SourceFactory based on the original data type, and custom objects can be supported via annotations like @SourceObject and @SourceName.
@Download(filename = "二维码.zip")
@GetMapping("/download")
public List<Device> download() { return deviceService.all(); }
public class Device {
private String name;
@SourceObject
private String qrCodeUrl;
@SourceName
public String getQrCodeName() { return name + ".png"; }
}Concurrent loading is handled by SourceLoader implementations, allowing flexible thread‑pool or coroutine strategies. Compression is performed by SourceCompressor (e.g., ZIP) with optional in‑memory or file‑based output.
public interface SourceLoader {
Mono<Source> load(Source source, DownloadContext context);
}
public interface SourceCompressor extends OrderProvider {
String getFormat();
default boolean support(String format, DownloadContext ctx) { return format.equalsIgnoreCase(getFormat()); }
Compression compress(Source source, DownloadWriter writer, DownloadContext ctx);
}Response writing abstracts both HttpServletResponse and ServerHttpResponse via DownloadResponse, with a reactive implementation that wraps a FluxSink as an OutputStream to emit DataBuffer objects.
public class ReactiveDownloadResponse implements DownloadResponse {
private final ServerHttpResponse response;
private OutputStream os;
private Mono<Void> mono;
@Override
public Mono<Void> write(Consumer<OutputStream> consumer) {
if (os == null) {
mono = response.writeWith(Flux.create(sink -> {
try { os = new FluxSinkOutputStream(sink, response); consumer.accept(os); }
catch (Throwable e) { sink.error(e); }
}));
} else { consumer.accept(os); }
return mono;
}
// FluxSinkOutputStream implementation omitted for brevity
}Event publishing via DownloadEventPublisher and listeners enables flexible logging of each step, progress updates, and timing, helping to discover bugs and monitor performance.
Finally, the article notes pitfalls such as context destruction not being triggered in WebFlux after response writing, which was solved by moving initialization/destruction to doAfterTerminate in the reactive chain.
Top Architecture Tech Stack
Sharing Java and Python tech insights, with occasional practical development tool tips.
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.
