it-swarm.com.de

So laden Sie eine Datei mit Java herunter REST Dienst und einen Datenstrom

Ich habe 3 Maschinen:

  1. server, auf dem sich die Datei befindet
  2. server, auf dem der REST -Dienst ausgeführt wird (Jersey) 
  3. client (Browser) mit Zugriff auf den 2. Server, aber keinen Zugriff auf den 1. Server

Wie kann ich die Datei direkt (ohne Speichern der Datei auf dem 2. Server) vom 1. Server auf den Client-Computer herunterladen? 
Vom 2. Server kann ich einen ByteArrayOutputStream erhalten, um die Datei vom 1. Server abzurufen. Kann ich diesen Stream mit dem Dienst REST an den Client weiterleiten?

Es wird so funktionieren?

Im Grunde möchte ich dem Client das Herunterladen einer Datei vom 1. Server über den Dienst REST auf dem 2. Server ermöglichen (da es keinen direkten Zugriff vom Client auf den 1. Server gibt), wobei nur Datenströme verwendet werden (also nein Daten, die das Dateisystem des 2. Servers berühren).

Was ich jetzt mit der EasyStream-Bibliothek probiere:

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

UPDATE2

Mein Code jetzt mit dem benutzerdefinierten MessageBodyWriter sieht also einfach aus:

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

Ich bekomme jedoch den gleichen Heap-Fehler, wenn ich mit großen Dateien versuche.

UPDATE3 Endlich hat es geschafft, dass es funktioniert! !__. StreamingOutput hat den Trick geleistet.

Vielen Dank @peeskillet! Danke vielmals !

25
Alex

"Wie kann ich direkt (ohne Speichern der Datei auf dem 2. Server) die Datei vom 1. Server auf den Client-Computer herunterladen?"

Verwenden Sie einfach die Client -API und rufen Sie die InputStream aus der Antwort ab

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

Es gibt zwei Varianten, um die InputStream zu erhalten. Sie können auch verwenden

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

Welches ist das effizientere? Ich bin mir nicht sicher, aber die zurückgegebenen InputStreams sind verschiedene Klassen.

Vom 2. Server kann ich einen ByteArrayOutputStream abrufen, um die Datei vom 1. Server abzurufen. Kann ich diesen Stream mithilfe des REST Service an den Client weiterleiten?

Die meisten Antworten, die Sie im Link von @GradyGCooper finden, scheinen die Verwendung von StreamingOutput zu bevorzugen. Eine Beispielimplementierung könnte so ähnlich sein

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

Wenn wir uns jedoch den Quellcode für StreamingOutputProvider ansehen, werden Sie in writeTo feststellen, dass die Daten einfach von einem Stream in einen anderen geschrieben werden. Bei unserer obigen Implementierung müssen wir also zweimal schreiben.

Wie können wir nur ein Schreiben bekommen? Geben Sie einfach das InputStream als Response zurück.

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

Wenn wir uns den Quellcode für InputStreamProvider ansehen, delegiert er einfach an ReadWriter.writeTo(in, out) , der einfach das tut, was wir getan haben oben in der StreamingOutput Implementierung

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

Nebenbei:

  • Client Objekte sind teure Ressourcen. Möglicherweise möchten Sie das gleiche Client für die Anforderung wiederverwenden. Sie können für jede Anforderung ein WebTarget aus dem Client extrahieren.

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

    Ich denke, das WebTarget kann sogar geteilt werden. Ich kann in der Jersey 2.x-Dokumentation nichts finden (nur, weil es sich um ein größeres Dokument handelt, und ich bin zu faul, um es jetzt zu durchsuchen :-), aber In der Jersey 1.x-Dokumentation heißt es, dass Client und WebResource (was WebTarget in 2.x entspricht) sein können zwischen Threads geteilt. Ich vermute also, dass Jersey 2.x dasselbe wäre. aber Sie möchten vielleicht selbst bestätigen.

  • Sie müssen die Client -API nicht verwenden. Ein Download kann leicht mit den Java.net - Paket-APIs erreicht werden. Aber da Sie Jersey bereits verwenden, schadet es nicht, seine APIs zu verwenden

  • Das Obige setzt Jersey 2.x voraus. Für Jersey 1.x sollte eine einfache Google-Suche eine Reihe von Treffern für die Arbeit mit der API (oder der Dokumentation, auf die ich oben verlinkt habe) liefern.


AKTUALISIEREN

Ich bin so ein Dufus. Während das OP und ich darüber nachdenken, wie wir ByteArrayOutputStream zu InputStream machen können, habe ich die einfachste Lösung verpasst, nämlich einfach MessageBodyWriter für ByteArrayOutputStream zu schreiben.

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

Dann können wir einfach das ByteArrayOutputStream in der Antwort zurückgeben

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

D'OH!

UPDATE 2

Hier sind die Tests, die ich verwendet habe (

Ressourcenklasse

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

Client-Test

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

UPDATE 3

Die endgültige Lösung für diesen speziellen Anwendungsfall bestand darin, dass das OP einfach die OutputStream aus der StreamingOutput -Methode des write übergibt. Scheint, dass die Drittanbieter-API ein OutputStream als Argument benötigt.

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

Nicht ganz sicher, aber das Lesen/Schreiben innerhalb der Ressourcenmethode mit ByteArrayOutputStream` scheint etwas im Speicher zu realisieren.

Der Sinn der downloadFile -Methode, die ein OutputStream akzeptiert, besteht darin, das Ergebnis direkt in das bereitgestellte OutputStream zu schreiben. Zum Beispiel ein FileOutputStream, wenn Sie es in eine Datei geschrieben haben, während der Download eingeht, wird es direkt in die Datei gestreamt.

Es ist nicht beabsichtigt, dass wir einen Verweis auf OutputStream behalten, wie Sie es mit baos versucht haben. Hier kommt die Erinnerungsrealisierung ins Spiel.

So wie es funktioniert, schreiben wir direkt an den für uns bereitgestellten Antwort-Stream. Die Methode write wird erst dann aufgerufen, wenn die writeTo -Methode (in der MessageBodyWriter), an die die OutputStream übergeben wird.

Sie können sich ein besseres Bild machen, wenn Sie das MessageBodyWriter anschauen, das ich geschrieben habe. Ersetzen Sie in der writeTo -Methode ByteArrayOutputStream durch StreamingOutput, und rufen Sie dann innerhalb der Methode streamingOutput.write(entityStream) auf. Sie können den Link sehen, den ich im vorherigen Teil der Antwort angegeben habe, wo ich auf StreamingOutputProvider verweise. Genau das passiert

32
Paul Samsotha

Verweisen Sie dies:

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

Details unter: https://twilblog.github.io/Java/spring/rest/file/stream/2015/08/14/return-a-file-stream-from-spring-rest.html

1

Siehe Beispiel hier: Eingabe und Ausgabe von Binärströmen mit JERSEY?

Pseudo-Code wäre ungefähr so ​​(es gibt einige andere ähnliche Optionen in dem oben genannten Beitrag):

@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