Backend Development 17 min read

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.

Java Captain
Java Captain
Java Captain
Design and Implementation of a Reactive Download Library for Spring MVC and WebFlux

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.

backendJavaSpringReactiveWebFluxlibrarydownload
Java Captain
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.