时间:2022-11-27 10:20:55 | 栏目:JAVA代码 | 点击:次
各个子系统之间通过feign
调用,每个服务提供方需要验证每个请求header
里的token
。
public void invokeFeign() throws Exception { feignService1.method(); feignService2.method(); feignService3.method(); .... }
定义拦截每次发送feign
调用拦截器RequestInterceptor
的子类,每次发送feign
请求前将token
带入请求头
@Configuration public class FeignTokenInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { public void apply(RequestTemplate template) { //上下文环境保持器,拿到刚进来这个请求包含的数据,而不会因为远程数据请求头被清除 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest();//老的请求 if (request != null) { //同步老的请求头中的数据,这里是获取cookie String cookie = request.getHeader("token"); template.header("token", cookie); } } ..... }
这样便能实现系统间通过同步方式feign
调用的认证问题。但是如果需要在invokeFeign
方法中feignService3
的方法调用比较耗时,并且invokeFeign
业务并不关心feignService3.method()
方法的执行结果,此时该怎么办。
修改feignService3.method()
方法,将其内部实现修改为异步,这种方案依赖服务的提供方,如果feignService3
服务是其他业务部门维护,并且无法修改实现为异步,此时只能采取方案2.
通过线程池调用feignServie3.method()
public void invokeFeign() throws Exception { feignService1.method(); feignService2.method(); executor.submit(()->{ feignService3.method(); }); .... }
怀着期待的心情开启了尝试,你会发现调用feignService3
方法并没有成功,查看日志你将会发现是由于feign
发送request
请求的header
中未携带token
导致。于是百度了下feign
异步调用传参,网上大部分的解决方案,如下
public void invokeFeign() throws Exception { feignService1.method(); feignService2.method(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); executor.submit(()->{ RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); feignService3.method(); }); } }
添加了上面的代码后,实测无效,此时确实有些束手无策。但是真的没无效吗?我仔细比对通过上述手段解决问题的博客,他们的业务代码和我的代码不同之处。确实有不同,比如https://www.jb51.net/article/249407.htm这篇。其代码如下
@Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo confirmVo = new OrderConfirmVo(); MemberResVo memberResVo = LoginUserInterceptor.loginUser.get(); //从主线程中获得所有request数据 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //1、远程查询所有地址列表 RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId()); confirmVo.setAddress(address); }, executor); //2、远程查询购物车所选的购物项,获得所有购物项数据 CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //放入子线程中request数据 RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> items = cartFeginService.getCurrentUserCartItems(); confirmVo.setItem(items); }, executor).thenRunAsync(()->{ RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> items = confirmVo.getItem(); List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList()); //远程调用查询是否有库存 R hasStock = wmsFeignService.getSkusHasStock(collect); //形成一个List集合,获取所有物品是否有货的情况 List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() { }); if (data!=null){ //收集起来,Map<Long,Boolean> stocks; Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock)); confirmVo.setStocks(map); } },executor); //feign远程调用在调用之前会调用很多拦截器,因此远程调用会丢失很多请求头 //3、查询用户积分 Integer integration = memberResVo.getIntegration(); confirmVo.setIntegration(integration); //其他数据自动计算 CompletableFuture.allOf(getAddressFuture,cartFuture).get(); return confirmVo; }
我们看的出来,他的业务代码即使是开启多线程,也是等最后线程里的任务都执行完成后,业务方法才结束返回,而我的业务方法并不会等feignService3
调用完成结束,抱着尝试的心态,我调整了下代码添加了CountDownLatch
,让业务方法等待feign
调用结束后在返回。
public void invokeFeign() throws Exception { feignService1.method(); feignService2.method(); CountDownLatch latch = new CountDownLatch(1); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); executor.submit(()->{ RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); feignService3.method(); latch.countDown(); }); latch.await(); } }
不如所料,调用成功了。到这里看似是解决了问题,但是与我想象的异步差别太大了,最终业务线程还是需要等待feignService3.method()
调用业务方法才能返回,而且异步场景如发送短信、消息推送,记录日志可能调用耗时,业务方法可不想等待他们执行结束,此时该怎么解决?只能翻源码ServletRequestAttributes.java
首先看到了注释,这给了我灵感
Servlet-based implementation of the {@link RequestAttributes} interface. <p>Accesses objects from servlet request and HTTP session scope, with no distinction between "session" and "global session".
从servlet
请求和HTTP
会话范围访问对象,"session"和"global session"作用域没有区别。对呀会不会是因为header
中的参数是request
作用域的原因呢,因为请求结束,所以即使在子线程设置请求头,也取不到原因。回到请求拦截器RequestInterceptor
查看获取token
地方
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); //老的请求 HttpServletRequest request = attributes.getRequest(); if (request != null) { //同步老的请求头中的数据,这里是获取cookie String cookie = request.getHeader("token"); template.header("token", cookie); }
果然如此,从attributes
中获取request
,然后从request
中获取token
。但是没有考虑到request
请求结束,request
作用域的问题,此时肯定取不到header
里的token
了。
那么该怎么解决呢?思路不能变,肯定还是围绕着ServletRequestAttributes
展开,发现他有两个方法getAttributes
和setAttribute
,而且这俩方法都支持两个作用域request
、session
。
@Override public Object getAttribute(String name, int scope) { if (scope == SCOPE_REQUEST) { if (!isRequestActive()) { throw new IllegalStateException( "Cannot ask for request attribute - request is not active anymore!"); } return this.request.getAttribute(name); } else { HttpSession session = getSession(false); if (session != null) { try { Object value = session.getAttribute(name); if (value != null) { this.sessionAttributesToUpdate.put(name, value); } return value; } catch (IllegalStateException ex) { // Session invalidated - shouldn't usually happen. } } return null; } } @Override public void setAttribute(String name, Object value, int scope) { if (scope == SCOPE_REQUEST) { if (!isRequestActive()) { throw new IllegalStateException( "Cannot set request attribute - request is not active anymore!"); } this.request.setAttribute(name, value); } else { HttpSession session = obtainSession(); this.sessionAttributesToUpdate.remove(name); session.setAttribute(name, value); } }
既然我们的业务方法调用(HttpServletRequest
)不会等待feignService3.method
,我们可以通过ServletRequestAttributes.setAttributes
指定作用域为session
呀。此时invokeFeign
代码如下
public void invokeFeign() throws Exception { feignService1.method(); feignService2.method(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); //在ServeletRequestAttributes中设置token,作用域为session attributes.setAttribute("token",attributes.getRequest().getHeader("token"),1); executor.submit(()->{ RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); feignService3.method(); }); } }
然后RequestInterceptor.apply
方法也做响应调整,如下
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); //老的请求 HttpServletRequest request = attributes.getRequest(); String token = (String) attributes.getAttribute("token",1); template.header("token",token); if (request != null) { //同步老的请求头中的数据,这里是获取cookie String cookie = request.getHeader("token"); template.header("token", cookie); }
问题得以圆满解决。