Simplify File Downloads in Spring with a One‑Annotation Library

This article introduces a Spring‑based download library that lets developers add a single @Download annotation to any controller method to automatically handle file, HTTP resource, or text downloads, supporting concurrent loading, compression, and both WebMVC and WebFlux response models.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Simplify File Downloads in Spring with a One‑Annotation Library

Downloading files is a common yet often cumbersome feature in many projects. The author created a library that reduces the implementation to a single @Download annotation, allowing any object—file path, File instance, string, HTTP URL, or a custom class—to be downloaded without additional boilerplate.

Basic 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";
}

For a real‑world scenario—exporting QR‑code images of devices as a zip file—the library can generate the entire response with less than 200 lines of code.

@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"; }
}

Annotations on the model tell the framework which field provides the download source and how to name the file.

Design Overview

The library is built on reactive programming using Mono<InputStream> to support both Spring MVC and WebFlux. A WebFilter captures the current request and response, storing them in a ReactiveDownloadHolder that provides Mono<ServerHttpRequest> and Mono<ServerHttpResponse> without blocking.

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(c -> Mono.just(c.get(ServerHttpRequest.class)));
    }
    public static Mono<ServerHttpResponse> getResponse() {
        return Mono.deferContextual(c -> Mono.just(c.get(ServerHttpResponse.class)));
    }
}

The processing pipeline consists of DownloadHandler components linked by a DownloadHandlerChain. Each step can be added, reordered, or removed, enabling flexible composition such as source loading, compression, and response writing.

public interface DownloadHandler extends OrderProvider {
    Mono<Void> handle(DownloadContext context, DownloadHandlerChain chain);
}

public interface DownloadHandlerChain {
    Mono<Void> next(DownloadContext context);
}

A central DownloadContext carries intermediate data between handlers. SourceFactory implementations create concrete Source objects (e.g., FileSource, HttpSource) based on the input type.

public interface SourceFactory extends OrderProvider {
    boolean support(Object source, DownloadContext context);
    Source create(Object source, DownloadContext context);
}

Compression is abstracted by SourceCompressor, allowing custom algorithms; the default implementation uses ZIP. The response writer abstracts the differences between HttpServletResponse and ServerHttpResponse, converting an OutputStream into a reactive FluxSink when necessary.

public class ReactiveDownloadResponse implements DownloadResponse {
    private final ServerHttpResponse response;
    private OutputStream os;
    private Mono<Void> mono;
    // write method creates FluxSinkOutputStream when needed
}

class FluxSinkOutputStream extends OutputStream {
    private final FluxSink<DataBuffer> fluxSink;
    private final ServerHttpResponse response;
    @Override public void write(byte[] b) throws IOException { writeSink(b); }
    // other write overloads convert bytes to DataBuffer and emit
}

Event publishing via DownloadEventPublisher and listeners provides detailed logging of each stage—loading, compression, writing—and measures execution time, helping to surface bugs.

Additional challenges such as context cleanup after a reactive write were solved by invoking the destroyer in a doAfterTerminate hook.

Architecture Diagram

图片
图片

The overall flow for a multi‑file zip download is: (1) resolve sources (file paths, URLs, or custom objects), (2) optionally load remote resources concurrently, (3) compress the collection, and (4) write the compressed bytes to the HTTP response.

In summary, the library demonstrates a clean, annotation‑driven approach to handling complex download scenarios in Spring, leveraging reactive streams, modular handlers, and extensible factories to keep user code minimal while remaining highly configurable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

springreactiveAnnotationWebFluxFile CompressionDownload
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

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.