上周五下午三点,我在对接一个第三方支付平台的时候,差点把键盘砸了。事情是这样的,对方提供了一份号称”十分钟上手”的API文档,我按照示例调了个查询余额的接口,结果返回了一堆乱码。我当时的第一反应是编码问题,但试了UTF-8、GBK、ISO-8859-1全都不对。后来用Postman直接请求,发现响应里混着几个不可见字符,才意识到对方返回的数据是经过Gzip压缩的——文档里一个字没提。
这个坑踩得我挺憋屈,因为我的代码里根本没有处理响应压缩的逻辑。Java这边用Apache HttpClient 4.5.13,默认是不会自动解压Gzip的。解决方案其实很简单,给HttpClient加个拦截器:
“`java
CloseableHttpClient httpClient = HttpClients.custom()
.addInterceptorFirst((HttpRequestInterceptor) (request, context) -> {
if (!request.containsHeader(“Accept-Encoding”)) {
request.addHeader(“Accept-Encoding”, “gzip”);
}
})
.addInterceptorFirst((HttpResponseInterceptor) (response, context) -> {
HttpEntity entity = response.getEntity();
if (entity != null && entity.getContentEncoding() != null) {
HeaderElement[] codecs = entity.getContentEncoding().getElements();
for (HeaderElement codec : codecs) {
if (“gzip”.equalsIgnoreCase(codec.getName())) {
response.setEntity(new GzipDecompressingEntity(entity));
return;
}
}
}
})
.build();
“`
但问题没完。第二天联调的时候,对方说签名校验一直失败。我拿着自己的签名算法反复核对,SHA256withRSA,私钥格式也确认是PKCS8,但就是过不去。后来我把自己的请求报文和签名值发给对方,让他们在服务端打印日志,发现他们收到的报文里多了个换行符。原来我组装请求体的时候,用的是StringBuilder,结尾习惯性加了个`\n`。去掉之后签名就通过了。这个真的不怪文档,怪我自己的编码习惯,但你们说谁写代码还没点小癖好呢。
更离谱的是第三个问题。接口文档里写明了请求参数`orderId`是字符串类型,长度不超过32位。我传了个”ORDER20241201_001″,长度27位,没问题。但对方返回的响应里,`orderId`变成了”ORDER20241201_00″。他们服务端做了截断处理,把超过20位的自动截了。文档里只说了32位限制,没提到实际存储字段是varchar(20)。这个我后来是通过抓包对比请求和响应才发现的。解决办法是改自己的订单号生成规则,控制在20位以内。
还有一次,接口返回了HTTP 200,但业务状态码是”FAIL”,错误信息是”参数校验异常”。我把所有必填参数都传了,格式也检查过,就是不知道哪个参数有问题。找对方技术支持,他们给了一个curl示例,我一行一行对比才发现,他们接口里有个参数叫`extraInfo`,文档里写的是”可选”,但实际逻辑里如果没有传这个参数,会触发一个隐藏的校验规则。最后传了个空JSON对象`{}`才通过。这种”可选参数但实际必传”的坑,真的很让人抓狂。
再说一个关于重试的问题。有一次调用退款接口,网络超时了,我加了重试逻辑,用的是Spring Retry的`@Retryable`注解,配置了最多重试3次,间隔200毫秒。结果第2次重试成功了,但对方处理了两次退款。后来才知道对方的接口不是幂等的,退款请求如果重复提交,会生成多笔退款单。我后来把重试策略改成了”先查询再重试”——超时之后先调查询接口确认状态,如果没处理再重新提交。代码大概是这样:
“`java
@Retryable(value = TimeoutException.class, maxAttempts = 3, backoff = @Backoff(delay = 500))
public RefundResult refund(RefundRequest request) {
try {
return refundClient.refund(request);
} catch (TimeoutException e) {
RefundQueryResult queryResult = refundClient.query(request.getRefundId());
if (queryResult != null && “SUCCESS”.equals(queryResult.getStatus())) {
return new RefundResult(queryResult);
}
throw e;
}
}
“`
这样至少保证了在超时场景下,不会无脑重复提交。
总结一下这几个坑的经验吧。第一,对接API之前,先花10分钟用curl或者Postman把核心接口调通,确认返回格式和编码。第二,签名相关的问题,让对方服务端打日志是最快的排查方式,不要自己瞎猜。第三,文档里的字段长度、类型、可选必选,都只能信一半,最好自己抓包验证。第四,重试逻辑一定要考虑幂等性,不确定的话就加个去重或者先查询后提交。第五,遇到很诡异的错误,先把HTTP响应体完整打印出来,很多时候错误信息就藏在里面,只是你解析的时候忽略了。
写这篇的时候我还在处理另一个对接,对方接口返回的JSON里有个字段叫`data`,有时候是对象,有时候是数组。我打算写个自定义的反序列化器来处理这个,但今天先到这吧,改天再聊。