Simplify Complex File Downloads with One Annotation – Inside the Concept-Download Library
This article introduces a Spring‑based library that reduces multi‑step download, compression, and response handling to a single annotation, supporting files, HTTP URLs, strings, collections and custom objects across WebMvc and WebFlux with reactive programming.
Introduction
Download functionality is common but often cumbersome; this article introduces a library that simplifies its implementation.
Portal: https://github.com/Linyuzai/concept/wiki/Concept-Download
With a single annotation you can download any object.
@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";
}Example requirement: export QR‑code images of devices as a zip file, naming each file with the device name.
Query device list
Download QR‑code images to local cache
Check cache existence
Concurrent download for performance
Wait for all downloads
Generate zip file
Write to response
The implementation grew to about 200 lines, prompting a more concise solution.
Goal: provide data (file path, object, string, HTTP URL, collection, or custom class) and let the library handle everything.
Challenges include distinguishing files from directories, converting strings to temporary files, downloading HTTP resources, compressing multiple files, and writing the result to the HTTP response.
Idea
Discuss design ideas and pitfalls encountered.
Basics
The library is based on reactive programming but not fully reactive; it uses Mono<InputStream> as a core type.
Supporting both WebMvc and WebFlux required converting traditional InputStream handling to reactive streams.
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(contextView -> Mono.just(contextView.get(ServerHttpRequest.class)));
}
public static Mono<ServerHttpResponse> getResponse() {
return Mono.deferContextual(contextView -> Mono.just(contextView.get(ServerHttpResponse.class)));
}
}WebMvc can obtain request/response via RequestContextHolder, while WebFlux requires method‑parameter injection.
public void classpath(ServerHttpResponse response) {
// implementation
}Using a WebFilter we can capture the response object, but the return type is Mono<ServerHttpResponse>.
Blocking with Mono.block() is prohibited in Netty‑based WebFlux.
Architecture
The overall architecture is illustrated below.
Typical download steps: obtain file paths or objects, compress them, and write the result to the response. For HTTP sources an extra download step is required before compression.
Download Context
A DownloadContext is used to share intermediate results across the pipeline; a DownloadContextFactory can create custom contexts.
Supported Types
All download objects are abstracted as Source (e.g., FileSource, HttpSource) and created via SourceFactory.
public interface SourceFactory extends OrderProvider {
boolean support(Object source, DownloadContext context);
Source create(Object source, DownloadContext context);
}Custom class support is achieved with annotations.
@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";
}
// other fields omitted
}Define @SourceModel on a class, @SourceObject on fields or methods to mark download data, and @SourceName to specify the file name.
Concurrent Loading
A SourceLoader interface allows custom loading logic (thread pool, coroutine, etc.).
public interface SourceLoader {
Mono<Source> load(Source source, DownloadContext context);
}Compression
Compression is abstracted by Compression and SourceCompressor, supporting in‑memory or file‑based compression and custom 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
The response is abstracted as DownloadResponse to work with both HttpServletResponse and ServerHttpResponse.
public class ReactiveDownloadResponse implements DownloadResponse {
private final ServerHttpResponse response;
private OutputStream os;
private Mono<Void> mono;
public ReactiveDownloadResponse(ServerHttpResponse response) {
this.response = response;
}
@Override
public Mono<Void> write(Consumer<OutputStream> consumer) {
if (os == null) {
mono = response.writeWith(Flux.create(fluxSink -> {
try {
os = new FluxSinkOutputStream(fluxSink, response);
consumer.accept(os);
} catch (Throwable e) {
fluxSink.error(e);
}
}));
} else {
consumer.accept(os);
}
return mono;
}
@SneakyThrows
@Override
public void flush() {
if (os != null) {
os.flush();
}
}
@AllArgsConstructor
public static class FluxSinkOutputStream extends OutputStream {
private FluxSink<DataBuffer> fluxSink;
private ServerHttpResponse response;
@Override
public void write(byte[] b) throws IOException {
writeSink(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
byte[] bytes = new byte[len];
System.arraycopy(b, off, bytes, 0, len);
writeSink(bytes);
}
@Override
public void write(int b) throws IOException {
writeSink((byte) b);
}
@Override
public void flush() {
fluxSink.complete();
}
public void writeSink(byte... bytes) {
DataBuffer buffer = response.bufferFactory().wrap(bytes);
fluxSink.next(buffer);
DataBufferUtils.release(buffer);
}
}
} DownloadWriterhandles the actual stream copy, supporting range requests, charset, length, and progress callbacks.
public interface DownloadWriter extends OrderProvider {
boolean support(Resource resource, Range range, DownloadContext context);
default void write(InputStream is, OutputStream os, Range range, Charset charset, Long length) {
write(is, os, range, charset, length, null);
}
void write(InputStream is, OutputStream os, Range range, Charset charset, Long length, Callback callback);
interface Callback {
void onWrite(long current, long increase);
}
}Events
To achieve flexible extensibility, the library uses an event system: DownloadEventPublisher publishes events and DownloadEventListener listens to them, compatible with Spring’s event mechanism.
Logging
Detailed logs are emitted for each pipeline step, progress updates (loading, compression, response writing), and timing, helping to discover bugs.
Other Pitfalls
In WebFlux the context destruction step was not executed because the response write returned Mono.empty(). The fix was to invoke the destroy logic in doAfterTerminate.
Conclusion
The library provides a powerful, extensible solution for complex download scenarios across WebMvc and WebFlux, though mastering reactive nuances still requires further study.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
