Design and Implementation of a Reactive Download Library for Spring MVC and WebFlux
This article introduces a Spring‑based download library that simplifies file, HTTP, and custom object downloads by using annotations, reactive programming, and a flexible handler chain to support both WebMVC and WebFlux, enabling concurrent loading, compression, and response writing with minimal controller code.
Download functionality is a common requirement in many projects, but implementing it can become cumbersome; to address this, the author created a library that simplifies download implementation.
With a single annotation, developers can download arbitrary objects such as files, strings, HTTP resources, or collections of custom objects without handling the underlying details.
Example usage:
@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";
}A practical scenario described is exporting all device QR‑code images as a zip file, where each device stores an HTTP URL for its QR code image. The steps include fetching the device list, downloading each image, checking cache, performing concurrent downloads, compressing the files, and writing the zip to the HTTP response.
The desired API is extremely simple: return a list of Device objects, annotate the QR‑code URL field and a method that provides the file name, and let the library handle the rest.
@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";
}
// other properties omitted
}The library is built on reactive programming, using Mono<InputStream> to bridge WebMVC and WebFlux. Compatibility issues arise because WebMVC uses RequestContextHolder while WebFlux requires injection via method parameters.
@Download(source = "classpath:/download/README.txt")
@GetMapping("/classpath")
public void classpath(ServerHttpResponse response) { }To obtain the response object without extra parameters, a WebFilter stores the request and response in the reactive context:
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 architecture follows a handler‑chain model similar to Spring Cloud Gateway. Each step implements DownloadHandler and is linked via DownloadHandlerChain , allowing arbitrary composition of processing stages.
public interface DownloadHandler extends OrderProvider {
Mono
handle(DownloadContext context, DownloadHandlerChain chain);
}
public interface DownloadHandlerChain {
Mono
next(DownloadContext context);
}A DownloadContext carries data throughout the pipeline, with factories and initializers for customization.
Download sources are abstracted as Source objects. Implementations such as FileSource and HttpSource are created by matching factories:
public interface SourceFactory extends OrderProvider {
boolean support(Object source, DownloadContext context);
Source create(Object source, DownloadContext context);
}Annotations like @SourceObject and @SourceName mark fields or methods that provide the download data and file name, respectively, and reflection is used to retrieve these values.
@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"; }
}Concurrent loading of network resources (e.g., HTTP images) is handled by a SourceLoader , which can be customized to use thread pools, coroutines, or other strategies.
public interface SourceLoader {
Mono
load(Source source, DownloadContext context);
}Compression is abstracted via SourceCompressor . The default implementation is a ZIP compressor, but the interface allows custom compression 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 is unified through DownloadResponse , supporting both HttpServletResponse and ServerHttpResponse . The reactive implementation wraps a FluxSink as an OutputStream :
@Getter
public class ReactiveDownloadResponse implements DownloadResponse {
private final ServerHttpResponse response;
private OutputStream os;
private Mono
mono;
public ReactiveDownloadResponse(ServerHttpResponse response) { this.response = response; }
@Override
public Mono
write(Consumer
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;
}
@SneakyThrows
@Override
public void flush() { if (os != null) { os.flush(); } }
@AllArgsConstructor
public static class FluxSinkOutputStream extends OutputStream {
private FluxSink
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);
}
}
}Event publishing and listening are used to log each stage of the download process, including loading progress, compression progress, response writing progress, and total time spent.
Additional pitfalls were encountered, such as the download context’s destruction step not being called after a WebFlux response is written because the reactive chain ends with Mono.empty() . The solution was to move the destruction logic to a doAfterTerminate hook.
In conclusion, the library demonstrates advanced usage of reactive programming, annotation‑driven configuration, and extensible handler chains to provide a concise API for complex download scenarios in Spring applications.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.