Как загрузить файл, используя сервис Java REST и поток данных

У меня 3 машины:

  1. сервер, на котором находится файл
  2. сервер, на котором работает служба REST (Джерси)
  3. клиент (браузер) с доступом ко второму серверу, но без доступа к первому серверу

Как я могу напрямую (без сохранения файла на 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
        }
    }
}
Другие вопросы по тегам