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

2022-09-01 10:53:48

我有3台机器:

  1. 文件所在的服务器
  2. 运行 REST 服务的服务器(泽西岛)
  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();
    }
}

更新2

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

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

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

更新3终于设法让它工作!StreamingOutput做到了这一点。

谢谢@peeskillet!非常感谢!


答案 1

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

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

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

哪一个更有效率?我不确定,但是返回的s是不同的类,所以如果你愿意,你可能想研究一下。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

我们怎么能只写一次呢?简单返回作为InputStreamResponse

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对象是昂贵的资源。您可能希望对请求重用相同的内容。您可以为每个请求从客户端中提取 。ClientWebTarget

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

    我认为甚至可以分享。我在泽西岛2.x文档中找不到任何东西(只是因为它是一个更大的文档,我现在懒得扫描它:-),但在泽西岛1.x文档中,它说and(相当于2.x)可以在线程之间共享。所以我猜泽西岛2.x也是一样的。但你可能想自己确认。WebTargetClientWebResourceWebTarget

  • 您不必使用 API。使用包 API 可以轻松实现下载。但是,由于您已经在使用泽西岛,因此使用其API并没有什么坏处。Clientjava.net

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


更新

我真是个傻瓜。虽然OP和我正在考虑将a变成a的方法,但我错过了最简单的解决方案,那就是简单地编写一个ByteArrayOutputStreamInputStreamMessageBodyWriterByteArrayOutputStream

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);
    }
}

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

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

哦!

更新 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简单地传递来自 的方法。似乎第三方API,需要一个作为参数。OutputStreamStreamingOutputwriteOutputStream

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

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

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

这并不意味着我们要保留对 的引用,就像你试图用 ,这是记忆实现的用武之地。OutputStreambaos

因此,通过工作方式,我们直接写入为我们提供的响应流。该方法实际上不会被调用,直到该方法(在 中)将 传递给它。writewriteToMessageBodyWriterOutputStream

你可以得到一个更好的图片,看看我写的。基本上在方法中,用替换,然后在方法里面调用。您可以看到我在答案的前面部分提供的链接,我链接到.这正是所发生的事情MessageBodyWriterwriteToByteArrayOutputStreamStreamingOutputstreamingOutput.write(entityStream)StreamingOutputProvider


答案 2

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

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

@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
        }
    }
}