单线程中Feign远程调用丢失请求头的情况
代码案例
订单服务:
Controller:
/**
* 当在购物车服务选中商品并点击“去结算”时触发订单服务。
* 该controller主要用于返回订单结算详情等信息
* */
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
model.addAttribute("orderConfirmData",orderConfirmVo);
return "confirm";
}
拦截器:
/**
* 该拦截器用于在跳转到订单服务时,判断用户是否登陆。若未登陆,则跳转到登陆页面。
* 若用于已登陆,则将用户的信息保存到ThreadLocal中。
* */
@Component
public class OrderLoginIntercepted implements HandlerInterceptor {
/**
* 本地线程,线程与线程之间的数据隔离
* */
public static ThreadLocal<MemberResponseVo> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo)request.getSession().getAttribute(AuthConstant.LOGIN_USER);
if(attribute!=null){
//把登录后用户的信息放在ThreadLocal里面进行保存
threadLocal.set(attribute);
return true;
}
else{
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.grapesmail.com/login.html");
return false;
}
}
}
ServiceImpl:
/**
* service实现层
* */
@Override
public OrderConfirmVo confirmOrder(){
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
/**
* 调用订单服务时,若已经登陆,则从订单服务的ThreadLocal中获取用户信息
* */
MemberResponseVo memberResponseVo = OrderLoginIntercepted.threadLocal.get();
//远程调用会员服务查询用户地址
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
orderConfirmVo.setMemberAddressVos(address);
//远程调用购物车服务查询购物车所有购物项。
//购物车服务涉及到拦截器session判断是否登陆。而Feign远程调用会构造一个新的request请求,该请求不携带请求头
//即:通过Feign远程调用会导致请求头消失,请求头消失,session,cookie等信息丢失,购物车就认为没有登陆
//解决方法:加上Feign的自己的拦截器
List<OrderItemVo> currentUserCarItems = cartFeignService.getCurrentUserCarItems();
orderConfirmVo.setItems(currentUserCarItems);
//查询用户积分
Integer integration = memberResponseVo.getIntegration();
orderConfirmVo.setIntegration(integration);
//TODO 接口幂等性,防重令牌
return orderConfirmVo;
}
购物车服务:
拦截器部分代码:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//登陆了就有用户id,没有登陆就为用户设置临时key
UserInfoTo userInfoTo = new UserInfoTo();
//通过SpringSession统一管理各服务之间的session
HttpSession session = request.getSession();
MemberResponseVo user = (MemberResponseVo)session.getAttribute(AuthConstant.LOGIN_USER);
//用户已登录,设置id,无需设置key,拿到该id
if(user!=null){
userInfoTo.setUserId(user.getId());
}
}
总结一下流程,即:当用户在购物车中选好商品并点击结算时,需要跳转到订单详情页。此时触发订单服务,订单服务"toTrade"在service层通过Feign远程调用,获取用户的收货地址以及购物车中选中的商品详情。而购物车服务中存在一个拦截器,该拦截器的作用是从session域中获取信息判断用户是否登陆。而通过远程Feign调用的话,Feign会默认构造一个新的request请求,该请求不携带头信息,因此购物车服务就无法通过拦截器从session域中得知用户是否登陆,这个时候默认用户没有登陆,而没有登陆的用户无法获取到购物车详情信息。
源码解析丢失请求头原因:
流程图解释:
解决方法:
在对应的controller中,添加request参数。以确保订单服务中的RequestInterceptor拦截器中,能够通过线程上下文拿到该请求。Order服务Controller->Order服务service->Order服务Feign调用订单服务->订单服务requestIntecerptor拦截器,一条线程中执行,由一个ThreadLocal共享线程数据。因此能够通过线程上下文拿到对应的请求。
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
model.addAttribute("orderConfirmData",orderConfirmVo);
return "confirm";
}
在订单服务中添加RequestInterceptor拦截器到容器中
@Configuration
public class mailFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
//远程之前均会先进行requestInterceptor.apply方法
//使用RequestContextHolder从请求下文中获得请求。即从controller参数中获得request信息
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();//老请求
if(request != null){
//同步头信息,cookie
String cookie = request.getHeader("Cookie");
//构造新请求,获取cookie
requestTemplate.header("Cookie",cookie);
}
}
};
}
}
异步调用中Feign远程调用丢失请求头的情况
由于异步编排是多线程执行,不属于以上Controller->service->Feign->RequestIntecerptor一条线程执行,因此可以手动通过RequestContextHolder设置请求信息,为每个线程保存自己的ThreadLocal变量,让各自的RequestIntecerptor去Threadlocal中获取自己需要的信息,示例代码如下:
代码案例
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberResponseVo memberResponseVo = OrderLoginIntercepted.threadLocal.get();
//在多线程异步任务之前,通过线程上下文获取到旧请求的属性(带头信息)。即从订单服务Controller防范参数的request中获取旧请求的属性。
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddress = CompletableFuture.runAsync(() -> {
//手动设置请求属性,确保会员服务的RequestIntecerptor能够拿到该请求信息
RequestContextHolder.setRequestAttributes(requestAttributes);
//远程查询用户地址
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
orderConfirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
CompletableFuture<Void> getCurrentUserCartItems = CompletableFuture.runAsync(() -> {
//手动设置旧属性,确保订单服务的RequestIntecerptor能够拿到该请求信息
RequestContextHolder.setRequestAttributes(requestAttributes);
//远程查询购物车所有购物项。
List<OrderItemVo> currentUserCarItems = cartFeignService.getCurrentUserCarItems();
orderConfirmVo.setItems(currentUserCarItems);
}, threadPoolExecutor);
//查询用户积分
Integer integration = memberResponseVo.getIntegration();
orderConfirmVo.setIntegration(integration);
//其他数据自动计算
//TODO 接口幂等性,防重令牌
//等待所有异步任务完成
CompletableFuture.allOf(getAddress,getCurrentUserCartItems).get();
return orderConfirmVo;
}
两者区别
详细视频教学:Feign远程调用丢失