PAYATHON 2026

如何通用化 Apache HttpClient POST 参数

支付阿杰

结论

不要把 List<NameValuePair> 直接作为通用参数模型。建议按下面的方式拆分:

  1. 用 Map<String, Object>、DTO 或自定义参数树表示业务参数;
  2. JSON 请求由 Gson 递归序列化;
  3. 表单请求通过单独的扁平化转换器生成 List<NameValuePair>;
  4. 每个支付网关使用各自的适配器,处理字段命名、数组格式、空值和签名规则。

这样,一组参数既可以生成 JSON,也可以转换成 card[name]=some name 这类表单字段。

通用参数模型

如果不同支付网关的参数结构差别较大,可以先用 Map<String, Object>:

Map<String, Object> params = new LinkedHashMap<>();
params.put("name", "test");
params.put("email", "testemail@domain.com");

Map<String, Object> card = new LinkedHashMap<>();
card.put("name", "some name");
card.put("no", "14444444444");

params.put("card", card);

这里选择 LinkedHashMap,是为了在必要时保留字段的插入顺序。某些支付网关生成签名时可能依赖参数顺序,不过具体应采用哪种顺序,仍要以网关协议为准。

这组参数可以表示成 JSON:

{
  "name": "test",
  "email": "testemail@domain.com",
  "card": {
    "name": "some name",
    "no": "14444444444"
  }
}

也可以展开成表单字段:

name=test
email=testemail@domain.com
card[name]=some name
card[no]=14444444444

转换为 JSON

Gson 可以直接序列化嵌套的 Map:

Gson gson = new Gson();
String json = gson.toJson(params);

HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(
    new StringEntity(json, ContentType.APPLICATION_JSON)
);

使用 Apache HttpClient 4.x 时,也可以显式指定 UTF-8:

HttpPost httpPost = new HttpPost(url);

String json = new Gson().toJson(params);
StringEntity entity = new StringEntity(
    json,
    ContentType.APPLICATION_JSON
);

httpPost.setEntity(entity);

Gson 会递归处理 Map、集合、数组、数字、布尔值以及普通 Java 对象。

转换为表单参数

application/x-www-form-urlencoded 只能表示键值对,并没有统一的嵌套对象标准。因此,需要按照目标支付网关的约定,把参数树展开成字段列表。

下面的转换器采用常见的方括号格式:

import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public final class FormParameterConverter {

    private FormParameterConverter() {
    }

    public static List<NameValuePair> convert(Map<String, ?> params) {
        List<NameValuePair> result = new ArrayList<>();

        for (Map.Entry<String, ?> entry : params.entrySet()) {
            flatten(entry.getKey(), entry.getValue(), result);
        }

        return result;
    }

    private static void flatten(
            String key,
            Object value,
            List<NameValuePair> result) {

        if (value == null) {
            // 空值策略需要根据支付网关协议决定:
            // 可以忽略,也可以提交为空字符串。
            return;
        }

        if (value instanceof Map<?, ?>) {
            Map<?, ?> map = (Map<?, ?>) value;

            for (Map.Entry<?, ?> entry : map.entrySet()) {
                String childKey = key + "[" + entry.getKey() + "]";
                flatten(childKey, entry.getValue(), result);
            }
            return;
        }

        if (value instanceof Iterable<?>) {
            for (Object item : (Iterable<?>) value) {
                flatten(key + "[]", item, result);
            }
            return;
        }

        if (value.getClass().isArray()) {
            int length = Array.getLength(value);

            for (int i = 0; i < length; i++) {
                flatten(key + "[]", Array.get(value, i), result);
            }
            return;
        }

        result.add(new BasicNameValuePair(key, String.valueOf(value)));
    }
}

使用方式如下:

List<NameValuePair> postParams =
    FormParameterConverter.convert(params);

HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(
    new UrlEncodedFormEntity(postParams, StandardCharsets.UTF_8)
);

编码前,生成的表单内容相当于:

name=test
email=testemail@domain.com
card[name]=some name
card[no]=14444444444

实际发送时,UrlEncodedFormEntity 会执行 URL 编码。例如,方括号和空格可能被编码成:

card%5Bname%5D=some+name

这是正常的。服务端解码后,得到的字段名仍然是 card[name]。

封装统一请求参数接口

为了让业务代码不直接依赖 Gson 或 Apache HttpClient,可以把这些转换操作封装起来:

import com.google.gson.Gson;
import org.apache.http.NameValuePair;

import java.util.List;
import java.util.Map;

public final class GatewayParameters {

    private final Map<String, Object> values;

    public GatewayParameters(Map<String, Object> values) {
        this.values = values;
    }

    public Map<String, Object> asMap() {
        return values;
    }

    public String toJson(Gson gson) {
        return gson.toJson(values);
    }

    public List<NameValuePair> toFormParameters() {
        return FormParameterConverter.convert(values);
    }
}

调用方只需构造一次参数:

GatewayParameters parameters = new GatewayParameters(params);

if (gatewayUsesJson) {
    httpPost.setEntity(
        new StringEntity(
            parameters.toJson(new Gson()),
            ContentType.APPLICATION_JSON
        )
    );
} else {
    httpPost.setEntity(
        new UrlEncodedFormEntity(
            parameters.toFormParameters(),
            StandardCharsets.UTF_8
        )
    );
}

如果以后改用其他 HTTP 客户端,只需增加相应的转换器,不必改动业务参数模型。

参数固定时优先使用 DTO

对于结构稳定并且需要校验的支付请求,DTO 通常比任意 Map 更安全:

public final class PaymentRequest {

    private final String name;
    private final String email;
    private final Card card;

    public PaymentRequest(String name, String email, Card card) {
        this.name = name;
        this.email = email;
        this.card = card;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public Card getCard() {
        return card;
    }
}
public final class Card {

    private final String name;
    private final String no;

    public Card(String name, String no) {
        this.name = name;
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public String getNo() {
        return no;
    }
}

Gson 可以直接序列化 DTO。转换表单时,可以先把 DTO 转成参数树,也可以为每个支付网关编写明确的映射器:

public List<NameValuePair> toForm(PaymentRequest request) {
    List<NameValuePair> result = new ArrayList<>();

    result.add(new BasicNameValuePair("name", request.getName()));
    result.add(new BasicNameValuePair("email", request.getEmail()));
    result.add(new BasicNameValuePair(
        "card[name]",
        request.getCard().getName()
    ));
    result.add(new BasicNameValuePair(
        "card[no]",
        request.getCard().getNo()
    ));

    return result;
}

支付接口往往对字段名、必填项和签名流程有严格要求,因此核心请求适合使用 DTO 和显式映射。Map<String, Object> 则更适合扩展字段,或者结构差异较大的网关协议。

需要特别注意的问题

嵌套表单没有统一标准

不同服务端可能采用下面任意一种格式:

card[name]=some name
card.name=some name
card_name=some name

数组也有多种表示方式:

items[]=a&items[]=b
items[0]=a&items[1]=b
items=a&items=b

所以,方括号格式并不是所有支付网关都支持的通用标准。转换器需要允许每个网关覆盖字段路径和数组编码策略。

不要过早把所有值转换成字符串

JSON 中的数字和布尔值应保留原有类型:

params.put("amount", 100);
params.put("capture", true);

生成的 JSON 是:

{
  "amount": 100,
  "capture": true
}

如果一开始就把所有值保存为 String,结果会变成:

{
  "amount": "100",
  "capture": "true"
}

这可能不符合接口要求。应当等到转换表单字段时,再调用 String.valueOf()。

明确空值处理规则

空值至少有以下几种处理方式:

  • 不发送该字段;
  • 发送空字符串;
  • 在 JSON 中发送 null;
  • 在表单中发送字符串 "null"。

这些方式的含义并不相同。Gson 默认可能忽略对象中的部分 null 字段。如果接口要求输出 null,需要根据当前使用的 Gson 配置显式处理。表单转换器同样应提供可配置的空值策略。

签名应基于网关规定的规范化结果

支付网关的签名规则可能要求:

  • 按字段名排序;
  • 排除空值;
  • 排除签名字段;
  • 使用编码前或编码后的值;
  • 按指定方式展开嵌套字段。

因此,不能直接假定 Gson 的输出或 UrlEncodedFormEntity 生成的最终字符串可以用于签名。应先按照网关要求生成规范化参数,再分别用于签名和发送。

避免记录敏感支付信息

银行卡号、CVV、访问令牌和签名密钥不应写入普通日志。参数模型的 toString()、异常信息和调试日志也不能泄露这些字段。处理真实银行卡数据时,还要遵守支付服务商的安全要求和适用的合规规范。

备注:内容仅供参考。