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

How to download a file using a Java REST service and a data stream

I have 3 machines:

  1. server where the file is located
  2. server where REST service is running ( Jersey)
  3. client(browser) with access to 2nd server but no access to 1st server

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

会这样吗?

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

我现在尝试使用 EasyStream 库:

final FTDClient client = FTDClient.getInstance();

try {
    final InputStreamFromOutputStream <String> isOs = new InputStreamFromOutputStream <String>() {
        @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();
    }
}

更新2

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

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

但是我在尝试处理大文件时遇到同样的堆错误。

更新3 终于设法让它工作了! StreamingOutput 成功了。

谢谢@peeskillet!非常感谢!

参见此处示例:Input and Output binary streams using 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(<read stream from first server>) {
              output.write(<bytes read from first server>)
          }
        } catch (Exception e) {
            throw new WebApplicationException(e);
        } finally {
              // close input stream
        }
    }
}

"How can I directly (without saving the file on 2nd server) download the file from 1st server to client's machine?"

只需使用 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();

哪个效率更高?我不确定,但是 returned InputStreams 是不同的 classes,所以如果你愿意的话,你可能想调查一下。

From 2nd server I can get a ByteArrayOutputStream to get the file from 1st server, can I pass this stream further to the client using the REST service?

所以您在 link provided by @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();

但是如果我们查看 source code for StreamingOutputProvider,您会在 writeTo 中看到它只是将数据从一个流写入另一个流。所以对于我们上面的实现,我们必须写两次。

如何才能只写一次?简单 return InputStreamResponse

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

如果我们看一下 source code for InputStreamProvider, it simply delegates to 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 documentation (only because it is a larger document, and I'm too lazy to scan through it right now :-), but in the Jersey 1.x documentation 中找不到任何内容,它说 ClientWebResource(相当于 2.x 中的 WebTarget)可以在线程之间共享.所以我猜 Jersey 2.x 会是一样的。但你可能想自己确认一下。

  • 您不必使用 Client API。使用 java.net 包 APIs 可以轻松下载。但由于您已经在使用 Jersey,使用它的 APIs

  • 也没什么坏处
  • 以上是假设泽西岛2.x。对于 Jersey 1.x,一个简单的 Google 搜索应该可以为您提供大量关于 API(或我 link 编辑到上面的文档)


更新

我真是个笨蛋。当 OP 和我正在考虑将 ByteArrayOutputStream 转换为 InputStream 的方法时,我错过了最简单的解决方案,即简单地为 ByteArrayOutputStream[= 编写 MessageBodyWriter 71=]

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<ByteArrayOutputStream> {

    @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<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        t.writeTo(entityStream);
    }
}

然后我们可以简单地return响应中的ByteArrayOutputStream

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

天啊!

更新 2

这是我使用的测试(

资源class

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

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

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

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

因此,通过有效的方式,我们直接写入为我们提供的响应流。方法 write 直到 writeTo 方法(在 MessageBodyWriter 中)才真正被调用,其中 OutputStream 被传递给它。

你可以看看我写的 MessageBodyWriter 以获得更好的图片。基本上在 writeTo 方法中,将 ByteArrayOutputStream 替换为 StreamingOutput,然后在方法内部调用 streamingOutput.write(entityStream)。你可以看到我在前面的答案中提供的 link,其中我 link 到 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