关于学习鼓励商城微服务中Feign远程调用请求头丢失问题记录

本文详细介绍了在单线程环境中,使用Feign进行远程调用时请求头丢失导致的会话信息缺失问题。问题源于Feign创建新请求时不携带原始请求头,导致拦截器无法获取用户登录状态。解决方案是在Feign客户端配置RequestInterceptor,通过RequestContextHolder获取并设置请求头,确保信息传递。对于异步调用,需在每个任务开始前手动设置请求属性。文章还提供了具体的代码示例和流程图以辅助理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单线程中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远程调用丢失

### 使用Feign客户端传递自定义HTTP请求头微服务架构中,当使用Feign作为Web Service客户端时,可以通过多种方式向远程服务发送带有特定头部信息的HTTP请求。一种常见做法是在发起请求前利用拦截器机制动态地附加这些头部字段。 对于希望每次调用`MyFeignClient.getSomeData()`方法时都附带固定的header名称与值的情况,可以构建一个实现了`RequestInterceptor`接口的类——例如命名为`HeaderInterceptor`,在这个类里重写其唯一的抽象方法`apply(RequestTemplate template)`,并通过设置模板对象上的headers属性完成对欲添加项的操作[^1]: ```java import feign.RequestInterceptor; import feign.RequestTemplate; public class HeaderInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { template.header("headerName", "headerValue"); } } ``` 为了让上述逻辑生效,还需要确保此拦截器已被注册至应用程序上下文中;通常可以在启动类或者其他配置组件内通过Java Config形式注入该bean实例即可。 另外,在实际业务场景下可能更倾向于依据当前环境变量或是其他运行时期条件灵活决定要追加的具体键值对组合,则此时应当考虑借助于Spring框架所提供的依赖注入特性来简化此类操作。比如下面的例子展示了如何基于`@ConfigurationProperties`注解读取外部化配置文件里的参数并将其应用到待发送的数据包之中[^2]: ```yaml # application.yml or equivalent configuration file custom.headers: header-name: custom-value ``` ```java @Configuration @EnableConfigurationProperties(CustomHeaders.class) public class FeignConfig { private final CustomHeaders headers; public FeignConfig(CustomHeaders headers){ this.headers=headers; } @Bean public RequestInterceptor requestInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { Map<String, String> map = headers.asMap(); for (String key : map.keySet()) { template.header(key,map.get(key)); } } }; } } @Component @Data // Lombok annotation to generate getters and setters automatically. @ConfigurationProperties(prefix="custom.headers") class CustomHeaders extends HashMap<String,String>{}; ``` 除了全局性的解决方案外,针对单次API交互而言也可以直接采用`@RequestHeader`标注形参的方式来显式指明所需携带的内容。这使得开发者能够更加直观地控制每一次具体的RPC行为,并且保持良好的代码可读性和维护性[^3]。 #### 示例:结合`@RequestHeader`实现局部范围内的请求头定制 假设存在如下所示的服务消费端接口定义: ```java package com.example.demo.clients; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; @FeignClient(name = "example-service-client", url = "${service.url}") public interface ExampleServiceClient { @GetMapping("/api/data") ResponseEntity<DataResponseDTO> getData(@RequestHeader(value = "Custom-Header") String customHeaderValue); } ``` 这里的关键在于`getData`函数签名部分所使用的`@RequestHeader`修饰符及其后面的表达式,它表明每当有人尝试执行这个动作时都需要额外提供一个名为`Custom-Header`的字符串类型的输入参数用于填充即将发出的消息体之外的部分[^4]。 最后值得注意的是,在某些特殊场合下(如涉及跨域资源共享CORS策略、安全认证token传播等问题),可能会遇到即使按照常规手段设置了相应条目却仍然无法正常抵达目的地的现象。这时就需要深入探究整个网络传输路径上各个节点的行为模式,特别是像Zuul这样的路由代理层是否会对接收到的信息做进一步处理而导致原始意图失效的情形发生[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值