apa cara yang benar untuk mengirim file dari layanan web REST ke klien?

103

Saya baru saja mulai mengembangkan layanan REST, tetapi saya menemukan situasi yang sulit: mengirim file dari layanan REST saya ke klien saya. Sejauh ini saya sudah memahami cara mengirim tipe data sederhana (string, integer, dll) tetapi mengirim file adalah masalah yang berbeda karena ada begitu banyak format file yang saya tidak tahu di mana saya harus memulai. Layanan REST saya dibuat di Java dan saya menggunakan Jersey, saya mengirim semua data menggunakan format JSON.

Saya telah membaca tentang pengkodean base64, beberapa orang mengatakan itu teknik yang bagus, yang lain mengatakan itu bukan karena masalah ukuran file. Bagaimana cara yang benar? Beginilah tampilan kelas sumber daya sederhana dalam proyek saya:

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

Saya menduga kode untuk mengirim file akan menjadi seperti:

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

Jenis anotasi apa yang harus saya gunakan? Saya telah melihat beberapa orang merekomendasikan untuk @GETdigunakan @Produces({application/x-octet-stream}), apakah itu cara yang benar? File yang saya kirim adalah file khusus sehingga klien tidak perlu menelusuri file. Adakah yang bisa memandu saya tentang bagaimana saya bisa mengirim file? Haruskah saya menyandikannya menggunakan base64 untuk mengirimkannya sebagai objek JSON? atau encoding tidak diperlukan untuk mengirimkannya sebagai objek JSON? Terima kasih atas bantuan yang mungkin Anda berikan.

Uriel
sumber
Apakah Anda memiliki sebenarnya java.io.File(atau jalur file) di server Anda atau apakah datanya berasal dari beberapa sumber lain, seperti database, layanan web, panggilan metode yang mengembalikan InputStream?
Philipp Reichart

Jawaban:

138

Saya tidak merekomendasikan pengkodean data biner di base64 dan membungkusnya dalam JSON. Itu hanya akan meningkatkan ukuran respons dan memperlambat segalanya.

Cukup sajikan data file Anda menggunakan GET dan application/octect-streammenggunakan salah satu metode pabrik javax.ws.rs.core.Response(bagian dari JAX-RS API, sehingga Anda tidak terkunci di 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();
}

Jika Anda tidak memiliki Fileobjek sebenarnya , tetapi sebuah InputStream, Response.ok(entity, mediaType)harus bisa mengatasinya juga.

Philipp Reichart
sumber
terima kasih, ini berfungsi dengan baik, tetapi bagaimana jika saya ingin menggunakan seluruh struktur folder? Saya sedang memikirkan sesuatu seperti ini Juga karena saya akan menerima berbagai file di klien, bagaimana saya harus memperlakukan respons entitas HttpResponse?
Uriel
4
Coba lihat ZipOutputStreambersama dengan mengembalikan StreamingOutputdari getFile(). Dengan cara ini Anda mendapatkan format multi-file terkenal yang dapat dibaca dengan mudah oleh sebagian besar klien. Gunakan kompresi hanya jika masuk akal untuk data Anda, yaitu bukan untuk file pra-kompresi seperti JPEG. Di sisi klien, ada ZipInputStreamuntuk mengurai respons.
Philipp Reichart
1
Ini mungkin membantu: stackoverflow.com/questions/10100936/…
Basil Dsouza
Apakah ada cara untuk menambahkan metadata file dalam respons bersama dengan data biner file?
abhig
Anda selalu dapat menambahkan lebih banyak tajuk ke respons. Jika itu tidak cukup, Anda harus menyandikannya ke dalam aliran oktet, yaitu menyajikan format wadah yang memiliki metadata dan file yang Anda inginkan.
Philipp Reichart
6

Jika Anda ingin mengembalikan File yang akan diunduh, khususnya jika Anda ingin mengintegrasikan dengan beberapa libs javascript dari unggah / unduh file, maka kode di bawah ini harus berfungsi:

@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
sumber
3

Ubah alamat mesin dari localhost ke alamat IP yang Anda ingin klien Anda sambungkan untuk memanggil layanan yang disebutkan di bawah ini.

Klien untuk memanggil layanan 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();
    }
}

Layanan untuk menanggapi klien:

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

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
sumber
-2

Karena Anda menggunakan JSON, saya akan Menyandikan Base64 sebelum mengirimkannya melalui kabel.

Jika filenya besar, coba lihat BSON, atau format lain yang lebih baik dengan transfer biner.

Anda juga dapat membuat file zip, jika dikompresi dengan baik, sebelum base64 mengenkodenya.

LarsK
sumber
Saya berencana untuk membuat zip sebelum mengirimnya karena alasan ukuran file secara keseluruhan, tetapi jika saya menyandikan base64, apa yang harus @Producesdimuat dalam anotasi saya ?
Uriel
application / json sesuai spesifikasi JSON, terlepas dari apa yang Anda masukkan. ( ietf.org/rfc/rfc4627.txt?number=4627 ) Ingatlah bahwa file yang dikodekan base64 harus tetap berada di dalam tag JSON
LarsK
3
Tidak ada manfaat dalam mengenkode data biner di base64 dan kemudian menggabungkannya dalam JSON. Itu hanya akan meningkatkan ukuran respons dan memperlambat segalanya.
Philipp Reichart