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.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);

	
	@RequestMapping(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);
			}
		};
	}
}