Ich habe 3 Maschinen:
- server, auf dem sich die Datei befindet
- server, auf dem der REST -Dienst ausgeführt wird (Jersey)
- 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 !
"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 InputStream
s 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.
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!
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();
}
}
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
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
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
}
}
}