流方式创建zip的JAX-RS RESTful文件下载API

JAX-RS(Java API for RESTful Web Services)是JavaEE标准的一部分,规范了RESTful WEB服务的编程接口。而其中一个大名鼎鼎的实现就是jersey了。这种标注式的开发方式极大地简化了RESTful API的创建过程。WEB请求/应答中的众多变量/参数仅通过标注就可以轻松截获和自动转换。

前几天,一个新的需求来了。要求用一个RESTful API实现小文件的打包下载,并且要做到zip包中的文件名和位置(entry)可以指定。也就是说zip包中的文件名不一定和源文件名相同。
一种实现方案是,拷贝文件到指定位置,并重命名,最后从外层文件夹打包。这种方案会创建临时文件和zip文件在磁盘上,在用户下载完文件之后还要做清理工作。用户什么时候下载完成呢,天知道,龟速的网速文件又很大下几天也说不定。因此这种方案逻辑虽然简单,但不可取。
另外一种实现就是使用流。源文件的内容如同流水一样流向一个输出流,输出流的目标就是网络IO。这种方式不会有任何临时磁盘文件,连在内存中创建临时缓冲都不需要,仅需完成之后关闭流。本文的主角ZipOutputStream就是这样的一个输出流。

ZipOutputStream的构造函数:
ZipOutputStream(OutputStream out);
对象out是真正的输出目标,可以是FileOutputStream,写入磁盘文件;可以是ByteArrayOutputStream,写入到内存;还可以是网络IO的输出流。
事实上out可以是任何的OutputStream实现类,甚至是ZipOutputStream。没错,我们甚至可以将ZipOutputStream嵌套起来用。

假设要创建一个zip包,里面有两个文件,2.zip和3.zip,2.zip中有文件utf-8.txt,3.zip中有文件gbk.txt。
不创建临时文件的流实现:

final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
FileOutputStream file = null;
ZipOutputStream zip1 = null;
ZipOutputStream zip2 = null;
ZipOutputStream zip3 = null;
try {
	file = new FileOutputStream("file.zip");
	zip1 = new ZipOutputStream(file);

	zip1.putNextEntry(new ZipEntry("2.zip"));
	zip2 = new ZipOutputStream(zip1);
	zip2.putNextEntry(new ZipEntry("utf-8.txt"));
	zip2.write(UTF8_BOM);
	zip2.write("これはUTF-8のテキストです。".getBytes("UTF-8"));
	zip2.closeEntry();
	zip2.finish();
	zip1.closeEntry();

	zip1.putNextEntry(new ZipEntry("3.zip"));
	zip3 = new ZipOutputStream(zip1);
	zip3.putNextEntry(new ZipEntry("gbk.txt"));
	zip3.write("这是一段GBK文本。".getBytes("GBK"));
	zip3.closeEntry();
	zip3.finish();
	zip1.closeEntry();

	zip1.finish();

	System.out.println("DONE!");
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
} finally {
	if (zip2 != null) {
		try {
			zip2.close();
		} catch (IOException e) {}
	}
	if (zip3 != null) {
		try {
			zip3.close();
		} catch (IOException e) {}
	}
	if (zip1 != null) {
		try {
			zip1.close();
		} catch (IOException e) {}
	}
}

流zip2和zip3的输出目标都是流zip1,zip1的输出目标是磁盘文件。
zip2和zip3的输出就是2.zip和3.zip的二进制内容,倘若直接保存到磁盘上,就是文件,如果交给zip1,zip1就会进行再一次压缩,不过那是zip1的内部事情。

因此ZipOutputStream的上游和下游都是二进制内容,我们不用关心内部的压缩逻辑,也无须在出入口进行流处理。
有了这个认识,应付各种打包需求是轻而易举的事情。

例如,一个来自磁盘文件,一个压根就不是文件而是程序中的一个字符串,将这两个打包在一起(忽略异常处理):

final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
FileOutputStream output = new FileOutputStream("demo.zip");
ZipOutputStream out = new ZipOutputStream(output);

out.putNextEntry(new ZipEntry("utf-8.txt"));
out.write(UTF8_BOM);
out.write("这是一段UTF-8文本".getBytes("UTF-8"));
out.closeEntry();

out.putNextEntry(new ZipEntry("image/fav.ico"));
int len = 0;
byte[] buffer = new byte[1024];
InputStream inputStream = ClassLoader.getResourceAsStream("fav.ico");
while ((len = inputStream.read(buffer)) > 0) {
	out.write(buffer, 0, len);
}
inputStream.close();
out.closeEntry();

out.flush();
out.finish();
out.close();

假如ZipOutputStream的输出目标是网络IO,那提供zip下载的RESTful API怎么写?

很多情况下,JAX-RS的Response直接用文件对象就能返回文件的二进制内容:

Response.ok(new File(...)).type("...").header("...").build();

但本文的情况,磁盘文件并不存在,有一种使用ByteArrayOutputStream的解决方法,这是先将内容写到内存,Response再从内存读取二进制内容。
如果zip文件很大,会明显增加内存的使用量

	@GET
	@Path("/get2")
	@Produces(MediaType.APPLICATION_OCTET_STREAM)
	public Response download2() {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ZipOutputStream zos = new ZipOutputStream(bos);

		try {
			zos.putNextEntry(new ZipEntry("utf-8.txt"));
			zos.write(UTF8_BOM);
			zos.write("这是一段UTF-8文本".getBytes("UTF-8"));
			zos.closeEntry();
			zos.flush();
			zos.finish();
			return Response.ok(bos.toByteArray(), "application/zip")
				.header("Content-Disposition", "attachment; filename=demo2.zip")
				.build();
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				zos.close();
			} catch (IOException e) {}
		}
	}

为了节省内存,跳过缓冲步骤,我们需要实现JAX-RS的StreamingOutput接口。
依据StreamingOutput的说明,当我们希望输出(Response的数据源头)是流时,可以使用它作为entity直接喂给Response。它是MessageBodyWriter的轻量化选择。

A type that may be used as a resource method return value or as the entity in a Response when the application wishes to stream the output. This is a lightweight alternative to a javax.ws.rs.ext.MessageBodyWriter.

StreamingOutput唯一的write方法的参数output就是我们寻找已久的输出目标,直接将ZipOutputStream的输出目标指向它即可

public interface StreamingOutput {
    /**
     * Called to write the message body. 
     * @param output the OutputStream to write to.
     * @throws java.io.IOException if an IO error is encountered
     * @throws javax.ws.rs.WebApplicationException if a specific 
     * HTTP error response needs to be produced. Only effective if thrown prior
     * to any bytes being written to output.
     */
    void write(OutputStream output) throws IOException, WebApplicationException; 
}

于是最终代码(略过了异常处理):

package kuyur.info.zipdemo.rest;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import kuyur.info.zipdemo.servlet.filter.AuthenticationFilter;

import com.sun.jersey.spi.container.ResourceFilters;

@ResourceFilters({ AuthenticationFilter.class})
@Path("/file")
public class ZipDemoService {
	private static final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
	private static final String FAV_ICO = "fav.ico";

	@GET
	@Path("/get")
	@Produces(MediaType.APPLICATION_OCTET_STREAM)
	public Response download() {
		StreamingOutput stream = new StreamingOutput() {
			@Override
			public void write(OutputStream output) throws IOException, WebApplicationException {
				ZipOutputStream out = new ZipOutputStream(output);

				out.putNextEntry(new ZipEntry("utf-8.txt"));
				out.write(UTF8_BOM);
				out.write("这是一段UTF-8文本".getBytes("UTF-8"));
				out.closeEntry();

				out.putNextEntry(new ZipEntry("image/fav.ico"));
				int len = 0;
				byte[] buffer = new byte[1024];
				InputStream inputStream = ZipDemoService.class.getResourceAsStream(FAV_ICO);
				while ((len = inputStream.read(buffer)) > 0) {
					out.write(buffer, 0, len);
				}
				inputStream.close();
				out.closeEntry();

				out.flush();
				out.finish();
				out.close();
			}
		};
		return Response.ok(stream, "application/zip")
			.header("Content-Disposition", "attachment; filename=demo.zip")
			.build();
	}
}

完整的工程文件:zipdemo,使用了maven解决包依赖,基本上可以作为RESTful Web Service的工程模板。

本文目前尚无任何评论.

发表评论

XHTML: 您可以使用这些标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
:wink: :-| :-x :twisted: :) 8-O :( :roll: :-P :oops: :-o :mrgreen: :lol: :idea: :-D :evil: :cry: 8) :arrow: :-? :?: :!: