Ответ 1
"Как я могу напрямую (без сохранения файла на 2-м сервере) загрузить файл с 1-го сервера на клиентскую машину?"
Просто используйте Client
API и получите InputStream
из ответа
Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);
Есть два варианта получения InputStream
. Вы также можете использовать
Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();
Какой из них является более эффективным? Я не уверен, но возвращаемый InputStream
- это разные классы, так что вы можете посмотреть на это, если хотите.
Со 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
, что он просто записывает данные из одного потока в другой. Так что с нашей реализацией выше, мы должны написать дважды.
Как мы можем получить только одну запись? Просто верните InputStream
как Response
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);
}
}
Asides:
-
Client
объекты - это дорогие ресурсы. Вы можете повторно использовать тот жеClient
для запроса. Вы можете извлечьWebTarget
из клиента для каждого запроса.WebTarget target = client.target(url); InputStream is = target.request().get(InputStream.class);
Я думаю, что
WebTarget
можно даже поделиться. Я ничего не могу найти в документации Jersey 2.x (только потому, что это документ большего размера, и мне лень сканировать его прямо сейчас :-), но в документации Jersey 1.x написаноClient
иWebResource
(который эквивалентенWebTarget
в 2.x) могут быть разделены между потоками. Так что я думаю, что Джерси 2.x будет таким же. но вы можете подтвердить это для себя. -
Вам не нужно использовать
Client
API. Загрузка может быть легко достигнута с помощью API пакетаjava.net
. Но поскольку вы уже используете Джерси, использование API-интерфейсов не повредит -
Выше предполагается, что Джерси 2.x. Для Jersey 1.x простой поиск в Google должен получить кучу хитов для работы с API (или документацию, на которую я ссылался выше)
ОБНОВИТЬ
Я такой придурок. В то время как OP и я обдумываем способы превращения ByteArrayOutputStream
в InputStream
, я упустил самое простое решение, которое заключается в простом написании MessageBodyWriter
для 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);
}
}
Тогда мы можем просто вернуть 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
Таким образом, окончательное решение для этого конкретного OutputStream
использования было для OP просто передать OutputStream
из метода write
StreamingOutput
. Кажется, сторонний API, требуется OutputStream
в качестве аргумента.
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
thirdPartyApi.downloadFile(.., .., .., out);
}
}
return Response.ok(output).build();
Не совсем уверен, но кажется, что чтение/запись в методе ресурса, используя ByteArrayOutputStream ', реализовали что-то в памяти.
OutputStream
метода downloadFile
принимающего OutputStream
заключается в том, что он может записать результат непосредственно в предоставленный OutputStream
. Например, FileOutputStream
, если вы записали его в файл, пока идет загрузка, он будет напрямую передан в файл.
Это не означает, что мы должны хранить ссылку на OutputStream
, как вы пытались сделать с baos
, где и происходит реализация памяти.
Итак, с тем, как это работает, мы пишем прямо в поток ответов, предоставленный для нас. Метод write
самом деле не вызывается до метода writeTo
(в MessageBodyWriter
), где ему OutputStream
.
Вы можете получить лучшую картину, глядя на MessageBodyWriter
я написал. В основном в методе writeTo
замените ByteArrayOutputStream
на StreamingOutput
, затем внутри метода вызовите streamingOutput.write(entityStream)
. Вы можете увидеть ссылку, которую я предоставил в предыдущей части ответа, где я ссылаюсь на StreamingOutputProvider
. Это именно то, что происходит