-
-
个人笔记,如有描述不当,欢迎留言指出~
前提
公司使用springcloud开发微服务,我在开发审批模块中,其中一个新增审批请求中请求了其他几个模块,使用feign接口依次请求,最后发现请求平均耗时1.5s。这速度怎么能忍,于是进行了异步化改造。
异步化
同步版关键代码
1 | logger.info("rpc请求开始"); |
console
从打印日志中可以看出,由于网络波动性,请求耗时可能只要1秒也可能将近4秒,但rpc请求总是会占总耗时的90%左右,所以影响新增审批的请求的瓶颈就是这些rpc请求,下面我使用异步线程发送这些rpc请求。
异步版关键代码
1 | logger.info("rpc请求开始"); |
console
exception分析
what😨!竟然报未授权登录异常,不可能,feign请求里应该是带了请求头阿,下面拓展一些知识。
知识拓展
completableFuture
completableFuture扩展了Future的功能,并且实现了线程间同步的功能,我们用它提供的语法,可以很简单的实现异步编程。
oauth2
框架里引用了spring security以及oauth2来实现授权认证服务,所以客户请求是要带上Authorization请求头的。
feign
feign的本质,其实就是使用httpclient帮我们封装好了http请求。所以调用feign接口的方法就是发起一次http请求而已。如果不做处理的,那么feign发送的http请求里是没有Authorization请求头的。
RequestInterceptor
RequestInterceptor是feign提供的一个拦截器
放出我的配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FeignOauth2RequestInterceptor implements RequestInterceptor {
private final String AUTHORIZATION_HEADER = "Authorization";
private final String BEARER_TOKEN_TYPE = "Bearer";
public void apply(RequestTemplate requestTemplate) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if (authentication != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
requestTemplate.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, details.getTokenValue()));
}
}
}
拦截器的作用就是从securityContext拿到当前请求用户的认证信息authentication
,然后为feign的请求模板·requestTemplate·赋上Authorization请求头。
断点跟踪
断点1
打个断点跟进,看看拦截后feign执行了哪些操作,关键位置如下图:
可以看到feign将requestTemplate封装成request对象,并最终被client执行得到response,里面详细细节就不深入。从这里我们可以知道,调用feign发送的请求是携带Authorization请求头的,这样模块间发起请求就不会报未授权错误了。
断点2
所以按道理是不应该报用户未登录授权异常的,所以我们在拦截器打个端点,看看requestTemplate到底有没有被赋上请求头。
what😮!这不是推翻了我的认知吗,眼神逐渐呆滞😑
开个玩笑,authentication从securityContext里获取,securityContext从SecurityContextHolder静态方法中获取
从图中可以看出securityContext是从strategy里拿到的。SecurityContextHolderStrategy
是spring security安全上下文的存取策略。SecurityContextHolderStrategy
接口有三个实现类,对应三种实现策略:
GlobalSecurityContextHolderStrategy
:使用 一个静态变量存放securityContextThreadLocalSecurityContextHolderStrategy
:使用ThreadLocal存放securityContextInheritableThreadLocalSecurityContextHolderStrategy
:使用InheritableThreadLocal存放securityContext断点3
那我当前环境里SecurityContextHolder里使用的是哪个策略呢,打个断点
恍然大悟!🤣原来采用的是ThreadLocalSecurityContextHolderStrategy
还记得我异步改造里怎么写的吗1
2
3
4
5
6
7
8
9CompletableFuture<StoreDTO> storeDTOCompletableFuture = CompletableFuture.supplyAsync(() -> {
try {
logger.info("当前线程名:{}", Thread.currentThread().getName());
return storeClient.getStoreDTO(userDTO.getStoreUuid());
} catch (Exception e) {
logger.error("获取门店信息rpc错误,原因:", e);
throw e;
}
});
我将调用feign请求代码写在CompletableFuture.supplyAsync(()->{})
中,而CompletableFuture里默认线程池会分配新线程去执行任务,所以新线程里是没有securityContext的线程副本的!所以在拦截器里才会取到空的securityContext,最终报未授权登录异常!bingo😎
最终异步版关键代码
1 | logger.info("rpc请求开始"); |
console
可以看到rpc请求的耗时被平均缩短到500毫秒内😁
CompletableFuture
学习可以看这篇👉here,我觉得这位博主写的还行。