31

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

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

UPDATE3 终于设法让它工作了!StreamingOutput 成功了。

谢谢@peeskillet!非常感谢 !

4

3 回答 3

38

“我怎样才能直接(不将文件保存在第二台服务器上)将文件从第一台服务器下载到客户端的机器上?”

只需使用ClientAPI 并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的 s 是不同的类,所以如果你愿意的话,你可能想研究一下。

从第二台服务器我可以得到一个 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(相当于WebTarget2.x 中的)可以在线程之间共享。所以我猜Jersey 2.x 会是一样的。但您可能需要自己确认。

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

  • 以上假设 Jersey 2.x。对于 Jersey 1.x,一个简单的 Google 搜索应该会为您提供大量使用 API(或我上面链接到的文档)的点击率


更新

我真是个笨蛋。虽然我和 OP 正在考虑将 aByteArrayOutputStream转换为 a 的方法InputStream,但我错过了最简单的解决方案,即简单地MessageBodyWriterByteArrayOutputStream

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 简单地传递OutputStreamfromStreamingOutputwrite方法。似乎是第三方 API,需要 aOutputStream作为参数。

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

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

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

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

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

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

于 2015-04-18T05:05:18.670 回答
0

请参阅此处的示例:使用 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
        }
    }
}
于 2015-04-18T04:11:19.270 回答
0

参考这个:

@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

于 2016-11-10T10:47:20.367 回答