Design and Implementation of a Reactive Download Library for Spring MVC and WebFlux
This article introduces a Java library that simplifies file download functionality in Spring MVC and WebFlux by using a single @Download annotation, handling various source types, concurrent loading, compression, and response writing through a flexible, reactive handler chain architecture.
The article starts by pointing out that download functionality is common but often cumbersome, and introduces a tool library that makes implementing downloads much simpler with a single annotation.
Example usage shows how the @Download annotation can be placed on a controller method to download a classpath resource, a local file, or an HTTP URL:
@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';
}To illustrate a real scenario, the author describes a need to export QR‑code images of devices as a zip file, which would normally require many steps such as querying devices, downloading each image, checking cache, performing concurrent downloads, compressing, and writing the result to the HTTP response.
By annotating fields in a Device class with @SourceObject (the URL) and @SourceName (the desired file name), the library can automatically handle all those steps, reducing the controller method to a simple return of the device list:
@Download(filename = '二维码.zip')
@GetMapping('/download')
public List
download() {
return deviceService.all();
}
public class Device {
private String name;
@SourceObject
private String qrCodeUrl;
@SourceName
public String getQrCodeName() {
return name + '.png';
}
}The library is built on reactive programming, using Mono<InputStream> to bridge between traditional Servlet‑based MVC and the non‑blocking WebFlux model. A custom WebFilter captures the current request and response and stores them 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)));
}
}The core processing is organized as a chain of DownloadHandler objects, each implementing a single step (e.g., loading a source, compressing, writing the response). The chain can be extended or reordered freely.
public interface DownloadHandler extends OrderProvider {
Mono
handle(DownloadContext context, DownloadHandlerChain chain);
}
public interface DownloadHandlerChain {
Mono
next(DownloadContext context);
}A DownloadContext travels through the whole flow, carrying intermediate results such as the list of Source objects. Different source types (file, HTTP, custom object) are abstracted by the Source interface and created by corresponding SourceFactory implementations.
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 a SourceLoader , which can be customized (thread pool, coroutine, etc.) to improve performance.
public interface SourceLoader {
Mono
load(Source source, DownloadContext context);
}Compression is abstracted by SourceCompressor , allowing different formats (ZIP, custom) and supporting in‑memory or file‑based compression.
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 is unified through a DownloadResponse abstraction that works for both HttpServletResponse and ServerHttpResponse . In the reactive case, a FluxSinkOutputStream converts written byte[] data into DataBuffer objects for the Netty pipeline.
public class ReactiveDownloadResponse implements DownloadResponse {
private final ServerHttpResponse response;
private OutputStream os;
private Mono
mono;
// write method creates FluxSinkOutputStream when needed
}
public static class FluxSinkOutputStream extends OutputStream {
private final FluxSink
fluxSink;
private final ServerHttpResponse response;
@Override
public void write(byte[] b) throws IOException { writeSink(b); }
// other write overloads convert bytes to DataBuffer and call fluxSink.next()
}To make the system observable, an event‑based mechanism ( DownloadEventPublisher and DownloadEventListener ) is used, allowing logging of each step, progress updates for loading, compression, and response writing, as well as total time consumption.
The author also notes several pitfalls encountered, such as the need to manually trigger context destruction after the reactive response completes (using doAfterTerminate ) and the incompatibility of blocking calls like Mono.block() in a Netty thread.
In summary, the library provides a highly extensible, annotation‑driven solution for complex download scenarios in Spring applications, covering source abstraction, concurrent loading, flexible compression, unified response handling, and event‑driven logging.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.