jaki jest prawidłowy sposób wysyłania pliku z usługi internetowej REST do klienta?

103

Dopiero co zacząłem rozwijać usługi REST, ale natknąłem się na trudną sytuację: wysyłanie plików z mojej usługi REST do klienta. Do tej pory zrozumiałem, jak wysyłać proste typy danych (ciągi znaków, liczby całkowite itp.), Ale wysyłanie pliku to inna sprawa, ponieważ istnieje tak wiele formatów plików, że nie wiem, od czego powinienem zacząć. Moja usługa REST jest wykonana na Javie i używam Jersey, wszystkie dane wysyłam w formacie JSON.

Czytałem o kodowaniu base64, niektórzy mówią, że to dobra technika, inni mówią, że nie jest to spowodowane problemami z rozmiarem pliku. Jaki jest właściwy sposób? Oto jak wygląda prosta klasa zasobów w moim projekcie:

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

Domyślam się, że kod do wysłania pliku wyglądałby tak:

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

Jakiego rodzaju adnotacji mam używać? Widziałem, jak niektórzy polecają @GETużycie @Produces({application/x-octet-stream}), czy to właściwy sposób? Pliki, które wysyłam, są specyficzne, więc klient nie musi ich przeglądać. Czy ktoś może mnie poinstruować, jak mam wysłać plik? Czy powinienem zakodować go przy użyciu base64, aby wysłać go jako obiekt JSON? czy kodowanie nie jest konieczne, aby wysłać go jako obiekt JSON? Dziękuję za wszelką pomoc, której możesz udzielić.

Uriel
źródło
Czy masz rzeczywistą java.io.File(lub ścieżkę do pliku) na swoim serwerze, czy też dane pochodzą z innego źródła, takiego jak baza danych, usługa sieciowa, wywołanie metody zwraca InputStream?
Philipp Reichart,

Odpowiedzi:

138

Nie polecam kodowania danych binarnych w base64 i zawijania ich w JSON. Po prostu niepotrzebnie zwiększy rozmiar odpowiedzi i spowolni działanie.

Po prostu podaj dane pliku za pomocą GET i application/octect-streamużywając jednej z fabrycznych metod javax.ws.rs.core.Response(część JAX-RS API, więc nie jesteś zamknięty w 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();
}

Jeśli nie masz rzeczywisty Fileobiekt, ale InputStream, Response.ok(entity, mediaType)powinien być w stanie obsłużyć, że dobrze.

Philipp Reichart
źródło
dzięki, to działało świetnie, ale co jeśli chcę wykorzystać całą strukturę folderów? Myślałam coś to także ponieważ będę otrzymywać różne pliki na kliencie, w jaki sposób należy traktować HttpResponse reagowania jednostki?
Uriel
4
Spójrz na ZipOutputStreamwraz z powrotem StreamingOutputz getFile(). W ten sposób otrzymujesz dobrze znany format wielu plików, który większość klientów powinna łatwo odczytać. Używaj kompresji tylko wtedy, gdy ma to sens w przypadku danych, tj. Nie w przypadku wstępnie skompresowanych plików, takich jak JPEG. Po stronie klienta ZipInputStreamtrzeba przeanalizować odpowiedź.
Philipp Reichart,
1
To może pomóc: stackoverflow.com/questions/10100936/…
Basil Dsouza
Czy istnieje sposób na dodanie metadanych pliku w odpowiedzi wraz z danymi binarnymi pliku?
abhig
Zawsze możesz dodać więcej nagłówków do odpowiedzi. Jeśli to nie wystarczy, będziesz musiał zakodować go w strumieniu oktetu, tj. Podać format kontenera, który ma zarówno metadane, jak i żądany plik.
Philipp Reichart
6

Jeśli chcesz zwrócić plik do pobrania, szczególnie jeśli chcesz zintegrować z niektórymi bibliotekami javascript do przesyłania / pobierania plików, poniższy kod powinien wykonać zadanie:

@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
źródło
3

Zmień adres maszyny z localhost na adres IP, z którym ma łączyć się klient, aby wywołać poniższą usługę.

Klient dzwoni do serwisu WWW 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();
    }
}

Usługa odpowiadająca klientowi:

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 potrzebny:

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
źródło
-2

Ponieważ używasz JSON, zakodowałbym go w Base64 przed wysłaniem go przez kabel.

Jeśli pliki są duże, spróbuj spojrzeć na BSON lub inny format, który jest lepszy w przypadku transferów binarnych.

Możesz także spakować pliki, jeśli są dobrze skompresowane, przed zakodowaniem ich w base64.

LarsK
źródło
Planowałem spakować je przed wysłaniem ze względu na cały rozmiar pliku, ale jeśli zakoduję go base64, co powinna @Produceszawierać moja adnotacja?
Uriel
application / json zgodnie ze specyfikacją JSON, niezależnie od tego, co w niej umieścisz. ( ietf.org/rfc/rfc4627.txt?number=4627 ) Pamiętaj, że plik zakodowany w base64 powinien nadal znajdować się wewnątrz tagów JSON
LarsK,
3
Nie ma żadnej korzyści z kodowania danych binarnych w base64, a następnie opakowywania ich w JSON. Po prostu niepotrzebnie zwiększy rozmiar odpowiedzi i spowolni działanie.
Philipp Reichart,