web-dev-qa-db-ja.com

Java RESTサービスとデータストリームを使用してファイルをダウンロードする方法

3台のマシンがあります。

  1. ファイルが置かれているサーバー
  2. RESTサービスが実行されているサーバー(ジャージー)
  3. 2番目のサーバーにはアクセスできるが、1番目のサーバーにはアクセスできないクライアント(ブラウザー)

(2番目のサーバーにファイルを保存せずに)直接1番目のサーバーからクライアントのマシンにファイルをダウンロードするにはどうすればよいですか?
2番目のサーバーからByteArrayOutputStreamを取得して1番目のサーバーからファイルを取得できますが、RESTサービスを使用してこのストリームをクライアントにさらに渡すことができますか?

このように動作しますか?

だから基本的に私が達成したいのは、データストリームのみを使用して、クライアントが第2サーバーのRESTサービスを使用して第1サーバーからファイルをダウンロードできるようにすることです(クライアントから第1サーバーへの直接アクセスがないため)そのため、2番目のサーバーのファイルシステムにデータが接触することはありません。

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

PDATE2

したがって、カスタムMessageBodyWriterを使用した私のコードは簡単に見えます。

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

しかし、大きなファイルを使用しようとすると、同じヒープエラーが発生します。

PDATEやっと機能するようになりました! StreamingOutputがトリックを行いました。

@peeskilletありがとう!どうもありがとう !

27
Alex

「(2番目のサーバーにファイルを保存せずに)1番目のサーバーからクライアントのマシンにファイルを直接ダウンロードするにはどうすればよいですか?」

Client APIを使用して、応答からInputStreamを取得するだけです

Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);

InputStreamを取得するには2つのフレーバーがあります。使用することもできます

Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();

どちらがより効率的ですか?よくわかりませんが、返されるInputStreamsは異なるクラスですので、気になったらそれを調べてください。

2番目のサーバーからByteArrayOutputStreamを取得して1番目のサーバーからファイルを取得できますが、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で、あるストリームから別のストリームにデータを書き込むだけであることがわかります。したがって、上記の実装では、2回書く必要があります。

書き込みを1つだけ取得するにはどうすればよいですか? 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オブジェクトは高価なリソースです。同じClientをリ​​クエストに再利用することもできます。リクエストごとにクライアントからWebTargetを抽出できます。

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

    WebTargetも共有できると思います。 Jersey 2.x documentation (これは大きなドキュメントであり、今はスキャンするのが面倒だからです:-)には何も見つかりませんが、 Jersey 1.xドキュメント 、スレッド間でClientおよびWebResource(2.xのWebTargetと同等)を共有できると記載されています。だから、Jersey 2.xも同じだと思います。ただし、自分で確認することもできます。

  • Client APIを使用する必要はありません。ダウンロードは、Java.netパッケージAPIを使用して簡単に実現できます。ただし、すでにJerseyを使用しているので、そのAPIを使用しても害はありません。

  • 上記はJersey 2.xを想定しています。 Jersey 1.xの場合、単純なGoogle検索では、API(または上記にリンクしたドキュメント)を操作することで多くのヒットが得られます。


更新

私はそのようなデュファです。 OPと私はByteArrayOutputStreamInputStreamに変える方法を考えていますが、私は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();

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がOutputStreamStreamingOutputメソッドからwriteを単に渡すことでした。引数としてOutputStreamが必要なサードパーティAPIのようです。

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メソッドで、ByteArrayOutputStreamStreamingOutputに置き換えてから、メソッド内でstreamingOutput.write(entityStream)を呼び出します。回答の前半で提供したリンクを見ることができます。ここで、StreamingOutputProviderにリンクしています。これはまさに起こることです

32
Paul Samsotha

これを参照してください:

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

1

ここの例を参照してください。 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
        }
    }
}
0
Grady G Cooper