流方式创建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的工程模板。
评论