Wejściowe i wyjściowe strumienie binarne za pomocą JERSEY?

111

Używam Jersey do zaimplementowania interfejsu API RESTful, który jest głównie pobierany i obsługujący dane zakodowane w formacie JSON. Ale są sytuacje, w których muszę wykonać następujące czynności:

  • Eksportuj dokumenty do pobrania, takie jak PDF, XLS, ZIP lub inne pliki binarne.
  • Pobierz dane wieloczęściowe, takie jak niektóre JSON oraz przesłany plik XLS

Mam jednostronicowego klienta sieci Web opartego na JQuery, który tworzy wywołania AJAX do tej usługi sieci Web. W tej chwili nie przesyła formularzy i używa GET i POST (z obiektem JSON). Czy powinienem użyć postu formularza do wysłania danych i załączonego pliku binarnego, czy mogę utworzyć żądanie wieloczęściowe z plikiem JSON plus binarnym?

Warstwa usług mojej aplikacji obecnie tworzy ByteArrayOutputStream podczas generowania pliku PDF. Jaki jest najlepszy sposób przesłania tego strumienia do klienta za pośrednictwem Jersey? Utworzyłem MessageBodyWriter, ale nie wiem, jak go używać z zasobu Jersey. Czy to właściwe podejście?

Przeglądałem próbki dołączone do Jersey, ale nie znalazłem jeszcze niczego, co ilustruje, jak zrobić jedną z tych rzeczy. Jeśli to ma znaczenie, używam Jersey z Jacksonem do wykonywania Object-> JSON bez kroku XML i tak naprawdę nie używam JAX-RS.

Tauren
źródło

Odpowiedzi:

109

Udało mi się uzyskać plik ZIP lub plik PDF, rozszerzając StreamingOutputobiekt. Oto przykładowy kod:

@Path("PDF-file.pdf/")
@GET
@Produces({"application/pdf"})
public StreamingOutput getPDF() throws Exception {
    return new StreamingOutput() {
        public void write(OutputStream output) throws IOException, WebApplicationException {
            try {
                PDFGenerator generator = new PDFGenerator(getEntity());
                generator.generatePDF(output);
            } catch (Exception e) {
                throw new WebApplicationException(e);
            }
        }
    };
}

Klasa PDFGenerator (moja własna klasa do tworzenia pliku PDF) pobiera strumień wyjściowy z metody write i zapisuje do niego zamiast nowo utworzonego strumienia wyjściowego.

Nie wiem, czy to najlepszy sposób, ale działa.

MikeTheReader
źródło
33
Możliwe jest również zwrócenie StreamingOutput jako jednostki do Responseobiektu. W ten sposób możesz łatwo kontrolować typ nośnika, kod odpowiedzi HTTP itp. Daj mi znać, jeśli chcesz, abym wysłał kod.
Hank
3
@MyTitle: patrz przykład
Hank
3
Użyłem przykładów kodu w tym wątku jako odniesienia i stwierdziłem, że muszę opróżnić OutputStream w StreamingOutput.write (), aby klient mógł niezawodnie odbierać dane wyjściowe. W przeciwnym razie czasami otrzymywałem „Content-Length: 0” w nagłówkach i nie było treści, mimo że dzienniki informowały mnie, że StreamingOutput jest wykonywane.
Jon Stewart
@JonStewart - Wydaje mi się, że robiłem flush w ramach metody generatedPDF.
MikeTheReader
1
@ Dante617. Czy opublikowałbyś kod po stronie klienta, w jaki sposób klient Jersey wysyła strumień binarny na serwer (z jersey 2.x)?
Débora
29

Musiałem zwrócić plik rtf i to zadziałało.

// create a byte array of the file in correct format
byte[] docStream = createDoc(fragments); 

return Response
            .ok(docStream, MediaType.APPLICATION_OCTET_STREAM)
            .header("content-disposition","attachment; filename = doc.rtf")
            .build();
Abhishek Rakshit
źródło
26
Niezbyt dobrze, ponieważ wyjście jest wysyłane dopiero po całkowitym przygotowaniu. Bajt [] nie jest strumieniem.
java.is.for.desktop
7
Zużywa to wszystkie bajty do pamięci, co oznacza, że ​​duże pliki mogą spowodować uszkodzenie serwera. Celem przesyłania strumieniowego jest uniknięcie zużywania wszystkich bajtów do pamięci.
Robert Christian
22

Używam tego kodu do eksportu pliku Excela (xlsx) (Apache Poi) w koszulce jako załącznika.

@GET
@Path("/{id}/contributions/excel")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public Response exportExcel(@PathParam("id") Long id)  throws Exception  {

    Resource resource = new ClassPathResource("/xls/template.xlsx");

    final InputStream inp = resource.getInputStream();
    final Workbook wb = WorkbookFactory.create(inp);
    Sheet sheet = wb.getSheetAt(0);

    Row row = CellUtil.getRow(7, sheet);
    Cell cell = CellUtil.getCell(row, 0);
    cell.setCellValue("TITRE TEST");

    [...]

    StreamingOutput stream = new StreamingOutput() {
        public void write(OutputStream output) throws IOException, WebApplicationException {
            try {
                wb.write(output);
            } catch (Exception e) {
                throw new WebApplicationException(e);
            }
        }
    };


    return Response.ok(stream).header("content-disposition","attachment; filename = export.xlsx").build();

}
Grégory
źródło
15

Oto kolejny przykład. Tworzę QRCode jako PNG za pomocą pliku ByteArrayOutputStream. Zasób zwraca plikResponse obiekt, a dane strumienia są jednostką.

Aby zilustrować obsługę kodu odpowiedzi, Dodałem obsługi nagłówków Cache ( If-modified-since, If-none-matchesitp).

@Path("{externalId}.png")
@GET
@Produces({"image/png"})
public Response getAsImage(@PathParam("externalId") String externalId, 
        @Context Request request) throws WebApplicationException {

    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    // do something with externalId, maybe retrieve an object from the
    // db, then calculate data, size, expirationTimestamp, etc

    try {
        // create a QRCode as PNG from data     
        BitMatrix bitMatrix = new QRCodeWriter().encode(
                data, 
                BarcodeFormat.QR_CODE, 
                size, 
                size
        );
        MatrixToImageWriter.writeToStream(bitMatrix, "png", stream);

    } catch (Exception e) {
        // ExceptionMapper will return HTTP 500 
        throw new WebApplicationException("Something went wrong …")
    }

    CacheControl cc = new CacheControl();
    cc.setNoTransform(true);
    cc.setMustRevalidate(false);
    cc.setNoCache(false);
    cc.setMaxAge(3600);

    EntityTag etag = new EntityTag(HelperBean.md5(data));

    Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(
            updateTimestamp,
            etag
    );
    if (responseBuilder != null) {
        // Preconditions are not met, returning HTTP 304 'not-modified'
        return responseBuilder
                .cacheControl(cc)
                .build();
    }

    Response response = Response
            .ok()
            .cacheControl(cc)
            .tag(etag)
            .lastModified(updateTimestamp)
            .expires(expirationTimestamp)
            .type("image/png")
            .entity(stream.toByteArray())
            .build();
    return response;
}   

Proszę, nie bij mnie na wypadek, gdyby stream.toByteArray()nie było mądrej pamięci :) To działa dla moich plików PNG <1KB ...

Motek
źródło
6
Myślę, że to zły przykład przesyłania strumieniowego, ponieważ zwracany obiekt w danych wyjściowych jest tablicą bajtów, a nie strumieniem.
AlikElzin-kilaka
Dobry przykład tworzenia odpowiedzi na żądanie zasobu GET, a nie dobry przykład dla strumienia. To wcale nie jest strumień.
Robert Christian
14

Moje usługi Jersey 1.17 komponowałem w następujący sposób:

FileStreamingOutput

public class FileStreamingOutput implements StreamingOutput {

    private File file;

    public FileStreamingOutput(File file) {
        this.file = file;
    }

    @Override
    public void write(OutputStream output)
            throws IOException, WebApplicationException {
        FileInputStream input = new FileInputStream(file);
        try {
            int bytes;
            while ((bytes = input.read()) != -1) {
                output.write(bytes);
            }
        } catch (Exception e) {
            throw new WebApplicationException(e);
        } finally {
            if (output != null) output.close();
            if (input != null) input.close();
        }
    }

}

GET

@GET
@Produces("application/pdf")
public StreamingOutput getPdf(@QueryParam(value="name") String pdfFileName) {
    if (pdfFileName == null)
        throw new WebApplicationException(Response.Status.BAD_REQUEST);
    if (!pdfFileName.endsWith(".pdf")) pdfFileName = pdfFileName + ".pdf";

    File pdf = new File(Settings.basePath, pdfFileName);
    if (!pdf.exists())
        throw new WebApplicationException(Response.Status.NOT_FOUND);

    return new FileStreamingOutput(pdf);
}

A klient, jeśli tego potrzebujesz:

Client

private WebResource resource;

public InputStream getPDFStream(String filename) throws IOException {
    ClientResponse response = resource.path("pdf").queryParam("name", filename)
        .type("application/pdf").get(ClientResponse.class);
    return response.getEntityInputStream();
}
Daniel Szalay
źródło
7

Ten przykład pokazuje, jak publikować pliki dziennika w JBoss za pośrednictwem zasobu resztkowego. Zwróć uwagę, że metoda get używa interfejsu StreamingOutput do przesyłania strumieniowego zawartości pliku dziennika.

@Path("/logs/")
@RequestScoped
public class LogResource {

private static final Logger logger = Logger.getLogger(LogResource.class.getName());
@Context
private UriInfo uriInfo;
private static final String LOG_PATH = "jboss.server.log.dir";

public void pipe(InputStream is, OutputStream os) throws IOException {
    int n;
    byte[] buffer = new byte[1024];
    while ((n = is.read(buffer)) > -1) {
        os.write(buffer, 0, n);   // Don't allow any extra bytes to creep in, final write
    }
    os.close();
}

@GET
@Path("{logFile}")
@Produces("text/plain")
public Response getLogFile(@PathParam("logFile") String logFile) throws URISyntaxException {
    String logDirPath = System.getProperty(LOG_PATH);
    try {
        File f = new File(logDirPath + "/" + logFile);
        final FileInputStream fStream = new FileInputStream(f);
        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                try {
                    pipe(fStream, output);
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
        return Response.ok(stream).build();
    } catch (Exception e) {
        return Response.status(Response.Status.CONFLICT).build();
    }
}

@POST
@Path("{logFile}")
public Response flushLogFile(@PathParam("logFile") String logFile) throws URISyntaxException {
    String logDirPath = System.getProperty(LOG_PATH);
    try {
        File file = new File(logDirPath + "/" + logFile);
        PrintWriter writer = new PrintWriter(file);
        writer.print("");
        writer.close();
        return Response.ok().build();
    } catch (Exception e) {
        return Response.status(Response.Status.CONFLICT).build();
    }
}    

}

Jaime Casero
źródło
1
Do Twojej wiadomości: zamiast metody potoku możesz również użyć IOUtils.copy z Apache commons I / O.
David,
7

Korzystanie z Jersey 2.16 Pobieranie pliku jest bardzo łatwe.

Poniżej znajduje się przykład pliku ZIP

@GET
@Path("zipFile")
@Produces("application/zip")
public Response getFile() {
    File f = new File(ZIP_FILE_PATH);

    if (!f.exists()) {
        throw new WebApplicationException(404);
    }

    return Response.ok(f)
            .header("Content-Disposition",
                    "attachment; filename=server.zip").build();
}
orangegiraffa
źródło
1
Działa jak marzenie. Mam jakieś pojęcie o tym strumieniowym przesyłaniu danych, nie do końca rozumiem ...
Oliver
1
To najłatwiejszy sposób, jeśli używasz Jersey. Dzięki
ganchito55
Czy można zrobić z @POST zamiast @GET?
spr
@spr Myślę, że tak, jest to możliwe. Gdy strona serwera odpowie, powinna wyświetlić okno pobierania
orangegiraffa
5

Poniższe informacje były dla mnie pomocne i chciałem się nimi podzielić na wypadek, gdyby pomogły Tobie lub komuś innemu. Chciałem czegoś takiego jak MediaType.PDF_TYPE, który nie istnieje, ale ten kod robi to samo:

DefaultMediaTypePredictor.CommonMediaTypes.
        getMediaTypeFromFileName("anything.pdf")

Zobacz http://jersey.java.net/nonav/apidocs/1.1.0-ea/contribs/jersey-multipart/com/sun/jersey/multipart/file/DefaultMediaTypePredictor.CommonMediaTypes.html

W moim przypadku wysyłałem dokument PDF do innej witryny:

FormDataMultiPart p = new FormDataMultiPart();
p.bodyPart(new FormDataBodyPart(FormDataContentDisposition
        .name("fieldKey").fileName("document.pdf").build(),
        new File("path/to/document.pdf"),
        DefaultMediaTypePredictor.CommonMediaTypes
                .getMediaTypeFromFileName("document.pdf")));

Następnie p zostanie przekazany jako drugi parametr funkcji post ().

Ten link był dla mnie pomocny w tworzeniu tego fragmentu kodu: http://jersey.576304.n2.nabble.com/Multipart-Post-td4252846.html

Dovev Hefetz
źródło
4

To działało dobrze ze mną adres URL: http://example.com/rest/muqsith/get-file?filePath=C : \ Users \ I066807 \ Desktop \ test.xml

@GET
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
@Path("/get-file")
public Response getFile(@Context HttpServletRequest request){
   String filePath = request.getParameter("filePath");
   if(filePath != null && !"".equals(filePath)){
        File file = new File(filePath);
        StreamingOutput stream = null;
        try {
        final InputStream in = new FileInputStream(file);
        stream = new StreamingOutput() {
            public void write(OutputStream out) throws IOException, WebApplicationException {
                try {
                    int read = 0;
                        byte[] bytes = new byte[1024];

                        while ((read = in.read(bytes)) != -1) {
                            out.write(bytes, 0, read);
                        }
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
        return Response.ok(stream).header("content-disposition","attachment; filename = "+file.getName()).build();
        }
    return Response.ok("file path null").build();
}
Muqsith
źródło
1
Nie jestem pewien Response.ok("file path null").build();, czy to naprawdę w porządku? Prawdopodobnie powinieneś użyć czegoś takiego jakResponse.status(Status.BAD_REQUEST).entity(...
Christophe Roussy,
1

Inny przykładowy kod, w którym można przesłać plik do usługi REST, usługa REST spakuje plik, a klient pobiera plik zip z serwera. To jest dobry przykład wykorzystania binarnych strumieni wejściowych i wyjściowych przy użyciu Jersey.

https://stackoverflow.com/a/32253028/15789

Ta odpowiedź została opublikowana przeze mnie w innym wątku. Mam nadzieję że to pomoże.

RuntimeException
źródło