Binäre Streams mit JERSEY eingeben und ausgeben?

111

Ich verwende Jersey, um eine RESTful-API zu implementieren, die hauptsächlich JSON-codierte Daten abruft und bereitstellt. Aber ich habe einige Situationen, in denen ich Folgendes erreichen muss:

  • Exportieren Sie herunterladbare Dokumente wie PDF, XLS, ZIP oder andere Binärdateien.
  • Rufen Sie mehrteilige Daten ab, z. B. JSON und eine hochgeladene XLS-Datei

Ich habe einen einseitigen JQuery-basierten Webclient, der AJAX-Aufrufe für diesen Webdienst erstellt. Im Moment werden keine Formularübermittlungen durchgeführt und GET und POST (mit einem JSON-Objekt) verwendet. Sollte ich einen Formularbeitrag zum Senden von Daten und einer angehängten Binärdatei verwenden oder kann ich eine mehrteilige Anforderung mit JSON plus Binärdatei erstellen?

Die Service-Schicht meiner Anwendung erstellt derzeit einen ByteArrayOutputStream, wenn eine PDF-Datei generiert wird. Was ist der beste Weg, um diesen Stream über Jersey an den Client auszugeben? Ich habe einen MessageBodyWriter erstellt, weiß aber nicht, wie ich ihn aus einer Jersey-Ressource verwenden soll. Ist das der richtige Ansatz?

Ich habe die in Jersey enthaltenen Beispiele durchgesehen, aber noch nichts gefunden, das veranschaulicht, wie eines dieser beiden Dinge zu tun ist. Wenn es darauf ankommt, verwende ich Jersey mit Jackson, um Object-> JSON ohne den XML-Schritt auszuführen, und verwende JAX-RS nicht wirklich.

Tauren
quelle

Antworten:

109

Ich habe es geschafft, eine ZIP-Datei oder eine PDF-Datei zu erhalten, indem ich das StreamingOutputObjekt erweitert habe. Hier ist ein Beispielcode:

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

Die PDFGenerator-Klasse (meine eigene Klasse zum Erstellen des PDF) nimmt den Ausgabestream von der Schreibmethode und schreibt in diesen anstelle eines neu erstellten Ausgabestreams.

Ich weiß nicht, ob es der beste Weg ist, aber es funktioniert.

MikeTheReader
quelle
33
Es ist auch möglich, den StreamingOutput als Entität an ein ResponseObjekt zurückzugeben. Auf diese Weise können Sie den Medientyp, den HTTP-Antwortcode usw. einfach steuern. Lassen Sie mich wissen, ob ich einen Code veröffentlichen soll.
Hank
3
@ MyTitle: siehe Beispiel
Hank
3
Ich habe die Codebeispiele in diesem Thread als Referenz verwendet und festgestellt, dass ich den OutputStream in StreamingOutput.write () leeren muss, damit der Client die Ausgabe zuverlässig empfängt. Andernfalls würde manchmal "Content-Length: 0" in den Headern und kein Body angezeigt, obwohl mir Protokolle mitteilten, dass der StreamingOutput ausgeführt wurde.
Jon Stewart
@ JonStewart - Ich glaube, ich habe den Flush innerhalb der generatePDF-Methode durchgeführt.
MikeTheReader
1
@ Dante617. Würden Sie den clientseitigen Code veröffentlichen, wie der Jersey-Client den Binärstrom an den Server sendet (mit Jersey 2.x)?
Débora
29

Ich musste eine RTF-Datei zurückgeben und das funktionierte für mich.

// 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
quelle
26
Nicht so gut, da die Ausgabe erst gesendet wird, nachdem sie vollständig vorbereitet wurde. Ein Byte [] ist kein Stream.
java.is.for.desktop
7
Dies verbraucht alle Bytes im Speicher, was bedeutet, dass große Dateien den Server herunterfahren können. Der Zweck des Streamings besteht darin, zu vermeiden, dass alle Bytes in den Speicher geladen werden.
Robert Christian
22

Ich verwende diesen Code, um eine Excel-Datei (xlsx) (Apache Poi) in Jersey als Anhang zu exportieren.

@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
quelle
15

Hier ist ein weiteres Beispiel. Ich erstelle einen QRCode als PNG über a ByteArrayOutputStream. Die Ressource gibt ein ResponseObjekt zurück und die Daten des Streams sind die Entität.

Um die Antwortcode Handhabung veranschaulicht, habe ich den Umgang mit Cache - Header hinzugefügt ( If-modified-since, If-none-matchesusw.).

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

Bitte verprügel mich nicht, falls stream.toByteArray()es ein No-No-Speicher ist :) Es funktioniert für meine <1 KB PNG-Dateien ...

Strang
quelle
6
Ich denke, es ist ein schlechtes Streaming-Beispiel, da das zurückgegebene Objekt in der Ausgabe ein Byte-Array und kein Stream ist.
AlikElzin-Kilaka
Gutes Beispiel für die Erstellung einer Antwort auf eine GET-Ressourcenanforderung, kein gutes Beispiel für Stream. Dies ist überhaupt kein Stream.
Robert Christian
14

Ich habe meine Jersey 1.17-Dienste folgendermaßen zusammengestellt:

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);
}

Und der Kunde, wenn Sie es brauchen:

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
quelle
7

Dieses Beispiel zeigt, wie Protokolldateien in JBoss über eine Restressource veröffentlicht werden. Beachten Sie, dass die get-Methode die StreamingOutput-Schnittstelle verwendet, um den Inhalt der Protokolldatei zu streamen.

@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
quelle
1
Nur zu Ihrer Information: Anstelle der Pipe-Methode können Sie auch IOUtils.copy von Apache Commons I / O verwenden.
David
7

Das Herunterladen von Jersey 2.16-Dateien ist sehr einfach.

Unten sehen Sie das Beispiel für die ZIP-Datei

@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
quelle
1
Klappt wunderbar. Haben Sie eine Idee über dieses Streaming-Zeug, ich verstehe es nicht ganz ...
Oliver
1
Es ist der einfachste Weg, wenn Sie Jersey verwenden, danke
ganchito55
Ist es möglich, mit @POST anstelle von @GET zu arbeiten?
Spr
@spr Ich denke ja, es ist möglich. Wenn die Serverseite antwortet, sollte sie das Download-Fenster
bereitstellen
5

Ich fand das Folgende hilfreich für mich und wollte es teilen, falls es Ihnen oder jemand anderem hilft. Ich wollte so etwas wie MediaType.PDF_TYPE, das es nicht gibt, aber dieser Code macht dasselbe:

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

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

In meinem Fall habe ich ein PDF-Dokument auf einer anderen Site veröffentlicht:

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")));

Dann wird p als zweiter Parameter an post () übergeben.

Dieser Link war hilfreich für mich beim Zusammenstellen dieses Code-Snippets: http://jersey.576304.n2.nabble.com/Multipart-Post-td4252846.html

Dovev Hefetz
quelle
4

Dies funktionierte gut mit meiner 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
quelle
1
Nicht sicher Response.ok("file path null").build();, ist es wirklich in Ordnung? Sie sollten wahrscheinlich so etwas wieResponse.status(Status.BAD_REQUEST).entity(...
Christophe Roussy
1

Ein weiterer Beispielcode, mit dem Sie eine Datei in den REST-Dienst hochladen können. Der REST-Dienst komprimiert die Datei und der Client lädt die Zip-Datei vom Server herunter. Dies ist ein gutes Beispiel für die Verwendung von binären Eingabe- und Ausgabestreams mit Jersey.

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

Diese Antwort wurde von mir in einem anderen Thread gepostet. Hoffe das hilft.

RuntimeException
quelle