整合 retrofit-spring-boot-starter 和 sentinel-okhttp-adapter
缘由
由于业务需要,我们需要对接另一个平台(国内某 ERP 厂商)的接口。 其中有两个点: 一个是请求头的处理,另一个是限流的处理。
请求头
对方的开放平台要求请求附有两个请求头。
- accessToken 获取令牌,具有有效期。
当然这意味着可以缓存多次使用(事实证明这里面也有坑) - sign 参数加签,每次请求都需要处理
限流
每个接口的限流策略都不一样,大部分接口是 1 秒 1 次。
所以这里需要针对不同的接口要有不同的限流规则
技术栈
由于另一个平台的 sdk 没有释放,那么对接接口其实就是发请求,处理响应结果。我们一般的话直接使用 HttpClient 或者 RestTemplate。 不过就使用上来讲,最后还是选择了 retrofit-spring-boot-starter。 至于为什么,可以上手试用一下便知,然后在整合过程中也印证了选择它是个正确的选择。
解决方案
请求头
基于 注解式拦截器,所以我们可以很方便的对请求头做出修改。大致代码如下:
@Override
protected Response doIntercept(Chain chain) throws IOException {
Request request = chain.request();
// 加签
String sign = sign(request);
Request newReq = request.newBuilder()
.addHeader("accessToken", gerpGoAccessTokenService.accessToken())
.addHeader("sign", sign)
.build();
return chain.proceed(newReq);
}
请求头之 sign
这里的加签比较简单,要求如下: 从 RequestBody 中获取参数然后做一定规则的处理即可。
请求头之 accessToken
这里关于 accessToken 有个”小坑”: 第一眼看返回结果中有个生效时间,以为是每一次请求都重新对应了一个时间。例如间隔 1 秒发送请求:
第 1 次请求 accessToken 有效期 7 秒
第 2 次请求 accessToken 有效期 7 秒
然鹅,通过调试发现,原来一旦颁发了 accessToken ,那么就对应了一个有效期,这个有效期是递减的。直至归零,重新颁发下一个周期。所以实际上是这样的,间隔 1 秒发送请求:
第 1 次请求 accessToken 有效期 7 秒
第 2 次请求 accessToken 有效期 6 秒
...
第 7 次请求 accessToken 有效期 1 秒
有效期归零后重新颁发
第 8 次请求 accessToken 有效期 7 秒
第 9 次请求 accessToken 有效期 6 秒
...
那么这样子的话,直接做缓存就会有问题了。准确来讲,缓存 OK,但由于请求来回有时间差,所以在两次 accessToken 切换的时候会有麻烦,这里可以想一想。 我刚开始的想法是这样子的:
第一次请求,拿到生效时间,然后直接记录到缓存中。(这里可能就有了时间差,有延迟,在两次切换时候可能导致 accessToken 已经重新颁发。但是请求中还是之前的 accessToken)。 之后的请求,直接从缓存中获取 accessToken。
事实上,后面的确报了相关的错误:
{"code":48005,"message":“ invalid credential, access token is invalid or lates","data:null,"extobj":null)
最终,为了应对各种未知的错误,也不管它到底如何切换。决定结合重试拦截器做处理。由于默认的重试拦截器不满足需求,所以进行了自定义,关键代码如下:
可能看上去不够优雅,但是绝对管用!!!
限流
说起限流,刚开始是尝试下 guava 的 RateLimier,这个家伙是令牌桶的实现,这个与要求是不符合的。我们要的是匀速通过,想要的是漏桶的实现。 所以后来尝试手撸了一套,原理比较简单,也是拦截器,记录上一次的时间,然后当前请求与上一次的比较,如果大于那么就放行。如果小于就睡上一会儿。大体代码是这样子:
但是不好针对不同的接口做不同的限流规则,所以最后还是决定使用大厂阿里的 Sentinel
重点在此
这里是重头戏,做一下 retrofit-spring-boot-starter 和 sentinel-okhttp-adapter 的整合。
引入依赖
按照国际惯例,先引入依赖:
<dependency>
<groupId>com.github.lianjiatech</groupId>
<artifactId>retrofit-spring-boot-starter</artifactId>
<version>2.3.5</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-okhttp-adapter</artifactId>
<version>1.8.5</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
<version>1.8.5</version>
</dependency>
其中 sentinel-okhttp-adapter 是 Sentinel 针对开源框架 OkHttp 的适配。 然后 sentinel-datasource-extension 是为了应用 动态规则扩展
整合
前提要求
限流的同时要兼顾重试机制。发生异常时需要进行重试,如果重试次数用尽依旧失败,那么就抛出异常。为什么要提这个,因为这里遇到了点问题,哈哈。
具体实施
其实按照 sentinel-okhttp-adapter 介绍 的话非常简单,就是添加一个 SentinelOkHttpInterceptor 拦截器。但事实上整合起来肯定不是这么简单了,不然就不会有下文。
首先看一下框架的处理,在创建 okhttp 客户端中是这样处理的:
那么结合前提要求,限流拦截器就势必在重试拦截器之后。到这一步,其实已经明了,只能利用 networkInterceptor 做文章。 不过这里的 NetworkInterceptor 最终添加到的是 OKhttpclient 的 interceptors 中,而不是 networkInterceptors 中。这样也好,正是想要的效果。
所以最后自定义 NetworkInterceptor,委托给 SentinelOkHttpInterceptor 做处理进行流控。
public class GerpGoNetworkInterceptor implements NetworkInterceptor {
private final SentinelOkHttpInterceptor sentinelOkHttpInterceptor;
public GerpGoNetworkInterceptor(SentinelOkHttpInterceptor sentinelOkHttpInterceptor) {
this.sentinelOkHttpInterceptor = sentinelOkHttpInterceptor;
}
@Override
public @NotNull Response intercept(@NotNull Chain chain) throws IOException {
return sentinelOkHttpInterceptor.intercept(chain);
}
}
然后注入 bean
@Bean
public NetworkInterceptor networkInterceptor() {
return new GerpGoNetworkInterceptor(new SentinelOkHttpInterceptor(new SentinelOkHttpConfig()));
}
最后的 chain :
目前是单机流控,动态规则配置使用的 拉模式:使用文件配置规则。
流量控制 使用了匀速器方式。具体配置在:
关键是配置参数 com.alibaba.csp.sentinel.slots.block.RuleConstant#CONTROL_BEHAVIOR_RATE_LIMITER 最终的处理逻辑在 com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
感谢 两个开源库,节省了不少开发时间。
撒花完结
感谢阅读,还是有一些细节在里面。主要是 chain 的顺序。