Design and Implementation of a Reactive Download Library for Spring MVC/WebFlux
This article introduces a Java library that simplifies downloading various resources—files, HTTP URLs, or custom objects—by using a single @Download annotation, reactive programming, customizable handlers, source factories, concurrent loading, compression, and event‑driven logging to streamline complex download workflows in Spring MVC and WebFlux applications.
The author presents a download library that aims to reduce boilerplate code for common download scenarios in Java projects, allowing a single @Download annotation to handle arbitrary objects such as file paths, HTTP URLs, strings, or custom class instances.
@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‑world need, the author describes a platform that manages devices, each with a QR‑code image URL, and the requirement to export all QR‑codes as a zip file named after each device.
@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 (Mono) to support both Spring MVC and WebFlux, using AOP to capture the response object without exposing unnecessary parameters.
@Override
public Mono
write(Consumer
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;
}A filter (ReactiveDownloadFilter) stores the current request and response in a context that can be accessed later 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 architecture separates the download process into handlers forming a chain, each operating on a shared DownloadContext that carries intermediate results.
public interface DownloadHandler extends OrderProvider {
Mono
handle(DownloadContext context, DownloadHandlerChain chain);
}
public interface DownloadHandlerChain {
Mono
next(DownloadContext context);
}Sources are abstracted via a Source interface; concrete factories (FileSourceFactory, HttpSourceFactory, etc.) create appropriate Source objects based on the input type.
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 (e.g., thread‑pool or coroutine based) to improve performance.
public interface SourceLoader {
Mono
load(Source source, DownloadContext context);
}Compression is performed by a SourceCompressor; the default implementation is ZipSourceCompressor, but the design allows custom compressors.
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 abstracts both HttpServletResponse and ServerHttpResponse via DownloadResponse, enabling byte‑stream writing in a unified way.
public class ReactiveDownloadResponse implements DownloadResponse {
private final ServerHttpResponse response;
private OutputStream os;
private Mono
mono;
// write method implementation shown earlier
}Progress and logging are achieved through an event system (DownloadEventPublisher / DownloadEventListener) that records each step, loading progress, compression progress, and total time, helping to discover bugs.
Several pitfalls are discussed, such as the need to trigger context destruction after response writing because Mono.empty() prevents further chain execution, which is solved by invoking the destroyer in doAfterTerminate.
In conclusion, the library provides a flexible, annotation‑driven, reactive download solution for Spring applications, covering source abstraction, concurrent loading, compression, response handling, and extensible event‑based logging.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.