Как загрузить файл, используя сервис Java REST и поток данных
У меня 3 машины:
- сервер, на котором находится файл
- сервер, на котором работает служба REST (Джерси)
- клиент (браузер) с доступом ко второму серверу, но без доступа к первому серверу
Как я могу напрямую (без сохранения файла на 2-м сервере) загрузить файл с 1-го сервера на компьютер клиента?
Со 2-го сервера я могу получить ByteArrayOutputStream для получения файла с 1-го сервера. Могу ли я передать этот поток дальше клиенту, используя службу REST?
Так будет работать?
Таким образом, в основном я хочу разрешить клиенту загружать файл с 1-го сервера, используя службу REST на 2-м сервере (поскольку прямого доступа клиента к 1-му серверу нет), используя только потоки данных (поэтому данные не касаются файла система 2-го сервера).
Что я сейчас пытаюсь с библиотекой 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
Так что мой код теперь с пользовательским MessageBodyWriter выглядит просто:
ByteArrayOutputStream baos = new ByteArrayOutputStream (2048); client.downloadFile (location, spaceId, filePath, baos); return Response.ok (baos).build ();
Но я получаю ту же ошибку кучи при попытке с большими файлами.
ОБНОВЛЕНИЕ3 Наконец-то удалось заставить его работать! StreamingOutput сделал свое дело.
Спасибо @peeskillet! Большое спасибо!
3 ответа
"Как я могу напрямую (без сохранения файла на 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. Загрузка может быть легко достигнута с помощьюjava.net
Пакет API. Но так как вы уже используете Джерси, использование API-интерфейсов не повредитВыше предполагается, что Джерси 2.x. Для Jersey 1.x простой поиск в Google должен дать вам толчок для работы с API (или с документацией, на которую я ссылался выше)
ОБНОВИТЬ
Я такой придурок. В то время как ФП и я обдумываем способы превратить 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
от StreamingOutput
"s write
метод. Кажется сторонний API, требуется OutputStream
в качестве аргумента.
StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
thirdPartyApi.downloadFile(.., .., .., out);
}
}
return Response.ok(output).build();
Не совсем уверен, но кажется, что чтение / запись в методе ресурса, используя ByteArrayOutputStream`, реализовали что-то в памяти.
Суть downloadFile
метод принятия OutputStream
так что он может записать результат непосредственно в OutputStream
предоставлена. Например, FileOutputStream
, если вы записали его в файл, пока идет загрузка, он будет напрямую передан в файл.
Мы не имеем права хранить ссылку на OutputStream
, как вы пытались сделать с баосом, который вы пытались сделать, вот где приходит реализация памяти.
Итак, с тем, как это работает, мы пишем прямо в поток ответов, предоставленный для нас. Метод write
на самом деле не вызывается, пока writeTo
метод (в MessageBodyWriter
), где OutputStream
передается к нему.
Вы можете получить лучшую картину, глядя на MessageBodyWriter
Я написал. В основном в writeTo
метод, заменить ByteArrayOutputStream
с StreamingOutput
затем внутри метода вызовите streamingOutput.write(entityStream)
, Вы можете увидеть ссылку, которую я предоставил в предыдущей части ответа, где я ссылаюсь на StreamingOutputProvider
, Это именно то, что происходит
Отослать это:
@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();
}
Подробности на: https://twilblog.github.io/java/spring/rest/file/stream/2015/08/14/return-a-file-stream-from-spring-rest.html
Смотрите пример здесь: Входные и выходные двоичные потоки с использованием JERSEY?
Псевдокод будет выглядеть примерно так (есть несколько других подобных опций в вышеупомянутом посте):
@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
}
}
}