qual è il modo corretto per inviare un file dal servizio web REST al client?


103

Ho appena iniziato a sviluppare servizi REST, ma mi sono imbattuto in una situazione difficile: inviare file dal mio servizio REST al mio cliente. Finora ho capito come inviare semplici tipi di dati (stringhe, numeri interi, ecc.) Ma l'invio di un file è una questione diversa poiché ci sono così tanti formati di file che non so nemmeno dove dovrei iniziare. Il mio servizio REST è realizzato su Java e sto utilizzando Jersey, invio tutti i dati utilizzando il formato JSON.

Ho letto della codifica base64, alcune persone dicono che è una buona tecnica, altri dicono che non è a causa di problemi di dimensione del file. Qual è il modo corretto? Ecco come appare una semplice classe di risorse nel mio progetto:

import java.sql.SQLException;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.UriInfo;

import com.mx.ipn.escom.testerRest.dao.TemaDao;
import com.mx.ipn.escom.testerRest.modelo.Tema;

@Path("/temas")
public class TemaResource {

    @GET
    @Produces({MediaType.APPLICATION_JSON})
    public List<Tema> getTemas() throws SQLException{

        TemaDao temaDao = new TemaDao();        
        List<Tema> temas=temaDao.getTemas();
        temaDao.terminarSesion();

        return temas;
    }
}

Immagino che il codice per l'invio di un file sarebbe qualcosa del tipo:

import java.sql.SQLException;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/resourceFiles")
public class FileResource {

    @GET
    @Produces({application/x-octet-stream})
    public File getFiles() throws SQLException{ //I'm not really sure what kind of data type I should return

        // Code for encoding the file or just send it in a data stream, I really don't know what should be done here

        return file;
    }
}

Che tipo di annotazioni dovrei usare? Ho visto alcune persone consigliare per un @GETutilizzo @Produces({application/x-octet-stream}), è il modo corretto? I file che sto inviando sono specifici, quindi il client non ha bisogno di sfogliare i file. Qualcuno può guidarmi su come dovrei inviare il file? Dovrei codificarlo usando base64 per inviarlo come oggetto JSON? o la codifica non è necessaria per inviarlo come oggetto JSON? Grazie per qualsiasi aiuto tu possa dare.


Hai un effettivo java.io.File(o percorso file) sul tuo server o i dati provengono da qualche altra fonte, come un database, un servizio web, una chiamata al metodo che restituisce un InputStream?
Philipp Reichart

Risposte:


138

Non consiglio di codificare i dati binari in base64 e di avvolgerli in JSON. Aumenterà inutilmente la dimensione della risposta e rallenterà le cose.

Fornisci semplicemente i dati del tuo file utilizzando GET e application/octect-streamutilizzando uno dei metodi di fabbrica di javax.ws.rs.core.Response(parte dell'API JAX-RS, quindi non sei bloccato in Jersey):

@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response getFile() {
  File file = ... // Initialize this to the File path you want to serve.
  return Response.ok(file, MediaType.APPLICATION_OCTET_STREAM)
      .header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"" ) //optional
      .build();
}

Se non si dispone di un vero e proprio Fileoggetto, ma una InputStream, Response.ok(entity, mediaType)dovrebbe essere in grado di gestire anche quello.


grazie, ha funzionato benissimo, ma cosa succede se voglio consumare un'intera struttura di cartelle? Stavo pensando qualcosa di simile a questo Anche perché sarò ricevendo vari file sul client, come devo trattare la risposta dell'entità del HttpResponse?
Uriel

4
Dai un'occhiata ZipOutputStreaminsieme al ritorno di un StreamingOutputda getFile(). In questo modo si ottiene un noto formato multi-file che la maggior parte dei client dovrebbe essere facilmente in grado di leggere. Usa la compressione solo se ha senso per i tuoi dati, cioè non per file precompressi come i JPEG. Sul lato client, c'è ZipInputStreamda analizzare la risposta.
Philipp Reichart,


C'è un modo per aggiungere i metadati del file nella risposta insieme ai dati binari del file?
abhig

Puoi sempre aggiungere più intestazioni alla risposta. Se ciò non è sufficiente, dovrai codificarlo nel flusso di ottetti, ovvero servire un formato contenitore che abbia sia i metadati che il file che desideri.
Philipp Reichart

6

Se vuoi restituire un file da scaricare, specialmente se vuoi integrarlo con alcune librerie javascript di caricamento / download di file, allora il codice qui sotto dovrebbe fare il lavoro:

@GET
@Path("/{key}")
public Response download(@PathParam("key") String key,
                         @Context HttpServletResponse response) throws IOException {
    try {
        //Get your File or Object from wherever you want...
            //you can use the key parameter to indentify your file
            //otherwise it can be removed
        //let's say your file is called "object"
        response.setContentLength((int) object.getContentLength());
        response.setHeader("Content-Disposition", "attachment; filename="
                + object.getName());
        ServletOutputStream outStream = response.getOutputStream();
        byte[] bbuf = new byte[(int) object.getContentLength() + 1024];
        DataInputStream in = new DataInputStream(
                object.getDataInputStream());
        int length = 0;
        while ((in != null) && ((length = in.read(bbuf)) != -1)) {
            outStream.write(bbuf, 0, length);
        }
        in.close();
        outStream.flush();
    } catch (S3ServiceException e) {
        e.printStackTrace();
    } catch (ServiceException e) {
        e.printStackTrace();
    }
    return Response.ok().build();
}

3

Modificare l'indirizzo della macchina da localhost a indirizzo IP a cui si desidera connettere il client per chiamare il servizio indicato di seguito.

Cliente da chiamare il servizio web REST:

package in.india.client.downloadfiledemo;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.MultiPart;

public class DownloadFileClient {

    private static final String BASE_URI = "http://localhost:8080/DownloadFileDemo/services/downloadfile";

    public DownloadFileClient() {

        try {
            Client client = Client.create();
            WebResource objWebResource = client.resource(BASE_URI);
            ClientResponse response = objWebResource.path("/")
                    .type(MediaType.TEXT_HTML).get(ClientResponse.class);

            System.out.println("response : " + response);
            if (response.getStatus() == Status.OK.getStatusCode()
                    && response.hasEntity()) {
                MultiPart objMultiPart = response.getEntity(MultiPart.class);
                java.util.List<BodyPart> listBodyPart = objMultiPart
                        .getBodyParts();
                BodyPart filenameBodyPart = listBodyPart.get(0);
                BodyPart fileLengthBodyPart = listBodyPart.get(1);
                BodyPart fileBodyPart = listBodyPart.get(2);

                String filename = filenameBodyPart.getEntityAs(String.class);
                String fileLength = fileLengthBodyPart
                        .getEntityAs(String.class);
                File streamedFile = fileBodyPart.getEntityAs(File.class);

                BufferedInputStream objBufferedInputStream = new BufferedInputStream(
                        new FileInputStream(streamedFile));

                byte[] bytes = new byte[objBufferedInputStream.available()];

                objBufferedInputStream.read(bytes);

                String outFileName = "D:/"
                        + filename;
                System.out.println("File name is : " + filename
                        + " and length is : " + fileLength);
                FileOutputStream objFileOutputStream = new FileOutputStream(
                        outFileName);
                objFileOutputStream.write(bytes);
                objFileOutputStream.close();
                objBufferedInputStream.close();
                File receivedFile = new File(outFileName);
                System.out.print("Is the file size is same? :\t");
                System.out.println(Long.parseLong(fileLength) == receivedFile
                        .length());
            }
        } catch (UniformInterfaceException e) {
            e.printStackTrace();
        } catch (ClientHandlerException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String... args) {
        new DownloadFileClient();
    }
}

Servizio al cliente di risposta:

package in.india.service.downloadfiledemo;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.sun.jersey.multipart.MultiPart;

@Path("downloadfile")
@Produces("multipart/mixed")
public class DownloadFileResource {

    @GET
    public Response getFile() {

        java.io.File objFile = new java.io.File(
                "D:/DanGilbert_2004-480p-en.mp4");
        MultiPart objMultiPart = new MultiPart();
        objMultiPart.type(new MediaType("multipart", "mixed"));
        objMultiPart
                .bodyPart(objFile.getName(), new MediaType("text", "plain"));
        objMultiPart.bodyPart("" + objFile.length(), new MediaType("text",
                "plain"));
        objMultiPart.bodyPart(objFile, new MediaType("multipart", "mixed"));

        return Response.ok(objMultiPart).build();

    }
}

JAR necessario:

jersey-bundle-1.14.jar
jersey-multipart-1.14.jar
mimepull.jar

WEB.XML:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">
    <display-name>DownloadFileDemo</display-name>
    <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>JAX-RS REST Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <init-param>
             <param-name>com.sun.jersey.config.property.packages</param-name> 
             <param-value>in.india.service.downloadfiledemo</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>JAX-RS REST Servlet</servlet-name>
        <url-pattern>/services/*</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

-2

Dato che stai usando JSON, lo codificherei Base64 prima di inviarlo attraverso il cavo.

Se i file sono di grandi dimensioni, prova a esaminare BSON o un altro formato migliore con i trasferimenti binari.

Puoi anche comprimere i file, se si comprimono bene, prima di codificarli in base64.


Avevo intenzione di comprimerli prima di inviarli per l'intero motivo della dimensione del file, ma se lo codifico in base64, cosa dovrebbe @Producescontenere la mia annotazione?
Uriel

application / json secondo le specifiche JSON, indipendentemente da ciò che ci metti. ( ietf.org/rfc/rfc4627.txt?number=4627 ) Tieni presente che il file con codifica base64 dovrebbe essere ancora all'interno dei tag JSON
LarsK

3
Non c'è alcun vantaggio nella codifica dei dati binari in base64 e quindi nel wrapping in JSON. Aumenterà inutilmente la dimensione della risposta e rallenterà le cose.
Philipp Reichart
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.