qual é a maneira correta de enviar um arquivo do serviço da web REST para o cliente?

103

Acabei de começar a desenvolver serviços REST, mas me deparei com uma situação difícil: enviar arquivos do meu serviço REST para o meu cliente. Até agora peguei o jeito de enviar tipos de dados simples (strings, inteiros, etc), mas enviar um arquivo é uma questão diferente, já que existem tantos formatos de arquivo que não sei por onde devo começar. Meu serviço REST é feito em Java e estou usando Jersey, estou enviando todos os dados no formato JSON.

Eu li sobre a codificação base64, algumas pessoas dizem que é uma boa técnica, outras dizem que não é por causa de problemas de tamanho de arquivo. Qual é a maneira correta? Esta é a aparência de uma classe de recurso simples em meu projeto:

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

Acho que o código para enviar um arquivo seria algo como:

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

Que tipo de anotações devo usar? Já vi algumas pessoas recomendarem um @GETuso @Produces({application/x-octet-stream}), é esse o jeito correto? Os arquivos que estou enviando são específicos para que o cliente não precise navegar pelos arquivos. Alguém pode me orientar sobre como devo enviar o arquivo? Devo codificá-lo usando base64 para enviá-lo como um objeto JSON? ou a codificação não é necessária para enviá-lo como um objeto JSON? Obrigado por qualquer ajuda que você possa dar.

Uriel
fonte
Você tem um java.io.File(ou caminho de arquivo) real em seu servidor ou os dados vêm de alguma outra fonte, como um banco de dados, serviço da web, chamada de método retornando um InputStream?
Philipp Reichart

Respostas:

138

Não recomendo codificar dados binários em base64 e envolvê-los em JSON. Isso apenas aumentará desnecessariamente o tamanho da resposta e tornará as coisas mais lentas.

Basta fornecer seus dados de arquivo usando GET e application/octect-streamusando um dos métodos de fábrica de javax.ws.rs.core.Response(parte da API JAX-RS, para que você não fique preso a 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 você não tem um Fileobjeto real , mas um InputStream, Response.ok(entity, mediaType)deve ser capaz de lidar com isso também.

Philipp Reichart
fonte
obrigado, funcionou muito bem, mas e se eu quiser consumir uma estrutura de pastas inteira? Eu estava pensando em algo como isso também desde que eu estarei recebendo vários arquivos no cliente, como devo tratar a resposta entidade de HttpResponse?
Uriel
4
Dê uma olhada ZipOutputStreamjunto com retornando um StreamingOutputde getFile(). Dessa forma, você obtém um formato de vários arquivos bem conhecido que a maioria dos clientes deve ser capaz de ler facilmente. Use a compactação apenas se fizer sentido para seus dados, ou seja, não para arquivos pré-compactados como JPEGs. Do lado do cliente, é ZipInputStreamnecessário analisar a resposta.
Philipp Reichart,
1
Isso pode ajudar: stackoverflow.com/questions/10100936/…
Basil Dsouza
Existe uma maneira de adicionar metadados do arquivo na resposta junto com os dados binários do arquivo?
abhig
Você sempre pode adicionar mais cabeçalhos à resposta. Se isso não for suficiente, você terá que codificá-lo no fluxo de octeto, ou seja, servir um formato de contêiner que contém os metadados e o arquivo que você deseja.
Philipp Reichart
6

Se você deseja retornar um arquivo para ser baixado, especialmente se você deseja integrar com algumas bibliotecas javascript de upload / download de arquivo, então o código abaixo deve fazer o trabalho:

@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();
}
john 4d5
fonte
3

Altere o endereço da máquina de localhost para o endereço IP com o qual deseja que seu cliente se conecte para chamar o serviço mencionado abaixo.

Cliente para chamar o serviço da 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();
    }
}

Serviço ao cliente de resposta:

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 necessário:

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>
Shankar Saran Singh
fonte
-2

Como você está usando JSON, gostaria de codificá-lo em Base64 antes de enviá-lo pelo fio.

Se os arquivos forem grandes, tente olhar o BSON ou algum outro formato que seja melhor com transferências binárias.

Você também pode compactar os arquivos, se eles forem bem compactados, antes de codificá-los em base64.

LarsK
fonte
Eu estava planejando compactá-los antes de enviá-los devido ao tamanho total do arquivo, mas se eu codificá-lo em base64, o que minha @Producesanotação deve conter?
Uriel
application / json de acordo com a especificação JSON, independentemente do que você colocar nele. ( ietf.org/rfc/rfc4627.txt?number=4627 ) Tenha em mente que o arquivo codificado em base64 ainda deve estar dentro de tags JSON
LarsK
3
Não há benefício em codificar dados binários em base64 e envolvê-los em JSON. Isso apenas aumentará desnecessariamente o tamanho da resposta e tornará as coisas mais lentas.
Philipp Reichart