How to Zip Multiple Files for Download in Spring Boot/Java
Spring provides StreamingResponseBody interface for asynchronous request processing. This interface allows the application to write directly to the response OutputStream without blocking the Servlet container thread.
In this example tutorial, we will show you how to write code for zipping multiple large files for download via a REST API in Spring Boot.
Here is a complete code of a controller with a REST method for downloading large zipped files:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@RestController
@RequestMapping(path = "/api")
public class DownloadZipController {
private static final Logger logger = LoggerFactory.getLogger(DownloadFileExample.class);
@GetMapping(path = "/downloads/large-files/{sampleId}")
public ResponseEntity<StreamingResponseBody> downloadZip(HttpServletResponse response,
@PathVariable(name = "sampleId") String sampleId) {
logger.info("download request for sampleId = {}", sampleId);
// list of file paths for download
List<String> paths = Arrays.asList("/home/Videos/part1.mp4",
"/home/Videos/part2.mp4",
"/home/Videos/part3.mp4",
"/home/Videos/part4.pp4");
int BUFFER_SIZE = 1024;
StreamingResponseBody streamResponseBody = out -> {
final ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
ZipEntry zipEntry = null;
InputStream inputStream = null;
try {
for (String path : paths) {
File file = new File(path);
zipEntry = new ZipEntry(file.getName());
inputStream = new FileInputStream(file);
zipOutputStream.putNextEntry(zipEntry);
byte[] bytes = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(bytes)) >= 0) {
zipOutputStream.write(bytes, 0, length);
}
}
// set zip size in response
response.setContentLength((int) (zipEntry != null ? zipEntry.getSize() : 0));
} catch (IOException e) {
logger.error("Exception while reading and streaming data {} ", e);
} finally {
if (inputStream != null) {
inputStream.close();
}
if (zipOutputStream != null) {
zipOutputStream.close();
}
}
};
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=example.zip");
response.addHeader("Pragma", "no-cache");
response.addHeader("Expires", "0");
return ResponseEntity.ok(streamResponseBody);
}
}
When using StreamingResponseBody, it is highly recommended to explicitly configure the TaskExecutor used in Spring MVC for handling asynchronous requests.
Lets create AsyncConfiguration class and configure the TaskExecutor. The following is the complete code for configuring the TaskExecutor with request timeout of 3600000 milliseconds (60 minutes). The code also registers an interceptor which is called when there is a request timeout.
import java.util.concurrent.Callable;
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;
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor")
public AsyncTaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(15);
executor.setQueueCapacity(50);
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
@Bean
public WebMvcConfigurer webMvcConfigurerConfigurer(AsyncTaskExecutor taskExecutor,
CallableProcessingInterceptor callableProcessingInterceptor) {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(3600000).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 {
return super.handleTimeout(request, task);
}
};
}
}