J'ai 3 machines:
- serveur où se trouve le fichier
- serveur sur lequel le service REST est en cours d'exécution (Jersey)
- client (navigateur) avec accès au 2e serveur mais pas d'accès au 1er serveur
Comment puis-je directement (sans enregistrer le fichier sur le 2ème serveur) télécharger le fichier du 1er serveur sur la machine du client?
À partir du 2e serveur, je peux obtenir un ByteArrayOutputStream pour obtenir le fichier du 1er serveur, puis-je transmettre ce flux au client en utilisant le service REST?
Cela fonctionnera de cette façon?
En gros, je veux permettre au client de télécharger un fichier du 1er serveur à l’aide du service REST sur le 2e serveur (car il n’ya pas d’accès direct du client au 1er serveur) en utilisant uniquement des flux de données données touchant le système de fichiers du 2e serveur).
Ce que j'essaie maintenant avec la bibliothèque 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();
UPDATE2
Donc, mon code maintenant avec le MessageBodyWriter personnalisé est simple:
ByteArrayOutputStream baos = new ByteArrayOutputStream (2048); Client.downloadFile (emplacement, spaceId, filePath, baos); Return Response.ok (baos) .build ();
Mais j'obtiens la même erreur de tas lorsque j'essaie avec des fichiers volumineux.
UPDATE3 Enfin réussi à le faire fonctionner! StreamingOutput a fait l'affaire.
Merci @peeskillet! Merci beaucoup !
"Comment puis-je directement (sans enregistrer le fichier sur le 2ème serveur) télécharger le fichier du 1er serveur sur la machine du client?"
Il suffit d’utiliser l’API Client
et d’obtenir la InputStream
à partir de la réponse.
Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);
Il existe deux types d’obtention de la variable InputStream
. Vous pouvez aussi utiliser
Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();
Lequel est le plus efficace? Je ne suis pas sûr, mais les InputStream
retournés sont des classes différentes, vous pouvez donc vous renseigner à ce sujet si vous le souhaitez.
À partir du 2e serveur, je peux obtenir un ByteArrayOutputStream pour obtenir le fichier du 1er serveur. Puis-je transmettre ce flux au client en utilisant le service REST?
Donc, la plupart des réponses que vous verrez dans le lien fourni par @GradyGCooper semblent favoriser l'utilisation de StreamingOutput
. Un exemple d'implémentation pourrait être quelque chose comme
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();
Mais si nous examinons le code source de StreamingOutputProvider , vous verrez dans la variable writeTo
qu’il écrit simplement les données d’un flux à l’autre. Donc, avec notre implémentation ci-dessus, nous devons écrire deux fois.
Comment pouvons-nous obtenir une seule écriture? Retour simple la InputStream
en tant que Response
final InputStream responseStream = client.target(url).request().get(InputStream.class);
return Response.ok(responseStream).header(
"Content-Disposition", "attachment, filename=\"...\"").build();
Si nous examinons le code source pour InputStreamProvider , il délègue simplement des délégués à ReadWriter.writeTo(in, out)
, qui fait simplement ce que nous avons fait ci-dessus dans l’implémentation 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);
}
}
(À part:} _
Les objets Client
sont des ressources coûteuses. Vous voudrez peut-être réutiliser la même Client
pour la demande. Vous pouvez extraire une WebTarget
du client pour chaque demande.
WebTarget target = client.target(url);
InputStream is = target.request().get(InputStream.class);
Je pense que la WebTarget
peut même être partagée. Je ne trouve rien dans Documentation Jersey 2.x (uniquement parce que c’est un document plus volumineux et que je suis trop paresseux pour le parcourir maintenant :-), mais dans le Jersey 1. x documentation , il est indiqué que les Client
et WebResource
(ce qui équivaut à WebTarget
dans 2.x) peuvent être partagés entre les threads. Donc, je suppose que Jersey 2.x serait le même. mais vous voudrez peut-être confirmer par vous-même.
Vous n'êtes pas obligé d'utiliser l'API Client
. Un téléchargement peut être facilement réalisé avec les API de package Java.net
. Mais comme vous utilisez déjà Jersey, utiliser ses API ne fait pas de mal
Ce qui précède suppose que Jersey 2.x. Pour Jersey 1.x, une simple recherche sur Google devrait vous donner un tas de résultats pour travailler avec l'API (ou la documentation que j'ai liée à ci-dessus)
Je suis un dufus. Alors que le PO et moi-même examinons les moyens de transformer une ByteArrayOutputStream
en une InputStream
, j’ai raté la solution la plus simple, qui consiste simplement à écrire une MessageBodyWriter
pour la ByteArrayOutputStream
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);
}
}
Ensuite, nous pouvons simplement renvoyer le ByteArrayOutputStream
dans la réponse
return Response.ok(baos).build();
D'OH!
Voici les tests que j'ai utilisés (
Classe de ressources
@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();
}
}
Test client
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();
}
}
La solution finale pour ce cas d'utilisation particulier consistait donc à ce que l'OP passe simplement la méthode OutputStream
à partir de la méthode StreamingOutput
's write
. Semble l’API tierce, requiert une OutputStream
en tant qu’argument.
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
thirdPartyApi.downloadFile(.., .., .., out);
}
}
return Response.ok(output).build();
Pas tout à fait sûr, mais il semble que la lecture/écriture au sein de la méthode de ressource, en utilisant ByteArrayOutputStream`, ait réalisé quelque chose en mémoire.
Le fait que la méthode downloadFile
accepte une OutputStream
a pour but d'écrire le résultat directement dans la variable OutputStream
fournie. Par exemple, un FileOutputStream
, si vous l'écriviez dans un fichier, alors que le téléchargement arrivait, il serait directement transmis au fichier.
Cela ne signifie pas que nous gardions une référence à OutputStream
, comme vous essayiez de le faire avec baos
, qui est le lieu où la réalisation de la mémoire entre en jeu.
Donc, avec la façon dont cela fonctionne, nous écrivons directement dans le flux de réponses fourni pour nous. La méthode write
n'est en fait pas appelée jusqu'à la méthode writeTo
(dans la MessageBodyWriter
), où la OutputStream
lui est transmise.
Vous pouvez obtenir une meilleure image en regardant la MessageBodyWriter
que j'ai écrite. En gros, dans la méthode writeTo
, remplacez la ByteArrayOutputStream
par StreamingOutput
, puis dans la méthode, appelez streamingOutput.write(entityStream)
. Vous pouvez voir le lien que j'ai fourni dans la partie précédente de la réponse, où je fais un lien vers StreamingOutputProvider
. C'est exactement ce qui se passe
Reportez ceci:
@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();
}
Détails sur: https://twilblog.github.io/Java/spring/rest/file/stream/2015/08/14/return-a-file-stream-from-spring-rest.html
Voir exemple ici: Flux binaires d'entrée et de sortie utilisant JERSEY?
Le pseudo-code ressemblerait à ceci (il y a quelques autres options similaires dans l'article mentionné ci-dessus):
@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
}
}
}