invocar - java consume rest service json
Cómo descargar un archivo utilizando un servicio REST de Java y un flujo de datos (3)
"¿Cómo puedo directamente (sin guardar el archivo en el segundo servidor) descargar el archivo del primer servidor a la máquina del cliente?"
Simplemente use la API de Client
y obtenga InputStream
de la respuesta
Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);
Hay dos sabores para obtener el InputStream
. También puedes usar
Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();
No estoy seguro de cuál es más eficiente, pero los InputStream
devueltos son clases diferentes, por lo que es posible que desee examinar eso si lo desea.
Desde el segundo servidor, puedo obtener un ByteArrayOutputStream para obtener el archivo del primer servidor. ¿Puedo pasar esta secuencia más al cliente usando el servicio REST?
Así que la mayoría de las respuestas que verá en el enlace proporcionado por @GradyGCooper parecen favorecer el uso de StreamingOutput
. Una implementación de ejemplo podría ser algo como
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();
Pero si miramos el código fuente de StreamingOutputProvider , verá en el writeTo
, que simplemente escribe los datos de una secuencia a otra. Así que con nuestra implementación anterior, tenemos que escribir dos veces.
¿Cómo podemos conseguir solo una escritura? Simplemente devuelve el InputStream
como la Response
final InputStream responseStream = client.target(url).request().get(InputStream.class);
return Response.ok(responseStream).header(
"Content-Disposition", "attachment, filename=/".../"").build();
Si observamos el código fuente de InputStreamProvider , simplemente lo delega a ReadWriter.writeTo(in, out)
, que simplemente hace lo que hicimos anteriormente en la implementación de 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);
}
}
Aparte:
Client
objetos delClient
son recursos caros. Es posible que desee reutilizar el mismoClient
para su solicitud. Puede extraer unWebTarget
del cliente para cada solicitud.WebTarget target = client.target(url); InputStream is = target.request().get(InputStream.class);
Creo que el
WebTarget
puede incluso ser compartido. No puedo encontrar nada en la documentación de Jersey 2.x (solo porque es un documento más grande, y soy demasiado perezoso para escanearlo en este momento :-), pero en la documentación de Jersey 1.x , dice queClient
yWebResource
(que es equivalente aWebTarget
en 2.x) se pueden compartir entre subprocesos. Así que supongo que Jersey 2.x sería lo mismo. pero es posible que desee confirmar por sí mismo.No tienes que hacer uso de la API del
Client
. Una descarga se puede lograr fácilmente con las APIs del paquetejava.net
. Pero como ya estás usando Jersey, no está mal usar sus APILo anterior está asumiendo Jersey 2.x. Para Jersey 1.x, una simple búsqueda en Google debería darle un montón de resultados para trabajar con la API (o la documentación a la que he vinculado anteriormente)
ACTUALIZAR
Soy un dufus Mientras el OP y yo contemplamos formas de convertir un ByteArrayOutputStream
en un InputStream
, me perdí la solución más simple, que es simplemente escribir un MessageBodyWriter
para el 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);
}
}
Entonces simplemente podemos devolver el ByteArrayOutputStream
en la respuesta
return Response.ok(baos).build();
D''OH!
ACTUALIZACIÓN 2
Aquí están las pruebas que utilicé (
Clase de recursos
@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();
}
}
Prueba de cliente
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();
}
}
ACTUALIZACIÓN 3
Así que la solución final para este caso de uso en particular fue que el OP simplemente pasara el OutputStream
del método de write
StreamingOutput
. Parece que la API de terceros requiere un OutputStream
como argumento.
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
thirdPartyApi.downloadFile(.., .., .., out);
}
}
return Response.ok(output).build();
No es seguro, pero parece que la lectura / escritura dentro del método de recursos, utilizando ByteArrayOutputStream`, se dio cuenta de algo en la memoria.
El punto del método downloadFile
que acepta un OutputStream
es para que pueda escribir el resultado directamente en el OutputStream
proporcionado. Por ejemplo, un FileOutputStream
, si lo escribiste en un archivo, mientras se realiza la descarga, se enviará directamente al archivo.
No estamos destinados a que mantengamos una referencia a OutputStream
, como intentabas hacer con los baos, que intentabas hacer, que es donde entra en juego la realización de la memoria.
Así que con la forma en que funciona, estamos escribiendo directamente en el flujo de respuesta que se nos proporciona. El método de write
no se llama realmente hasta el método writeTo
(en el MessageBodyWriter
), donde se le pasa el OutputStream
.
Puedes obtener una imagen mejor viendo el MessageBodyWriter
que escribí. Básicamente, en el método writeTo
, reemplace ByteArrayOutputStream
con StreamingOutput
, luego, dentro del método, llame a streamingOutput.write(entityStream)
. Puede ver el enlace que proporcioné en la parte anterior de la respuesta, donde me vinculo al StreamingOutputProvider
. Esto es exactamente lo que sucede
Tengo 3 máquinas:
- servidor donde se encuentra el archivo
- servidor donde se ejecuta el servicio REST (Jersey)
- cliente (navegador) con acceso al segundo servidor pero sin acceso al primer servidor
¿Cómo puedo descargar directamente (sin guardar el archivo en el segundo servidor) el archivo del primer servidor a la máquina del cliente?
Desde el segundo servidor, puedo obtener un ByteArrayOutputStream para obtener el archivo del primer servidor. ¿Puedo pasar esta secuencia más al cliente usando el servicio REST?
¿Funcionará de esta manera?
Básicamente, lo que quiero lograr es permitir que el cliente descargue un archivo del primer servidor utilizando el servicio REST en el segundo servidor (ya que no hay acceso directo desde el cliente al primer servidor) utilizando solo flujos de datos (por lo tanto, no hay datos que toquen el archivo). sistema del 2do servidor).
Lo que intento ahora con la biblioteca 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();
Actualización2
Así que mi código ahora con el MessageBodyWriter personalizado parece simple:
ByteArrayOutputStream baos = new ByteArrayOutputStream (2048); client.downloadFile (ubicación, spaceId, filePath, baos); devuelve Response.ok (baos) .build ();
Pero me sale el mismo error de pila cuando intento con archivos grandes.
ACTUALIZACIÓN3 ¡ Finalmente logró hacerlo funcionar! StreamingOutput hizo el truco.
Gracias @peeskillet! Muchas gracias !
Consulte esto:
@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();
}
Detalles en: https://twilblog.github.io/java/spring/rest/file/stream/2015/08/14/return-a-file-stream-from-spring-rest.html
Vea el ejemplo aquí: ¿ Entradas binarias de entrada y salida usando JERSEY?
El pseudo código sería algo como esto (hay algunas otras opciones similares en la publicación mencionada anteriormente):
@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
}
}
}