整合 retrofit-spring-boot-starter 和 sentinel-okhttp-adapter

这里其实更多的是 retrofit-spring-boot-starter 和 sentinel-okhttp-adapter 整合。不关心其他的话,可以直接调整到 重点在此 章节。

原始文章发布在: 从现在开始的个人博客,欢迎点击阅读,谢谢

缘由

由于业务需要,我们需要对接另一个平台(国内某 ERP 厂商)的接口。 其中有两个点: 一个是请求头的处理,另一个是限流的处理。

请求头

对方的开放平台要求请求附有两个请求头。

  • accessToken 获取令牌,具有有效期。当然这意味着可以缓存多次使用(事实证明这里面也有坑)
  • sign 参数加签,每次请求都需要处理

限流

每个接口的限流策略都不一样,大部分接口是 1 秒 1 次。

image-1660543009111

所以这里需要针对不同的接口要有不同的限流规则

技术栈

由于另一个平台的 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

这里的加签比较简单,要求如下: image-1660631834179 从 RequestBody 中获取参数然后做一定规则的处理即可。

请求头之 accessToken

这里关于 accessToken 有个"小坑": image-1660630948727 第一眼看返回结果中有个生效时间,以为是每一次请求都重新对应了一个时间。例如间隔 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)
复制代码

最终,为了应对各种未知的错误,也不管它到底如何切换。决定结合重试拦截器做处理。由于默认的重试拦截器不满足需求,所以进行了自定义,关键代码如下:

image-1660633978495

可能看上去不够优雅,但是绝对管用!!!

限流

说起限流,刚开始是尝试下 guava 的 RateLimier,这个家伙是令牌桶的实现,这个与要求是不符合的。我们要的是匀速通过,想要的是漏桶的实现。 所以后来尝试手撸了一套,原理比较简单,也是拦截器,记录上一次的时间,然后当前请求与上一次的比较,如果大于那么就放行。如果小于就睡上一会儿。大体代码是这样子: image-1660551849091

但是不好针对不同的接口做不同的限流规则,所以最后还是决定使用大厂阿里的 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 客户端中是这样处理的: image-1660635148313

那么结合前提要求,限流拦截器就势必在重试拦截器之后。到这一步,其实已经明了,只能利用 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 :

image-1660637082544

目前是单机流控,动态规则配置使用的 拉模式:使用文件配置规则image-1660637333076

流量控制 使用了匀速器方式。具体配置在: image-1660637477604

关键是配置参数 com.alibaba.csp.sentinel.slots.block.RuleConstant#CONTROL_BEHAVIOR_RATE_LIMITER 最终的处理逻辑在 com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

感谢 两个开源库,节省了不少开发时间。

撒花完结

感谢阅读,还是有一些细节在里面。主要是 chain 的顺序。

一枚普通的 Java 开发工程师,各位看官如果有任何的建议,欢迎在评论区留言!再次感谢!

猜你喜欢

转载自juejin.im/post/7132390273618804743