如何通用化 Apache HttpClient POST 参数
结论
不要把 List<NameValuePair> 直接作为通用参数模型。建议按下面的方式拆分:
- 用
Map<String, Object>、DTO 或自定义参数树表示业务参数; - JSON 请求由 Gson 递归序列化;
- 表单请求通过单独的扁平化转换器生成
List<NameValuePair>; - 每个支付网关使用各自的适配器,处理字段命名、数组格式、空值和签名规则。
这样,一组参数既可以生成 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()、异常信息和调试日志也不能泄露这些字段。处理真实银行卡数据时,还要遵守支付服务商的安全要求和适用的合规规范。
备注:内容仅供参考。
