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

Posted

技术标签:

【中文标题】如何使用 Java REST 服务和数据流下载文件【英文标题】:How to download a file using a Java REST service and a data stream 【发布时间】:2015-06-25 02:13:53 【问题描述】:

我有 3 台机器:

    文件所在的服务器 运行 REST 服务的服务器(泽西岛) 客户端(浏览器)可以访问第二台服务器,但无法访问第一台服务器

如何直接(不将文件保存在第二台服务器上)将文件从第一台服务器下载到客户端计算机? 从第二台服务器我可以得到一个 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】:

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

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

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

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

因此,您将在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 中看到,它只是将数据从一个流写入另一个流。所以对于我们上面的实现,我们必须写两次。

我们怎样才能只写一次呢?简单地将InputStream 作为Response 返回

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

如果我们查看source code for 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 documentation 中找不到任何东西(只是因为它是一个较大的文档,我现在懒得扫描它:-),但在Jersey 1.x documentation 中,它显示ClientWebResource(相当于 2.x 中的 WebTarget)可以在线程之间共享。所以我猜Jersey 2.x 会是一样的。但您可能需要自己确认。

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

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


更新

我真是个笨蛋。虽然我和 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<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();

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 所做的那样,这是内存实现的来源。

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

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

【讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 你不是傻逼!【参考方案2】:

参考这个:

@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

【讨论】:

没有帮助!详细信息页面也缺少信息【参考方案3】:

在此处查看示例:Input and Output binary streams using 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
        
    

【讨论】:

while() output.write() 这会将字节读入内存还是直接写入输出流?

以上是关于如何使用 Java REST 服务和数据流下载文件的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Google Drive 上传和下载文件(使用 Rest Api v3)

如何使用 spring rest 服务/spring boot 下载 Excel

使用WireMock伪造REST服务

使用kbmmw 的REST 服务实现上传大文件

Java Rest Jersey:发布多种类型的数据(文件和JSON)

如何测试使用邮递员提供 .zip 文件的 REST API?