Как загрузить файл с помощью сервиса JAVA REST и потока данных

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

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

Как я могу напрямую (без сохранения файла на 2-м сервере) загрузить файл с 1-го сервера на машину клиента?
С 2nd сервера я могу получить 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();

обновление 2

Итак, мой код теперь с пользовательским MessageBodyWriter выглядит просто:

ByteArrayOutputStream baos = новый ByteArrayOutputStream(2048) ; клиент.downloadFile (location, spaceId, filePath, baos); ответный ответ.хорошо (баос).build();

но я получаю ту же ошибку при попытке кучи с большими файлами.

UPDATE3 Наконец удалось заставить его работать ! 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();

какой из них более эффективен, я не уверен, но возвращенный InputStreams-это разные классы, поэтому вы можете посмотреть на это, если вам не все равно к.

со 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 можно даже поделиться. Я ничего не могу найти в Джерси 2.х документация (только потому, что это большой документ, и я слишком ленив, чтобы просмотреть его прямо сейчас :-), а в Нью-Джерси 1.х документация она говорит Client и WebResource (что эквивалентно WebTarget в 2.x)может совместно использоваться между потоками. Так что я предполагаю Джерси 2.x будет таким же. но вы можете подтвердить это сами.

  • вы не должны использовать Client API-интерфейс. Загрузка может быть легко достигнута с помощью java.net пакета API-интерфейсы. Но так как вы уже используете Джерси, не помешает использовать его Апис

  • выше предполагая Джерси 2.х. Для Джерси 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();

ОХ!

обновление 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

таким образом, окончательное решение для этого конкретного случая использования было для OP просто передать 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, как вы пытались сделать с baos, что вы пытались сделать, в котором входит реализация памяти.

так с тем, как это работает, мы пишем непосредственно в поток ответа не дал. Метод 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


см. пример здесь: входные и выходные двоичные потоки с использованием Джерси?

псевдо-код будет примерно таким (есть несколько других подобных опций в вышеупомянутом сообщении):

@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
        }
    }
}