大家好,我是橘长,一个 Java 工程师。
前段时间看了一篇同行写的对接第三方的文章,那篇“专注于如何教你对接”这个过程,橘长觉得有一些更好的补充和思考,因此写下这篇长文,感兴趣可以 wx 搜索:橘长说 Java。
一、闲聊第三方
例举两个比较常见的功能,一是根据号码发送验证码,二是微信授权、支付等功能,第三方其实是简称,更准确的来描述应该是第三方服务提供商提供的能力 Api 接口。
一)为什么会存在第三方功能这类供应商?
本质上是市场有相关需求,客户花钱买服务,做服务聚合商,像上述罗列的短信功能属于业务侧功能性需求,作为一家 IT 公司它面对这类功能可以有两种选择,一是对接运营商去签订相关合同,二是找一家封装了短信服务的公司。
目前橘长经历比较多的都是选项二,找一家有能力的公司,他们向上对客户侧提供短信能力,向下封装和运营商交互的相关麻烦细节,抽佣。
二)供应商提供的 Api 能力
通常来说,一般调用服务商的 Api 都是服务端通信行为,很少会让客户端直接发起请求到供应商 Api 的相关操作,为啥呢,安全角度考虑。
对于供应商而言,他们的服务和客户是一对多的关系,那就需要做好客户密钥等信息的相关管理,对于客户端而言,只需要专注于 api 交互这段逻辑即可,今天我们主要是来分享客户端这块,供应商那块需要做客户相关信息管理,可以自行做相关思考即可。
二、第三方关注点和注意项
一)关注点
1、服务提供商
这里有一条指导原则:文档是写给人看的,而不是写给机器看的。
因此对于服务提供商的一个注意点是文档撰写,稍微好点团队会做一个静态的内容聚合页,比方说企业微信的相关文档:https://developer.work.weixin.qq.com/document/path/91201,
稍微一般的团队会撰写维护一个在线文档,比方说语雀知识库、石墨这些。
更差的团队做的是什么呢?写 word 文档,没有什么版本管理的思想,往往是不同的方式导致沟通成本相差巨大的问题。
写文档有两个目的,一是告知外界我们技术团队是专业的,在文档里面把 api 的入参出参详细描述,版本变更、交互时序图都清晰记录,二是从客户角度,考虑一下他们的用户体验,诸多客户怨声哀道说文档写的差,实际参数和响应和文档相差巨大,实在是想不通为什么这样他们还不重视这块文档。
最常见的理由就是:我们服务提供商开发团队很忙,没空维系那些,沟通这些时间就是恶果,宁愿慢一点,也请不要这块乱来好吧。
2、服务接入者
首要当然是读文档,所以从客户体验角度,服务提供商是不是得写好文档,看文档通常关注两块,一块是 API 交互认证问题,二是数据传输的加解密问题。
API 交互认证问题:解决的是互相认证,也就是你是谁的问题,是否是我们备案过的信任客户等。
数据传输的加解密问题:解决的是数据安全性问题,是否被纂改等,加密算法用对称还是非对称等细节问题。
二)注意项
本文主要是写服务接入这块,因此注意项也会专注接入这块,接入得有逻辑,其实就是讲规矩,看人服务提供商要求的步骤一步一步处理即可。
比方说:先备案申请填写相关资料、然后获取服务供应商颁发的相关配置、使用 postman 等 api 工具先尝试看网络是否通畅、接下来才是写代码去对接这快、然后再整合进业务侧。
如下分享一些代码侧的注意事项。
1、抽离和第三方交互的代码
以下用对接阿里的短信服务为例罗列相关代码。
1)自定义一个 AliApiServerException 的异常
/**
* AliAPIServerException
*
* @author AFlymamba
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AliApiServerException extends Exception {
private static final long serialVersionUID = -3064220748468350413L;
String message;
}
复制代码
注意:这里继承的是 Exception,而不是常规推荐的 RuntimeException。
2)构建一个 AliApiServerService
/**
* AliApiServerService
* <p>
* bad response:result=0&errMsg=xxx
*
* @author AFlymamba
*/
public interface AliApiServerService {
/**
* Send Single Message
*
* @param phone Mobile
* @param content content
* @return send result
* @throws AliApiServerException Server Exception
*/
JSONObject sendSingleMessage(String phone, String content) throws AliApiServerException;
}
复制代码
更好的处理方式其实是方法入参推荐用对象,出参用 JSON,但很多第三方 api 乱搞,他们直接返回一个非 json 格式,直接给 String,上述代码注释里面给了一个糟糕示例,出于尊重,这里统一用 json,然后在方法处声明可能会抛出一个自定义异常。
3)构建 AliApiServiceImpl
/**
* AliApiServerServiceImpl
*
* @author AFlymamba
*/
@Slf4j
@Service
public class AliApiServerServiceImpl implements AliApiServerService {
@Override
public JSONObject sendSingleMessage(final String phone, final String content) throws AliApiServerException {
// ignore phone、content check
String requestUrl = StrBuilder.create("http://xxx").append("/api/v1/xxx").toString();
JSONObject requestBody = JSONUtil.createObj().putOnce("phone", phone).putOnce("content", content);
// ignore header、encrypt、sign
try {
String responseBody = HttpRequest.post(requestUrl).body(requestBody.toString()).timeout(3000).execute().body();
log.info("[ AliApiServer ] sendSingleMessage,requestUrl is [{}],requestBody is [{}], origin response is [{}]", requestUrl, requestBody, responseBody);
if (JSONUtil.isJson(responseBody)) {
return JSONUtil.parseObj(responseBody);
}
} catch (Exception e) {
log.error("[ AliApiServer ] sendSingleMessage exception,requestUrl is [{}],requestBody is [{}],exception message is [{}],cause is [{}]", requestUrl, requestBody, e.getMessage(), e.getCause());
}
throw new AliApiServerException("AliApiServerException, please check log......");
}
}
复制代码
以上代码图省事就把相关校验去掉了,同样获取第三方服务的 url 等参数也采用了静态化方式,纯偷懒,更合理做法当然是 load from config or db。同样 log print 那块也有点问题,并未打印一个唯一标识,比方说服务商 id、用户 id 等信息,橘长给你们做了一个错误示例,反面教材。
2、业务侧和第三方交互代码的交互
上述和第三方交互的代码完成之后,其实我们已经将第三方的能力整合起来用接口的形式对外暴露能力了,接下来我们来做一些业务侧和第三方交互的演示以及优化。
以下存在三块代码,分别是 ActivityServiceImpl、MessageServiceImpl、AliApiServiceImpl,简单解释以下,Activity 那个属于业务点,Message 这个属于中间层过渡点,AliApi 这个属于和第三方交互的点,以下代码为了做演示,忽略了很多规范问题,谅解。
1)业务点和第三方交互点直接调用
/**
* ActivityServiceImpl
*
* @author AFlymamba
*/
@Slf4j
@Service
public class ActivityServiceImpl implements ActivityService {
private final AliApiServerService aliApiServerService;
@Autowired
public ActivityServiceImpl(
AliApiServerService aliApiServerService
) {
this.aliApiServerService = aliApiServerService;
}
@Override
public void sendMessage(final String phone, final String content) {
// ignore
try {
JSONObject sendResult = aliApiServerService.sendSingleMessage(phone, content);
} catch (AliApiServerException e) {
// ignore
}
}
}
复制代码
相当于 ActivityServiceImpl 直接耦合了 AliApiServerService。
2)引入 MessageServiceImpl 来解耦方式一的那种耦合点
@Slf4j
@Service
public class MessageServiceImpl extends MessageService {
private final AliApiServerService aliApiServerService;
@Autowired
public MessageServiceImpl(
AliApiServerService aliApiServerService
) {
this.aliApiServerService = aliApiServerService;
}
@Override
public void sendMessage(final String phone, final String content) {
// ignore
try {
JSONObject sendResult = aliApiServerService.sendSingleMessage(phone, content);
} catch (AliApiServerException e) {
// ignore
}
}
}
// 上述方式一中代码调整
@Slf4j
@Service
public class ActivityServiceImpl implements ActivityService {
private final MessageService messageService;
@Autowired
public ActivityServiceImpl(
MessageService messageService
) {
this.messageService = messageService;
}
@Override
public void sendMessage(final String phone, final String content) {
// ignore
try {
JSONObject sendResult = messageService.sendSingleMessage(phone, content);
} catch (AliApiServerException e) {
// ignore
}
}
}
复制代码
这样做的好处是解开方式一种 ActivityService 和 AliApiService 的直接耦合,变成间接耦合,通过引入 MessageService 这么一个中间层的操作,代码是不是稍微看起来好看一点。还有一个很关键的点是可以开始做异步处理了,在 MessageService 可以通过 @Async 异步注解来做异步。
3)借助发布事件机制
上述方式二解决了方式一的直接耦合,且可以引入异步处理机制,当然是好的,但还可以更好,比方说把这层间接耦合也给拆掉,怎么做,如果是单机的话,可以考虑 Spring 的发布事件机制。
/**
* MessageSendEvent
*
* @author AFlymamba
*/
@Getter
public class MessageSendEvent extends ApplicationEvent {
private static final long serialVersionUID = 6456904953414622941L;
private JSONObject messageData;
public MessageSendEvent(final Object source, final JSONObject messageData) {
super(source);
this.messageData = messageData;
}
}
复制代码
Event 类中持有一个 JSONObject 的属性,更好做法当然不用 k-v 这种指向类型不明确的对象,这里纯属图快做展示。
@Slf4j
@Service
public class ActivityServiceImpl implements ActivityService {
private final ApplicationContext applicationContext;
@Autowired
public ActivityServiceImpl(
ApplicationContext applicationContext
) {
this.applicationContext = applicationContext;
}
@Override
public void sendMessage(final String phone, final String content) {
// ignore
JSONObject messageData = JSONUtil.createObj().putOnce("phone", phone).putOnce("content", content);
MessageSendEvent messageSendEvent = new MessageSendEvent(this, messageData);
applicationContext.publishEvent(messageSendEvent);
}
}
复制代码
可以很明显的发现,我们将业务调用点和第三方那些代码解耦的算比较干净了,利用 Spring 的 publish-event 机制,这一部分是生产者逻辑。
@EventListener
public void consumeMessage(final MessageSendEvent messageSendEvent) {
// ignore
try {
JSONObject messageData = messageSendEvent.getMessageData();
JSONObject sendResult = aliApiServerService.sendSingleMessage(messageData.getStr("phone"), messageData.getStr("content"));
} catch (AliApiServerException e) {
// ignore
}
}
复制代码
依托 @EventListener 注解来实现消费者,通过 event 可以获取到相关业务数据,进而走接下来的逻辑,注意,这类方式也支持异步。
4)打开你的脑洞
演变到第三种就没问题了吗?请注意,这里前面的代码都是基于单机,如果是多机怎么办?这类机制还适用吗?对于生产者消费者模型而言,幂等如何做?扩容好处理吗?一大堆的问题在等着。
当然呢,自然还存在很多其他优秀的解决方案与技术,专业的 Message Queue、线程池等,就看你是否有兴趣继续往下面玩而已,橘长邀请你继续探索下去。
三、建议写点对接第三方代码的理由
这块代码看似简单,但细化起来很多值得参考的技术点,比方说缓存处理机制、异常处理机制、业务与第三方交互逻辑等,还是值得深入练一下这块代码能力的。
缓存处理:这块业务主要用于什么呢?缓存对方 api 接口的访问凭据,但如果采用的是双向验签的方式就不适用了,但也可以缓存什么呢?缓存第三方的相关配置,至于说做本地缓存还是做分布式缓存,依据具体业务而定。
异常处理机制:遵循的原则其实是『 对任何第三方应当保持不可信原则 』,在很多经典书都能看到这番话的,最简单的第三方超时进而导致业务执行失败,通过异常捕获和转换,让业务更清晰。
第三方交互逻辑:这块涉猎到 Http 工具、加解密,Apache 的 HttpClient、Hutool 的 HttpRequest、Spring 提供的 RestTemplate 等工具,对称或非对称加解密、单向 hash 相关算法、Base64、Md5 等编码,url encode 操作,不难学,但得有兴趣。
上述三类其实都算代码细节问题,往上看,还有业务与第三方 api 交互方式,异步、发布事件、引入策略等相关设计模式,这些属于高层一点的设计,唯有思考才会有兴趣去实践。
四、总结
我是橘长,一个 Java 工程师,以上就是我对对接第三方这事的相关技术沉淀与思考,沉下心来理解了相关思考,我猜你应该对拿下第三方这事就变得非常简单了,如果你觉得有所收获,感谢点赞、评论、转发、私信。
评论