与 Jersey RESTful Web 服务中的其他对象一起上传文件

Posted

技术标签:

【中文标题】与 Jersey RESTful Web 服务中的其他对象一起上传文件【英文标题】:File upload along with other object in Jersey restful web service 【发布时间】:2015-02-20 23:00:10 【问题描述】:

我想通过上传图像和员工数据在系统中创建员工信息。我可以使用球衣通过不同的休息电话来做到这一点。但我想在一个休息电话中实现。 我在结构下面提供。请帮我在这方面怎么做。

@POST
@Path("/upload2")
@Consumes(MediaType.MULTIPART_FORM_DATA,MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON)
public Response uploadFileWithData(
        @FormDataParam("file") InputStream fileInputStream,
        @FormDataParam("file") FormDataContentDisposition contentDispositionHeader,
        Employee emp) 

//..... business login


每当我尝试这样做时,Chrome 邮递员都会出错。下面给出了我的 Employee json 的简单结构。


    "Name": "John",
    "Age": 23,
    "Email": "john@gmail.com",
    "Adrs": 
        "DoorNo": "12-A",
        "Street": "Street-11",
        "City": "Bangalore",
        "Country": "Karnataka"
    

但是我可以通过拨打两个不同的电话来做到这一点,但我想在一个休息电话中实现,这样我就可以接收文件以及员工的实际数据。

请求您在这方面提供帮助。

【问题讨论】:

能否请您投票/接受答案,因为它在***.com/help/someone-answers 中有详细说明?谢谢 【参考方案1】:

你不能有两个Content-Types(从技术上讲,这就是我们在下面所做的,但它们与多部分的每个部分分开,但主要类型是多部分)。这基本上就是您对方法的期望。您期望 mutlipart json 一起作为主要媒体类型。 Employee 数据需要是多部分的一部分。因此,您可以为Employee 添加@FormDataParam("emp")

@FormDataParam("emp") Employee emp)  ...

这是我用来测试的类

@Path("/multipart")
public class MultipartResource 
    
    @POST
    @Path("/upload2")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response uploadFileWithData(
            @FormDataParam("file") InputStream fileInputStream,
            @FormDataParam("file") FormDataContentDisposition cdh,
            @FormDataParam("emp") Employee emp) throws Exception
        
        Image img = ImageIO.read(fileInputStream);
        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(img)));
        System.out.println(cdh.getName());
        System.out.println(emp);
        
        return Response.ok("Cool Tools!").build();
     

首先,我刚刚使用客户端 API 进行了测试,以确保它可以正常工作

@Test
public void testGetIt() throws Exception 
    
    final Client client = ClientBuilder.newBuilder()
        .register(MultiPartFeature.class)
        .build();
    WebTarget t = client.target(Main.BASE_URI).path("multipart").path("upload2");

    FileDataBodyPart filePart = new FileDataBodyPart("file", 
                                             new File("***.png"));
    // UPDATE: just tested again, and the below code is not needed.
    // It's redundant. Using the FileDataBodyPart already sets the
    // Content-Disposition information
    filePart.setContentDisposition(
            FormDataContentDisposition.name("file")
                                    .fileName("***.png").build());

    String empPartJson
            = ""
            + "  \"id\": 1234,"
            + "  \"name\": \"Peeskillet\""
            + "";

    MultiPart multipartEntity = new FormDataMultiPart()
            .field("emp", empPartJson, MediaType.APPLICATION_JSON_TYPE)
            .bodyPart(filePart);
          
    Response response = t.request().post(
            Entity.entity(multipartEntity, multipartEntity.getMediaType()));
    System.out.println(response.getStatus());
    System.out.println(response.readEntity(String.class));

    response.close();

我刚刚创建了一个简单的 Employee 类,其中包含一个用于测试的 idname 字段。这工作得很好。它显示图像,打印内容配置,并打印Employee 对象。

我对 Postman 不太熟悉,所以我把测试留到最后 :-)

它似乎也可以正常工作,您可以看到回复 "Cool Tools"。但是如果我们查看打印的Employee 数据,我们会发现它是空的。这很奇怪,因为客户端 API 可以正常工作。

如果我们查看预览窗口,就会发现问题

emp 正文部分没有 Content-Type 标头。您可以在客户端 API 中看到我明确设置它

MultiPart multipartEntity = new FormDataMultiPart()
        .field("emp", empPartJson, MediaType.APPLICATION_JSON_TYPE)
        .bodyPart(filePart);

所以我想这实际上只是完整答案的部分。就像我说的,我不熟悉邮递员所以我不知道如何为各个身体部位设置Content-Types。图像的image/png 是自动为我设置的图像部分(我猜它只是由文件扩展名决定的)。如果你能弄清楚这一点,那么问题应该得到解决。请,如果您知道如何执行此操作,请将其发布为答案。

请参阅下面的更新以获取解决方案


只是为了完整性......

See here for more about MultiPart with Jersey。

基本配置:

依赖:

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-multipart</artifactId>
    <version>$jersey2.version</version>
</dependency>

客户端配置:

final Client client = ClientBuilder.newBuilder()
    .register(MultiPartFeature.class)
    .build();

服务器配置:

// Create JAX-RS application.
final Application application = new ResourceConfig()
    .packages("org.glassfish.jersey.examples.multipart")
    .register(MultiPartFeature.class);

如果您在服务器配置方面遇到问题,以下帖子之一可能会有所帮助

What exactly is the ResourceConfig class in Jersey 2? 152 MULTIPART_FORM_DATA: No injection source found for a parameter of type public javax.ws.rs.core.Response

更新

从 Postman 客户端可以看出,一些客户端无法设置单个部分的 Content-Type,这包括浏览器,因为它是使用 FormData (js) 时的默认功能。

我们不能指望客户端绕过这个,所以我们可以做的是,在接收数据时,在反序列化之前显式设置 Content-Type。例如

@POST
@Path("upload2")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFileAndJSON(@FormDataParam("emp") FormDataBodyPart jsonPart,
                                  @FormDataParam("file") FormDataBodyPart bodyPart)  
     jsonPart.setMediaType(MediaType.APPLICATION_JSON_TYPE);
     Employee emp = jsonPart.getValueAs(Employee.class);

获取 POJO 需要做一些额外的工作,但它比强迫客户尝试找到自己的解决方案更好。

另一种选择是使用字符串参数并使用您使用的任何 JSON 库将字符串反序列化到 POJO(如 Jackson ObjectMapper)。使用前面的选项,我们只让 Jersey 处理反序列化,它将使用与所有其他 JSON 端点相同的 JSON 库(这可能是首选)。


旁白

如果您使用与默认 HttpUrlConnection 不同的连接器,您可能会对 these comments 中的对话感兴趣。

【讨论】:

您好,麻烦您(或任何其他阅读并知道的人)更新客户端 API 中的代码。例如,第一行有“c.target”,但这里的 c 是什么? @user3223841 请参阅最底部的“客户端配置”。 c == client。我更新了代码。谢谢。 您是否在大文件上测试过您的代码?我在提交表单时收到same 错误 如果可以的话,我会给你 100 票。这就是一个经过充分研究的、最新的、有用的、有变通办法的答案! @AzamatAlmukhametov 用于文件部分,不需要使用 FormDataBodyPart。我想我只是复制和粘贴。您可以将其保留为 InputStream 参数。 JSON 部分只需要 FormDataBodyPart,因为我们需要更改媒体类型。但是对于文件,我们可以只获取原始输入流。另一种方法是使用bodyPart.getValueAs(InputStream.class),但这不是必需的。只需将参数设为InputStream【参考方案2】:

当我使用 Jersey 客户端 2.21.1 尝试 @PaulSamsotha's solution 时,出现 400 错误。当我在客户端代码中添加以下内容时,它起作用了:

MediaType contentType = MediaType.MULTIPART_FORM_DATA_TYPE;
contentType = Boundary.addBoundary(contentType);

Response response = t.request()
        .post(Entity.entity(multipartEntity, contentType));

而不是在 POST 请求调用中硬编码 MediaType.MULTIPART_FORM_DATA

之所以需要这样做是因为当您为 Jersey 客户端使用不同的连接器(如 Apache)时,它无法更改出站标头,这是为 Content-Type 添加边界所必需的。此限制在Jersey Client docs 中进行了说明。所以如果你想使用不同的Connector,那么你需要手动创建边界。

【讨论】:

您不需要手动创建边界。你可以使用multipartEntity.getMediaType()。我最初使用的是MediaType.MULTIPART_FORM_DATA,它没有添加边界。但是当你使用MutliPartgetMediaType()方法时,它有边界。 @PaulSamsotha 如果您使用其他连接器(即 apache 而不是 jdk),则不会为您生成边界。 @Ben 你是对的。在文档的某处,它说明了不同的连接器,其中 WriterInterceptors 和 MessageBodyWriters 无法更改出站标头。这将是添加边界所必需的。请参阅客户端文档中的 the warning here。 所以要么自动生成边界,但没有分块上传(HttpUrlConnection 在这方面是假的)或手动边界生成。我选择了后者,因为在不警告用户的情况下缓冲 4 GiB 文件是不行的。我在他们的 github 跟踪器上创建了一个问题。 New link for docs from my comment above【参考方案3】:

请求类型是 multipart/form-data 并且您发送的本质上是表单字段,它们以字节形式发出,内容边界分隔不同的表单字段。要将对象表示作为表单字段(字符串)发送,您可以发送来自客户端的序列化表单,然后您可以在服务器上反序列化。

毕竟没有编程环境对象实际上是在线上传输的。双方的编程环境只是在做你也可以做的自动序列化和反序列化。这是最干净且编程环境无怪异的方式。

例如,这是一个发布到 Jersey 示例服务的 javascript 客户端,

submitFile()

    let data = new FormData();
    let account = 
        "name": "test account",
        "location": "Bangalore"
    

    data.append('file', this.file);
    data.append("accountKey", "44c85e59-afed-4fb2-884d-b3d85b051c44");
    data.append("device", "test001");
    data.append("account", JSON.stringify(account));
    
    let url = "http://localhost:9090/sensordb/test/file/multipart/upload";

    let config = 
        headers: 
            'Content-Type': 'multipart/form-data'
        
    

    axios.post(url, data, config).then(function(data)
        console.log('SUCCESS!!');
        console.log(data.data);
    ).catch(function()
        console.log('FAILURE!!');
    );
,

这里客户端发送一个文件、2 个表单字段(字符串)和一个已字符串化以便传输的帐户对象。这是表单域在网络上的样子,

在服务器上,您可以按照您认为合适的方式反序列化表单字段。为了完成这个简单的例子,

    @POST
@Path("/file/multipart/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultiPart(@Context ContainerRequestContext requestContext,
        @FormDataParam("file") InputStream fileInputStream,
        @FormDataParam("file") FormDataContentDisposition cdh,
        @FormDataParam("accountKey") String accountKey,
        @FormDataParam("account") String json)  
    

    
    System.out.println(cdh.getFileName());
    System.out.println(cdh.getName());
    System.out.println(accountKey);
    
    try 
        Account account = Account.deserialize(json);
        System.out.println(account.getLocation());
        System.out.println(account.getName());
        
     catch (Exception e) 
        e.printStackTrace();
    
    
    return Response.ok().build();
    

【讨论】:

【参考方案4】:

您可以使用以下代码使用 MULTIPART FORM DATA 从表单访问图像文件和数据。

@POST
@Path("/UpdateProfile")
@Consumes(value=MediaType.APPLICATION_JSON,MediaType.MULTIPART_FORM_DATA)
@Produces(value=MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML)
public Response updateProfile(
    @FormDataParam("file") InputStream fileInputStream,
    @FormDataParam("file") FormDataContentDisposition contentDispositionHeader,
    @FormDataParam("ProfileInfo") String ProfileInfo,
    @FormDataParam("registrationId") String registrationId) 

    String filePath= "/filepath/"+contentDispositionHeader.getFileName();

    OutputStream outputStream = null;
    try 
        int read = 0;
        byte[] bytes = new byte[1024];
        outputStream = new FileOutputStream(new File(filePath));

        while ((read = fileInputStream.read(bytes)) != -1) 
            outputStream.write(bytes, 0, read);
        

        outputStream.flush();
        outputStream.close();
     catch (FileNotFoundException e) 
        e.printStackTrace();
     catch (IOException e) 
        e.printStackTrace();
     finally 
        if (outputStream != null)  
            try 
                outputStream.close();
             catch(Exception ex) 
        
    

【讨论】:

【参考方案5】:

我使用的文件上传示例来自,

http://www.mkyong.com/webservices/jax-rs/file-upload-example-in-jersey/

在我的资源类中,我有以下方法

@POST
    @Path("/upload")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response  attachupload(@FormDataParam("file") byte[] is,
@FormDataParam("file") FormDataContentDisposition fileDetail,
@FormDataParam("fileName") String flename)
attachService.saveAttachment(flename,is);

在我的 attachService.java 中我有以下方法

 public void saveAttachment(String flename,  byte[] is) 
            // TODO Auto-generated method stub
         attachmentDao.saveAttachment(flename,is);

        

在道我有

attach.setData(is);
attach.setFileName(flename);

在我的 HBM 映射中是这样的

<property name="data" type="binary" >
            <column name="data" />
</property>

这适用于所有类型的文件,例如 .PDF、.TXT、.PNG 等,

【讨论】:

你刚刚用 byte[] 参数而不是 InputStream 救了我的命...@Ramam 我欠你一杯啤酒!! 如何在客户端进行测试?【参考方案6】:

您的 ApplicationConfig 应该从 glassfish.jersey.media.. 注册 MultiPartFeature.class 以便启用文件上传

@javax.ws.rs.ApplicationPath(ResourcePath.API_ROOT)
public class ApplicationConfig extends ResourceConfig   
public ApplicationConfig() 
        //register the necessary headers files needed from client
        register(CORSConfigurationFilter.class);
        //The jackson feature and provider is used for object serialization
        //between client and server objects in to a json
        register(JacksonFeature.class);
        register(JacksonProvider.class);
        //Glassfish multipart file uploader feature
        register(MultiPartFeature.class);
        //inject and registered all resources class using the package
        //not to be tempered with
        packages("com.flexisaf.safhrms.client.resources");
        register(RESTRequestFilter.class);
    

【讨论】:

以上是关于与 Jersey RESTful Web 服务中的其他对象一起上传文件的主要内容,如果未能解决你的问题,请参考以下文章

AJAX POST To Jersey 启用 RESTful Web 服务跨域

使用 Jersey 和 Spring Security 实现 RESTful Web 服务

使用 Jersey 的 RESTful Web 服务的会话管理

使用 Jersey 和 Apache Tomcat 构建 RESTful Web 服务

使用 Spring Security 和 Jersey Restful Web 服务进行登录身份验证

如何使用 Jersey API 从 RESTful Web 服务发送和接收 JSON 数据