Project Tree
demo-download-file-spring-mvc-webclient
├── pom.xml
├── Dockerfile
└── src
└── main
├── java
│ └── com
│ └── example
│ ├── controller
│ │ └── DownloadController.java
│ └── DemoDownloadFileSpringMVCApplication.java
└── resources
└── application.properties
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.7</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-download-file-spring-mvc-webclient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-download-file-spring-mvc-webclient</name>
<description>Demo project for Spring Boot</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
- config remote download url
spring.application.name=demo-download-file
remote-download-base-url=http://demo.example.com/download
DemoDownloadFileSpringMVCApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoDownloadFileSpringMVCApplication {
public static void main(String[] args) {
SpringApplication.run(DemoDownloadFileSpringMVCApplication.class, args);
}
}
DownloadController.java
package com.example.controller;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import reactor.core.publisher.Flux;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller
public class DownloadController {
static final Logger log= LoggerFactory.getLogger(DownloadController.class);
private final WebClient webClient;
@Value("${remote-download-base-url}")
private String baseUrl; // This can be overridden via application.properties + environment.
public DownloadController(WebClient.Builder builder) {
this.webClient = builder.build();
}
@GetMapping("/download-remote")
public ResponseEntity<StreamingResponseBody> downloadRemote(
@RequestParam("param1") String param1,
@RequestParam("param2") String param2) {
log.info("param1 = {}, param2 = {}", param1, param2);
String remoteUrl = baseUrl + "?param1=" + param1 +"¶m2="+ param2;
log.info("remoteUrl = {}", remoteUrl);
StreamingResponseBody body = outputStream -> {
// Obtain Flux<DataBuffer> from remote stream
Flux<DataBuffer> dataBufferFlux = webClient.get()
.uri(remoteUrl)
.retrieve()
.bodyToFlux(DataBuffer.class);
log.info("dataBufferFlux ok");
// Writing Flux to OutputStream (blocking MVC environment)
DataBufferUtils.write(dataBufferFlux, outputStream)
.doOnNext(DataBufferUtils::release)
.blockLast(); // MVC needs to block until the streaming is complete.
};
log.info("DataBufferUtils.write ok");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + fileName)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(body);
}
}
Dockerfile
optional - only for verify.
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app-data
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Build
mvn clean package
Run
export REMOTE_DOWNLOAD_BASE_URL="http://demo.server.local:58080/download"
java -jar target/app.jar
Test
Open Browser
http://localhost:8080/download-remote?param1=TEMP¶m2=TEST
NOTE:
Spring Boot Controller will download file from http://demo.server.local:58080/download?param1=TEMP¶m2=TEST
String remoteUrl = baseUrl + "?param1=" + param1 +"¶m2="+ param2;
To verify that the program does not download the entire file to memory, we create a Docker image and limit the Docker container's memory to 1GB during execution. We then test downloading a remote file up to 4GB (exceeding the 1GB allocated to the Docker container) to verify that the file is not downloaded entirely to memory.
Build Docker Image
docker build -t spring-download-remote .
Run Docker
docker run \
-it \
--rm \
--name download-remote \
-p 8080:8080 \
--memory="1g" \
--memory-swap="1g" \
-e REMOTE_DOWNLOAD_BASE_URL="http://demo.server.local:58080/download" \
spring-download-remote
Test
Open Browser:
http://localhost:8080/download-remote?param1=TEMP¶m2=TEST
In fact, the program will connect to this URL (http://demo.server.local:58080/download?param1=TEMP¶m2=TEST) to download the file.
Code:
MVC Controller Download remoteUrl file:
@Value("${remote-download-base-url}")
private String baseUrl; // This can be overridden via application.properties + environment.
@GetMapping("/download-remote")
public ResponseEntity<StreamingResponseBody> downloadRemote(
@RequestParam("param1") String param1,
@RequestParam("param2") String param2) {
...
String remoteUrl = baseUrl + "?param1=" + param1 +"¶m2="+ param2;
...
+--------+ +-------------------------+ +--------+
| Broser +-- Download File --> + Spring Boot Application + + +
| | + (MVC Controller) +------------+ + Remote +
+--------+ +------------------+ Web Client +-- Download File --> + Server +
+------------+ +--------+
Flux.