Java Servlet Generate Zip File Example
In this article we illustrate how to generate a zip (compressed) file via a GET
request to a custom servlet.
The servlet will serve said file (configurable via a WebInitParam
) to the browser and the browser will then prompt the user with a download prompt to save said file to the file system.
The file will be served asynchronously using FileChannels transferTo(...)
.
1. Introduction
To generate a zip file from a servlet we will be using a combination of FileChannel, ZipOuputStream, ZipEntry and asynchronous processing from within a WebServlet
.
The servlet will be configured with various meta-data specifying the timeout for the asynchronous processing, the file to serve, and a Runnable implementation that handles the actual asynchronous processing of reading and writing the file to the ZipOutputStream.
The program will be run from the command line using maven, specifically utilizing the maven cargo plugin to automatically and seamlessly deploy our code and run it without the need for explicitly installing and setting up a servlet 3.1 compliant container.
2. Technologies used
The example code in this article was built and run using:
- Java 8
- Maven 3.3.9
- STS (3.9.0.RELEASE)
- Ubuntu 16.04 (Windows, Mac or Linux will do fine)
3. Setup
To ensure that is installed the correct version of Maven and Java you can execute the following:
Confirming Setup
jean-jay@jeanjay-SATELLITE-L750D:~$ java -version java version "1.8.0_101" Java(TM) SE Runtime Environment (build 1.8.0_101-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode) jean-jay@jeanjay-SATELLITE-L750D:~$ javac -version javac 1.8.0_101 jean-jay@jeanjay-SATELLITE-L750D:~$ mvn -version Apache Maven 3.3.9 Maven home: /usr/share/maven Java version: 1.8.0_101, vendor: Oracle Corporation Java home: /home/jean-jay/runtimes/jdk1.8.0_101/jre Default locale: en_ZA, platform encoding: UTF-8 OS name: "linux", version: "4.10.0-37-generic", arch: "amd64", family: "unix" jean-jay@jeanjay-SATELLITE-L750D:~$
4. Maven Cargo Plugin
Cargo is a wrapper that allows us to do programmatic manipulation of Containers, in our case servlet containers, in a standardized way.
The maven cargo plugin allows us to easily, and as part of the maven build process, deploy and run our application from the command line.
Below follows our maven cargo plugin configuration: (Using version 1.6.4)
Maven Cargo Plugin Configuration
<plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <configuration> <container> <containerId>tomcat8x</containerId> <artifactInstaller> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat</artifactId> <version>${tomcat.version}</version> </artifactInstaller> </container> <configuration> <type>standalone</type> <home> ${project.build.directory}/apache-tomcat-${tomcat.version} </home> <properties> <cargo.servlet.port>8080</cargo.servlet.port> <cargo.logging>high</cargo.logging> </properties> </configuration> <deployables> <deployable> <groupId>${project.groupId}</groupId> <artifactId>${project.artifactId}</artifactId> <type>war</type> <properties> <context>/sample</context> </properties> </deployable> </deployables> </configuration> </plugin>
- lines 7-11: uses maven to find and download the relevant version of Tomcat (8.x) we want
- line 16: configure our container to be a standalone instance and place it in a specific directory
- lines 24-31: we specify the artifact to deploy, type of packaging and the context route
5. Channels
It wasn’t strictly necessary to use Channels, we could have just as easily handled reading from the input File and writing to the ZipOutputStream manually.
Channels, however, abstract away some of the ugliness of doing said tasks as well as provide us with some potential performance optimizations if we leverage the transferTo(...)
method of FileChannel.
Below follows the snippet of code that handles the creation of the input and output FileChannels and the transferring of bytes to and from.
FileChannel usage
... WritableByteChannel outputChannel = Channels.newChannel(zipStream); FileChannel inputChannel = FileChannel.open(this.srcPath, StandardOpenOption.READ); ... private void transferContents(final FileChannel inputChannel, final WritableByteChannel outputChannel) throws IOException { long size = inputChannel.size(); long position = 0; while (position < size) { position += inputChannel.transferTo(position, MAX_BYTE_COUNT, outputChannel); } } ...
- line 2: we create a WritableByteChannel by wrapping the ZipOutputStream, this will be the Channel to which we write the File contents
- line 3: we create an input FileChannel to the input File
- lines 8-10: in a loop we transfer the contents from one Channel to the other. The
MAX_BYE_COUNT
is the maximum number of bytes we will transfer at any given loop instance (determined by some windows limitation which results in a slow copy should the bytes exceed this magic number)
6. Why Asynchronous
It is quite common for most IO operations to become the bottleneck of any system, therefore we have opted to handle the processing of the File asynchronously to better illustrate part of the idiomatic preference when dealing with similar use cases.
I say part, because we could also have instead used a WriteListener
, to efficiently handle dispatching the contents to the ZipOutputStream .
In the sample program we ensure our servlet can support asynchronous mode and upon request we immediately set the HttpServletRequest
into asynchronous mode.
This allows the request to be handled in a dedicated, alternate thread, from another thread pool, designed specifically for the purpose of serving the zip File. Ordinarily we would like to keep thread pools designed for high CPU activity separate from thread pools designed for IO hence us defining a thread pool specifically for serving our zip File.
Servlet showing asynchronous mode settings
@WebServlet(urlPatterns = "/zipfile", initParams = { @WebInitParam(name = "src", value = "/tmp/sample.mp4"), @WebInitParam(name = "timeout", value = "5000") }, asyncSupported = true) public final class ZipFileServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final String TIMEOUT_PARAM_KEY = "timeout"; private static final String SRC_PATH = "src"; @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { final AsyncContext asyncCtx = request.startAsync(); asyncCtx.setTimeout(Long.valueOf(getServletConfig().getInitParameter(TIMEOUT_PARAM_KEY))); getExecutorService(request).execute(new ZipFileHandler(response, getServletConfig().getInitParameter(SRC_PATH))); } private ExecutorService getExecutorService(final HttpServletRequest request) { assert !Objects.isNull(request); return (ExecutorService) request.getServletContext().getAttribute(ContextListenerExecutorServiceInitializer.THREAD_POOL_EXECUTOR); } ...
- line 1: we configure certain attributes for our servlet including, importantly, a timeout for asynchronous processing and a setting indicating our servlet supports asynchronous processing
- lines 10-11: we immediately set our
HttpServletRequest
into asynchronous mode - lines 16-19: we retrieve an
ExecutorService
from theServletContext
so that we can submit our File serving code
7. Required Headers
The following headers are used to indicate to the client (browser) the content type and the content disposition. The content type indicates application/zip
(self explanatory) but the content disposition begs some clarity.
Content disposition simply indicates to the browser how the content should be displayed, inline in the browser (web page content) or as an attachment (downloadable as a file).
Setting required headers
private static final String ZIP_CONTENT_TYPE = "application/zip"; private static final String CONTENT_DISPOSITION_KEY = "Content-Disposition"; private static final String CONTENT_DISPOSITION_VALUE = "attachment; filename=\"%s\""; this.response.setContentType(ZIP_CONTENT_TYPE); this.response.setHeader(CONTENT_DISPOSITION_KEY, String.format(CONTENT_DISPOSITION_VALUE, fileName + ".zip"));
- In the above snippet of code we illustrate the setting of required headers to ensure the browser will respect that fact that we want the user to be challenged as to where to save the incoming File.
8. Running the Program
The core of the File processing is done from within a Runnable which is submitted to the ExecutorService (thread pool) to be processed in a different thread.
Runnable for processing the File
private static final class ZipFileHandler implements Runnable { private static final long MAX_BYTE_COUNT = 67076096l; private static final String ZIP_CONTENT_TYPE = "application/zip"; private static final String CONTENT_DISPOSITION_KEY = "Content-Disposition"; private static final String CONTENT_DISPOSITION_VALUE = "attachment; filename=\"%s\""; private final HttpServletResponse response; private final Path srcPath; private ZipFileHandler(final HttpServletResponse response, final String srcPath) { assert !Objects.isNull(response) && !Objects.isNull(srcPath); this.response = response; this.srcPath = Paths.get(srcPath); } @Override public void run() { try (ZipOutputStream zipStream = new ZipOutputStream(this.response.getOutputStream()); WritableByteChannel outputChannel = Channels.newChannel(zipStream); FileChannel inputChannel = FileChannel.open(this.srcPath, StandardOpenOption.READ);) { final String fileName = this.srcPath.getFileName().toString(); initResponse(response, fileName); zipStream.putNextEntry(new ZipEntry(fileName)); transferContents(inputChannel, outputChannel); zipStream.flush(); this.response.getOutputStream().flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { this.response.getOutputStream().close(); } catch (IOException e) { e.printStackTrace(); } } } private void initResponse(final HttpServletResponse response, final String fileName) { assert !Objects.isNull(response) && !Objects.isNull(fileName) && !fileName.isEmpty(); this.response.setContentType(ZIP_CONTENT_TYPE); this.response.setHeader(CONTENT_DISPOSITION_KEY, String.format(CONTENT_DISPOSITION_VALUE, fileName + ".zip")); } private void transferContents(final FileChannel inputChannel, final WritableByteChannel outputChannel) throws IOException { long size = inputChannel.size(); long position = 0; while (position < size) { position += inputChannel.transferTo(position, MAX_BYTE_COUNT, outputChannel); } } }
- line 20: we create a ZipOutputStream which wraps the
HttpServletResponse
OutputStream - line 21: we create a WritableByteChannel which wraps the ZipOutputStream, this way we can work with Channels and also ensure anything written to the WritableByteChannel will get compressed if possible
- line 22: we create a FileChannel to ensure we can leverage the
transferTo(..)
optimizations between Channels whilst transferring the File from input to output - line 28: we add a ZipEntry for the File, as expected a zipped file can contain more than 1 File
Before building and running the program one can specify where to find a file to serve. This can be configured in the class ZipFileServlet
via the WebInitParam
src
. Simply specify a file path to serve and the file will be served zipped via the ZipFileServlet
.
After downloading the sample project and configuring a file to server, navigate to the project root folder and execute the following:
- build:
mvn clean install package
- run:
mvn cargo:run
Navigate to localhost:8080/sample/zipfile
and you will be prompted to save the zip file.
9. Summary
In this example we illustrated how to generate a zip file from a servlet in an asynchronous manner.
We also illustrated how to setup and run our application using maven and more specifically the maven cargo plugin which allowed us to run our application without the need for explicitly installing and setting up a servlet 3.1 compliant container.
10. Download the Source Code
This was a Java Servlet Generate Zip File Example.
You can download the full source code of this example here: Java Servlet Generate Zip File Example