Streaming Data with Spring Boot RESTful Web Service
Streaming data is a radical new approach to sending data to web browsers which provides for dramatically faster page load times. Quite often, we need to allo...
Join the DZone community and get the full member experience.
Join For FreeStreaming data is a radical new approach to sending data to web browsers which provides for dramatically faster page load times. Quite often, we need to allow users to download files in web applications. When the data is too large, it becomes quite a challenge to provide a good user experience.
Spring offers support for asynchronous request processing via StreamingResponseBody. In this approach, an application can write data directly to the response OutputStream without holding up the Servlet container thread. There are a few other methods in Spring to handle asynchronous request processing.
In this article, we are going to look at an example to download files using StreamingResponseBody. In this approach, data is processed and written in chunks to the OutputStream.
Setting Up Spring Boot Project
Create a sample Spring Boot application. Here is my sample project structure. I have created the project manually, but you could also create using Spring Intializer.
Let us add some basic dependencies to Maven POM.
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.techshard.streamingresponse</groupId>
<artifactId>springboot-download</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</project>
We will now create a controller and add an API endpoint for download. Here is my complete controller.
package com.techshard.download.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@RestController
@RequestMapping ("/api")
public class DownloadController {
private final Logger logger = LoggerFactory.getLogger(DownloadController.class);
@GetMapping (value = "/download", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<StreamingResponseBody> download(final HttpServletResponse response) {
response.setContentType("application/zip");
response.setHeader(
"Content-Disposition",
"attachment;filename=sample.zip");
StreamingResponseBody stream = out -> {
final String home = System.getProperty("user.home");
final File directory = new File(home + File.separator + "Documents" + File.separator + "sample");
final ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream());
if(directory.exists() && directory.isDirectory()) {
try {
for (final File file : directory.listFiles()) {
final InputStream inputStream=new FileInputStream(file);
final ZipEntry zipEntry=new ZipEntry(file.getName());
zipOut.putNextEntry(zipEntry);
byte[] bytes=new byte[1024];
int length;
while ((length=inputStream.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
inputStream.close();
}
zipOut.close();
} catch (final IOException e) {
logger.error("Exception while reading and streaming data {} ", e);
}
}
};
logger.info("steaming response {} ", stream);
return new ResponseEntity(stream, HttpStatus.OK);
}
}
In this API endpoint, we are reading multiple files from a directory and creating a zip file. We are executing this process within StreamingResponseBody
. It writes data directly to an OutputStream
before passing that written information back to the client using aResponseEntity
. This means that the download process will start immediately on the client, while the server is processing and writing data in chunks.
Start the server and test this endpoint using http://localhost:8080/api/download.
When using StreamingResponseBody
, it is highly recommended to configure TaskExecutor
used in Spring MVC for executing asynchronous requests. TaskExecutor
is an interface that abstracts the execution of a Runnable.
Let us configure the TaskExecutor
. Here is the AsyncConfiguration
class which configures timeout using WebMvcCofigurer
and also registers an interceptor that is called when there's a timeout in case you need some special handling.
package com.techshard.download;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.TimeoutCallableProcessingInterceptor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.Callable;
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {
private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class);
@Override
@Bean (name = "taskExecutor")
public AsyncTaskExecutor getAsyncExecutor() {
log.debug("Creating Async Task Executor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
/** Configure async support for Spring MVC. */
@Bean
public WebMvcConfigurer webMvcConfigurerConfigurer(AsyncTaskExecutor taskExecutor, CallableProcessingInterceptor callableProcessingInterceptor) {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(360000).setTaskExecutor(taskExecutor);
configurer.registerCallableInterceptors(callableProcessingInterceptor);
WebMvcConfigurer.super.configureAsyncSupport(configurer);
}
};
}
@Bean
public CallableProcessingInterceptor callableProcessingInterceptor() {
return new TimeoutCallableProcessingInterceptor() {
@Override
public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws Exception {
log.error("timeout!");
return super.handleTimeout(request, task);
}
};
}
}
Conclusion
Using StreamingResponseBody
, we can now stream data easily for highly-concurrent applications. I hope you enjoyed this article. Let me know if you have any comments or suggestion in the comments section below.
The example for this article can be found on GitHub repository.
Published at DZone with permission of Swathi Prasad, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments