如何使用Java REST服务和数据流下载文件

我有3台机器:

  1. 文件所在的服务器
  2. 正在运行REST服务的服务器(Jersey)
  3. 客户端(浏览器)可访问第二台服务器但无法访问第一台服务器

如何直接(不保存第二台服务器上的文件)将文件从第一台服务器下载到客户机器?
从第二台服务器我可以得到一个ByteArrayOutputStream来从第一台服务器获取文件,我可以使用REST服务将此流进一步传递给客户端吗?

它会这样工作吗?

基本上我想要实现的是允许客户端使用第二台服务器上的REST服务从第一台服务器下载文件(因为没有从客户端到第一台服务器的直接访问)只使用数据流(所以没有数据接触文件第二服务器系统)。

我现在尝试使用EasyStream库:

final FTDClient client = FTDClient.getInstance(); try { final InputStreamFromOutputStream isOs = new InputStreamFromOutputStream() { @Override public String produce(final OutputStream dataSink) throws Exception { return client.downloadFile2(location, Integer.valueOf(spaceId), URLDecoder.decode(filePath, "UTF-8"), dataSink); } }; try { String fileName = filePath.substring(filePath.lastIndexOf("/") + 1); StreamingOutput output = new StreamingOutput() { @Override public void write(OutputStream outputStream) throws IOException, WebApplicationException { int length; byte[] buffer = new byte[1024]; while ((length = isOs.read(buffer)) != -1){ outputStream.write(buffer, 0, length); } outputStream.flush(); } }; return Response.ok(output, MediaType.APPLICATION_OCTET_STREAM) .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"" ) .build(); 

UPDATE2

所以现在使用自定义MessageBodyWriter的代码看起来很简单:

ByteArrayOutputStream baos = new ByteArrayOutputStream(2048); client.downloadFile(location,spaceId,filePath,baos); return Response.ok(baos).build();

但是在尝试使用大文件时遇到了相同的堆错误。

UPDATE3终于成功了! StreamingOutput成功了。

谢谢@peeskillet! 非常感谢 !

“我如何直接(不保存第二台服务器上的文件)将文件从第一台服务器下载到客户机器?”

只需使用Client API并从响应中获取InputStream

 Client client = ClientBuilder.newClient(); String url = "..."; final InputStream responseStream = client.target(url).request().get(InputStream.class); 

获取InputStream有两种风格。 你也可以使用

 Response response = client.target(url).request().get(); InputStream is = (InputStream)response.getEntity(); 

哪一个更有效,我不确定,但返回的InputStream是不同的类,所以如果你愿意,你可能想要研究一下。

从第二台服务器我可以得到一个ByteArrayOutputStream来从第一台服务器获取文件,我可以使用REST服务将此流进一步传递给客户端吗?

因此,您在@GradyGCooper提供的链接中看到的大多数答案似乎都支持使用StreamingOutput 。 示例实现可能是类似的

 final InputStream responseStream = client.target(url).request().get(InputStream.class); System.out.println(responseStream.getClass()); StreamingOutput output = new StreamingOutput() { @Override public void write(OutputStream out) throws IOException, WebApplicationException { int length; byte[] buffer = new byte[1024]; while((length = responseStream.read(buffer)) != -1) { out.write(buffer, 0, length); } out.flush(); responseStream.close(); } }; return Response.ok(output).header( "Content-Disposition", "attachment, filename=\"...\"").build(); 

但是,如果我们查看StreamingOutputProvider的源代码 ,您将在writeTo看到,它只是将数据从一个流写入另一个流。 因此,通过上面的实现,我们必须写两次。

我们怎么才能得到一个写? 简单地将InputStream作为Response返回

 final InputStream responseStream = client.target(url).request().get(InputStream.class); return Response.ok(responseStream).header( "Content-Disposition", "attachment, filename=\"...\"").build(); 

如果我们查看InputStreamProvider的源代码 ,它只是委托给ReadWriter.writeTo(in, out) ,这只是我们在StreamingOutput实现中做的事情。

  public static void writeTo(InputStream in, OutputStream out) throws IOException { int read; final byte[] data = new byte[BUFFER_SIZE]; while ((read = in.read(data)) != -1) { out.write(data, 0, read); } } 

旁白:

  • Client对象是昂贵的资源。 您可能希望重复使用同一Client进行请求。 您可以从客户端为每个请求提取WebTarget

     WebTarget target = client.target(url); InputStream is = target.request().get(InputStream.class); 

    我认为WebTarget甚至可以共享。 我在Jersey 2.x文档中找不到任何内容 (仅因为它是一个更大的文档,我现在懒得扫描它:-),但在Jersey 1.x文档中 ,它说的是ClientWebResource (相当于2.x中的WebTarget )可以在线程之间共享。 所以我猜测泽西岛2.x会是一样的。 但你可能想要自己确认一下。

  • 您不必使用Client API。 使用java.net包API可以轻松实现下载。 但由于你已经在使用Jersey,因此使用它的API并没有什么坏处

  • 以上是假设泽西岛2.x. 对于Jersey 1.x,一个简单的谷歌搜索应该会让你在使用API​​(或我上面链接的文档)时受到一致打击


UPDATE

我是个骗子。 虽然OP和我正在考虑将ByteArrayOutputStream转换为InputStream ,但我错过了最简单的解决方案,它只是为ByteArrayOutputStream编写MessageBodyWriter

 import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; @Provider public class OutputStreamWriter implements MessageBodyWriter { @Override public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return ByteArrayOutputStream.class == type; } @Override public long getSize(ByteArrayOutputStream t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } @Override public void writeTo(ByteArrayOutputStream t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { t.writeTo(entityStream); } } 

然后我们可以简单地在ByteArrayOutputStream中返回ByteArrayOutputStream

 return Response.ok(baos).build(); 

D’OH!

更新2

这是我用过的测试(

资源类

 @Path("test") public class TestResource { final String path = "some_150_mb_file"; @GET @Produces(MediaType.APPLICATION_OCTET_STREAM) public Response doTest() throws Exception { InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int len; byte[] buffer = new byte[4096]; while ((len = is.read(buffer, 0, buffer.length)) != -1) { baos.write(buffer, 0, len); } System.out.println("Server size: " + baos.size()); return Response.ok(baos).build(); } } 

客户测试

 public class Main { public static void main(String[] args) throws Exception { Client client = ClientBuilder.newClient(); String url = "http://localhost:8080/api/test"; Response response = client.target(url).request().get(); String location = "some_location"; FileOutputStream out = new FileOutputStream(location); InputStream is = (InputStream)response.getEntity(); int len = 0; byte[] buffer = new byte[4096]; while((len = is.read(buffer)) != -1) { out.write(buffer, 0, len); } out.flush(); out.close(); is.close(); } } 

更新3

因此,这个特定用例的最终解决方案是让OP简单地从StreamingOutputwrite方法传递OutputStream 。 似乎第三方API,需要OutputStream作为参数。

 StreamingOutput output = new StreamingOutput() { @Override public void write(OutputStream out) { thirdPartyApi.downloadFile(.., .., .., out); } } return Response.ok(output).build(); 

不确定,但似乎资源方法中的读/写,使用ByteArrayOutputStream`,在内存中实现了一些东西。

downloadFile方法接受OutputStream的要点是它可以将结果直接写入提供的OutputStream 。 例如FileOutputStream ,如果你把它写入文件,当下载进来时,它会直接传输到文件。

这并不意味着我们要保留对OutputStream的引用,就像你试图用baos做的那样,你正试图这样做,这就是内存实现的来源。

因此,通过有效的方式,我们将直接写入为我们提供的响应流。 在writeTo方法(在MessageBodyWriter )传递给它之前,实际上不会调用writeTo方法。

您可以更好地了解我编写的MessageBodyWriter 。 基本上在writeTo方法中,将ByteArrayOutputStream替换为StreamingOutput ,然后在方法内部调用streamingOutput.write(entityStream) 。 您可以在答案的前面部分看到我提供的链接,我链接到StreamingOutputProvider 。 这正是发生的事情

请参考:

 @RequestMapping(value="download", method=RequestMethod.GET) public void getDownload(HttpServletResponse response) { // Get your file stream from wherever. InputStream myStream = someClass.returnFile(); // Set the content type and attachment header. response.addHeader("Content-disposition", "attachment;filename=myfilename.txt"); response.setContentType("txt/plain"); // Copy the stream to the response's output stream. IOUtils.copy(myStream, response.getOutputStream()); response.flushBuffer(); } 

详细信息: https : //twilblog.github.io/java/spring/rest/file/stream/2015/08/14/return-a-file-stream-from-spring-rest.html

请参阅此处的示例: 使用JERSEY输入和输出二进制流?

伪代码就是这样的(在上面提到的post中还有一些其他类似的选项):

 @Path("file/") @GET @Produces({"application/pdf"}) public StreamingOutput getFileContent() throws Exception { public void write(OutputStream output) throws IOException, WebApplicationException { try { // // 1. Get Stream to file from first server // while() { output.write() } } catch (Exception e) { throw new WebApplicationException(e); } finally { // close input stream } } } 
Interesting Posts