You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Quarkus REST Client向Trello API发送带二进制文件的multipart/form-data

解决Quarkus中向Trello卡片发送二进制图片附件的问题

问题概述

正在将旧Java软件迁移至基于RESTEasy Reactive/MicroProfile的Quarkus框架,需实现向Trello卡片添加图片的功能。要求发送包含name、filename、mimeType、setCover及二进制文件的multipart/form-data,核心要求是文件以二进制形式发送(Trello官方文档存在疏漏,不可完全采信)。

可正常运行的旧代码

旧Java代码通过JAX-RS Client实现功能:

String name = "Cover.jpg";
File tempImage = new File(name);
try {
    Client client = ClientBuilder.newClient(new ClientConfig())
            .register(MultiPartFeature.class);

    String mime = "image/jpg";

    byte[] buffer = Files.readAllBytes(Paths.get("/PATH/" + name));

    // 保存临时文件
    FileOutputStream fos = new FileOutputStream(tempImage);
    fos.write(buffer);
    fos.flush();
    fos.close();

    FileDataBodyPart filePart = new FileDataBodyPart("file", tempImage);

    FormDataMultiPart formDataMultiPart = new FormDataMultiPart();
    final FormDataMultiPart multipart = (FormDataMultiPart) formDataMultiPart
            .field("mimeType", mime)
            .field("setCover", "true").field("name", name).bodyPart(filePart);

    // 添加封面
    WebTarget coverService = client.target(URI.create("https://api.trello.com/1/cards/CARD_ID/attachments"));
    String coverResponse = coverService.queryParam("key", "KEY").queryParam("token", "TOKEN").request()
            .header("Accept", "application/json")
            .post(Entity.entity(multipart, multipart.getMediaType()), String.class);
} catch (Exception e) {
    e.printStackTrace();
}

旧代码生成的请求示例

1 * Sending client request on thread main
1 > POST https://api.trello.com/1/cards/CARD_ID/attachments?key=KEY&token=TOKEN
1 > Accept: application/json
1 > Content-Type: multipart/form-data
--Boundary_1_2089589253_1678121120116
Content-Type: text/plain
Content-Disposition: form-data; name="mimeType"

image/jpg
--Boundary_1_2089589253_1678121120116
Content-Type: text/plain
Content-Disposition: form-data; name="setCover"

true
--Boundary_1_2089589253_1678121120116
Content-Type: text/plain
Content-Disposition: form-data; name="name"

Cover.jpg
--Boundary_1_2089589253_1678121120116
Content-Type: image/jpeg
Content-Disposition: form-data; filename="Cover.jpg"; modification-date="Mon, 06 Mar 2023 16:45:19 GMT"; size=105034; name="file"

<BINARY! content>
--Boundary_1_2089589253_1678121120116--

尝试过的失败Quarkus代码

REST Client接口

@Path(ADD_ATTACHMENT_CARD_URL)
@POST
@javax.ws.rs.Consumes(MediaType.MULTIPART_FORM_DATA)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@ClientHeaderParam(name = "Accept", value = MediaType.APPLICATION_JSON)
@ClientHeaderParam(name = "User-Agent", value = "FawnKeeper")
public void setCover(@QueryParam("key") String key, @QueryParam("token") String token, @PathParam("id") String id,@MultipartForm MultipartBody file);

MultipartBody类

public class MultipartBody implements Serializable {

    @PartType(MediaType.TEXT_PLAIN)
    public String mimeType = "image/jpg";

    @PartType(MediaType.TEXT_PLAIN)
    public String setCover = "false";

    @PartType(MediaType.TEXT_PLAIN)
    public String name = "Cover.jpg";

    @PartType("image/jpeg")
    public File file;
}

核心问题:文件被转为Base64而非二进制发送,且存在转换器错误。


可行解决方案

方案一:修复REST Client的Multipart处理

问题根源在于使用File类型会被默认序列化为Base64,改用InputStream并补充必要注解即可解决:

1. 修改MultipartBody类

import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.annotations.providers.multipart.PartFilename;
import org.jboss.resteasy.annotations.providers.multipart.PartType;

import java.io.InputStream;
import java.io.Serializable;

public class MultipartBody implements Serializable {

    @FormParam("mimeType")
    @PartType(MediaType.TEXT_PLAIN)
    public String mimeType = "image/jpg";

    @FormParam("setCover")
    @PartType(MediaType.TEXT_PLAIN)
    public String setCover = "true";

    @FormParam("name")
    @PartType(MediaType.TEXT_PLAIN)
    public String name = "Cover.jpg";

    @FormParam("file")
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    @PartFilename("Cover.jpg") // 指定文件名
    public InputStream fileInputStream;
}

2. 修正REST Client接口

移除重复的@Consumes注解,确保参数绑定正确:

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;

@RegisterRestClient(baseUri = "https://api.trello.com/1")
public interface TrelloClient {

    String ADD_ATTACHMENT_CARD_URL = "/cards/{id}/attachments";

    @Path(ADD_ATTACHMENT_CARD_URL)
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @ClientHeaderParam(name = "Accept", value = MediaType.APPLICATION_JSON)
    @ClientHeaderParam(name = "User-Agent", value = "FawnKeeper")
    void setCover(@QueryParam("key") String key, 
                  @QueryParam("token") String token, 
                  @PathParam("id") String id,
                  @MultipartForm MultipartBody requestBody);
}

3. 使用示例

import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import java.io.FileInputStream;
import java.io.IOException;

public class TrelloService {

    @Inject
    @RestClient
    TrelloClient trelloClient;

    public void addCoverToCard(String cardId, String apiKey, String apiToken, String filePath) throws IOException {
        MultipartBody body = new MultipartBody();
        body.setCover = "true";
        body.fileInputStream = new FileInputStream(filePath);
        
        trelloClient.setCover(apiKey, apiToken, cardId, body);
    }
}

方案二:使用Vert.x Web Client实现

如果REST Client方案仍有问题,可直接用Vert.x Web Client手动构建multipart请求,确保二进制发送:

import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.multipart.FormData;
import jakarta.inject.Inject;

import java.nio.file.Files;
import java.nio.file.Paths;

public class VertxTrelloService {

    @Inject
    Vertx vertx;

    public void addCoverToCard(String cardId, String apiKey, String apiToken, String filePath) throws Exception {
        WebClient client = WebClient.create(vertx);
        
        // 读取文件为二进制Buffer
        byte[] imageBytes = Files.readAllBytes(Paths.get(filePath));
        Buffer imageBuffer = Buffer.buffer(imageBytes);

        // 构建multipart表单
        FormData formData = FormData.create()
                .text("mimeType", "image/jpg")
                .text("setCover", "true")
                .text("name", "Cover.jpg")
                .binary("file", imageBuffer, "Cover.jpg", "image/jpeg");

        // 发送请求
        client.postAbs("https://api.trello.com/1/cards/" + cardId + "/attachments")
                .addQueryParam("key", apiKey)
                .addQueryParam("token", apiToken)
                .putHeader("Accept", "application/json")
                .putHeader("User-Agent", "FawnKeeper")
                .sendMultipartForm(formData, ar -> {
                    if (ar.succeeded()) {
                        System.out.println("附件添加成功:" + ar.result().bodyAsString());
                    } else {
                        ar.cause().printStackTrace();
                    }
                });
    }
}

内容的提问来源于stack exchange,提问作者J. Reisner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 07:24:59