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

Ale*_*lex 27 java rest download jersey bytestream

我有3台机器:

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

如何直接(不保存第二台服务器上的文件)将文件从第一台服务器下载到客户机器?
从第二台服务器我可以得到一个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();
Run Code Online (Sandbox Code Playgroud)

UPDATE2

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

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

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

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

谢谢@peeskillet!非常感谢 !

Pau*_*tha 33

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

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

Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);
Run Code Online (Sandbox Code Playgroud)

有两种口味可以得到InputStream.你也可以使用

Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();
Run Code Online (Sandbox Code Playgroud)

哪一个效率更高?我不确定,但返回的InputStreams是不同的类,所以如果你愿意,你可能想要调查一下.

从第二台服务器我可以得到一个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();
Run Code Online (Sandbox Code Playgroud)

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

我们怎么才能得到一个写?简单的回归InputStream作为Response

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

如果我们查看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);
    }
}
Run Code Online (Sandbox Code Playgroud)

旁白:

  • Client对象是昂贵的资源.您可能希望Client对请求重用相同的内容.您可以WebTarget从客户端为每个请求提取a .

    WebTarget target = client.target(url);
    InputStream is = target.request().get(InputStream.class);
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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


UPDATE

我真是个骗子.虽然OP和我正在考虑如何把一个ByteArrayOutputStream一个InputStream的,我错过了简单的解决方案,这是简单地写MessageBodyWriter为ByteArrayOutputStream

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们可以简单地返回ByteArrayOutputStream响应

return Response.ok(baos).build();
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

客户测试

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新3

因此,对于这个特定的用例的最终解决方案是为OP简单地通过OutputStream从所述StreamingOutput的write方法.似乎是第三方API,需要一个OutputStream参数.

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

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

downloadFile接受a 的方法的要点OutputStream是它可以将结果直接写入OutputStream提供的.例如FileOutputStream,如果您将其写入文件,则在下载进入时,它将直接流式传输到该文件.

这并不意味着我们要继续引用OutputStream,就像你试图做的那样baos,这是内存实现的来源.

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

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