场景
A 服务feign调用B服务查询数据
B服务
通过ThreadLocal获取到当前请求线程中的用户信息,
拦截器:
HttpSession session = request.getSession();
User user = (User)session.getAttribute(LOGIN_KEY);
Service
@Override
public List<CartItemVo> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
List<Cart> li = service.select(userInfo);
return li;
}
…
备注:
ThreadLocal
同一个线程共享数据,
比如, tomcat每次处理一个请求都会开一个线程,
从拦截器 --> Controller --> Service --> Dao --> 请求结束, 给浏览器响应
所以在同一个线程期间, 上游放了一个值, 下游也可以使用
A服务
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setMemberAddressVos(address);
可以看到, A服务直接远程调用B服务, 但是就是查询不到数据, 原因是:B服务被拦截器拦截, 提示用户获取时null
但是实际上用户已经登录了, 原因是:feign在远程调用的时候会自己创建request代理对象, 而且根据本项目中配置的requestInterceptor来配置这个新的request对象
…
Fegin远程调用丢失请求头
fegin在远程调用之前要构造请求, 调用很多的拦截器 RequestInterceptor interceptor
但是他默认构造的请求头(header)里边什么都没有
因此调用失败
…
以前浏览器发送请求, 请求头会自动带了cookie
但是现在是, A服务远程feign调用的B服务
这个请求是A服务, 自己创建了一个新的request, 而且这个请求里边没有任何请求头, 因此B服务从session里没取到用户信息, B服务提示用户没登录, 所以查询不到数据
所以为了解决这个问题, 就需要自己加上 feign远程调用拦截器
为了解决, 需要新增一个配置类
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Configuration
public class FeignConfig {
/*
feign调用之前, 每次都会构建新的request
而这个request的构造, 是遍历本服务内的所有requestInterceptor.apply()方法, 将这些信息统一应用到一个request中
之后用这个request进行远程调用
因此为了解决feign丢失请求头, 就可以自定义一个requestInterceptor,
将本次请求的信息, 同步给这个feign新建的代理的request里
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
//每次进行feigin调用之前都会调用这个方法, 重新设置feign调用request代理对象
System.out.println("feign远程之前会先进行RequestInterceptor.apply");
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest(); //老请求
//同步请求头数据, 主要是同步cookie信息
String cookie = request.getHeader("Cookie");
//给新请求把老请求的cookie放进去
requestTemplate.header("Cookie",cookie);
}
};
}
}
总结
1, feign远程调用的时候, 会自己构建request, 用这个新构建的去远程调用其他服务
2, feign构建的request对象, 会遍历本项目内的所有requestInterceptor的子类, 主要是根据他的apply()方法来构建一个新的request对象, 也就是这个request有所有apply()方法的配置信息
3, 可以通过添加RequestInterceptor的对象, 来实现给feign的request对象进行动态配置
4, 不只可以加cookie, 其他的也可以搞
异步模式下feign问题
根据上边的步骤也可以看出来, 关键点在于将本服务的request所有信息都同步给feign创建的新request
而因为异步模式相当于开启新线程
去做嘛,创建一个新线程, 相当于与本来同步调用的主线程不是一个线程了
, 而RequestContextHolder就是通过ThreadLocal来实现的
数据保存获取;
问题: 这样子的话, 异步feign调用的话, 肯定新线程就获取不到主线程request里的数据了
异步feign调用
CompletableFuture<Void> getAdressFutrue = CompletableFuture.runAsync(() -> {
memberFeignService.getAddress(memberRespVo.getId());
}, threadPoolExecutor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
cartFeignService.getCurrentItems();
});
这样异步调用, 因为新启线程, 和原主线程不是同一个了, 就出现问题了
/*
因为下边是用的异步feign调用, 异步就会启动新线程
而新线程和主线程不在同一个, 主线程的ThreadLocal数据没办法共享到新线程
因此, 需要先获取到主线程的请求头数据, 然后在异步任务开始之前将主线程的请求头数据共享给新线程
*/
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAdressFutrue = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
}, threadPoolExecutor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentItems();
});
关键点
1, 在异步调用之前, 先将主线程的数据获取出来,
2, 在异步调用之前, 在执行方法中, (新线程创建之后), 把主线程的数据同步给新的线程
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(requestAttributes);