微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)

一、前言 在前面的文章:

SpringCloud之Feign实现声明式客户端负载均衡详细案例
我们聊了OpenFeign的概述、为什么会使用Feign代替Ribbon、Feign和OpenFeign的区别、以及详细的OpenFeign实现声明式客户端负载均衡案例。
在一些业务场景中,微服务间相互调用需要做鉴权,以保证我们服务的安全性。即:服务A调用服务B的时候需要将服务B的一些鉴权信息传递给服务B,从而保证服务B的调用也可以通过鉴权,进而保证整个服务调用链的安全。
本文我们就讨论如果通过openfeign的拦截器RequestInterceptor实现服务调用链中上下游服务请求头数据的传递。
二、实现RequestInterceptor 通过RequestInterceptor 拦截器拦截我们的openfeign服务请求,将上游服务的请求头或者请求体中的数据封装到我们的openfeign调用的请求模板中,从而实现上游数据的传递。
1、RequestInterceptor实现类 1)RequestInterceptor实现类
package com.saint.feign.config; import feign.RequestInterceptor; import feign.RequestTemplate; import lombok.extern.slf4j.Slf4j; 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; import java.util.Enumeration; import java.util.Objects; /** * 自定义的Feign拦截器 * * @author Saint */ @Slf4j public class MyFeignRequestInterceptor implements RequestInterceptor { /** * 这里可以实现对请求的拦截,对请求添加一些额外信息之类的 * * @param requestTemplate */ @Override public void apply(RequestTemplate requestTemplate) { // 1. obtain request final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 2. 兼容hystrix限流后,获取不到ServletRequestAttributes的问题(使拦截器直接失效) if (Objects.isNull(attributes)) { log.error("MyFeignRequestInterceptor is invalid!"); return; } HttpServletRequest request = attributes.getRequest(); // 2. obtain request headers,and put it into openFeign RequestTemplate Enumeration headerNames = request.getHeaderNames(); if (Objects.nonNull(headerNames)) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = https://www.it610.com/article/request.getHeader(name); requestTemplate.header(name, value); } }// todo 需要传递请求参数时放开 // 3. obtain request body, and put it into openFeign RequestTemplate //Enumeration bodyNames = request.getParameterNames(); //StringBuffer body = new StringBuffer(); //if (bodyNames != null) { //while (bodyNames.hasMoreElements()) { //String name = bodyNames.nextElement(); //String value = request.getParameter(name); //body.append(name).append("=").append(value).append("&"); //} //} //if (body.length() != 0) { //body.deleteCharAt(body.length() - 1); //requestTemplate.body(body.toString()); //log.info("openfeign interceptor body:{}", body.toString()); //} } }

2)使RequestInterceptor生效(均已验证)
使RequestInterceptor生效的方式有四种;
1> 代码方式全局生效 直接在Spring可以扫描到的路径使用@Bean方法将RequestInterceptor实现类注入到Spring容器;
package com.saint.feign.config; import feign.RequestInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author Saint */ @Configuration public class MyConfiguration {@Bean public RequestInterceptor requestInterceptor() { return new MyFeignRequestInterceptor(); } }

2> 配置方式全局生效
feign: client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: full # 拦截器配置(和@Bean的方式二选一) requestInterceptors: - com.saint.feign.config.MyFeignRequestInterceptor

3> 代码方式针对某个服务生效 直接在@FeignClient注解中指定configuration属性为RequestInterceptor实现类
微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片

4、配置方式针对某个服务生效
feign: client: config: SERVICE-A: connectTimeout: 5000 readTimeout: 5000 loggerLevel: full # 拦截器配置(和@Bean的方式二选一) requestInterceptors: - com.saint.feign.config.MyFeignRequestInterceptor

2、效果验证 1)feign-server服务改造
在文章 SpringCloud之Feign实现声明式客户端负载均衡详细案例的基础下,我们修改feign-server项目,添加一个MVC拦截器(用于获取请求头中的数据)
微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片

1> MvcInterceptor
package com.saint.feign.config; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 自定义MVC拦截器 * * @author Saint */ @Slf4j public class MvcInterceptor implements HandlerInterceptor {@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token-saint"); log.info("obtain token is : {}", token); return true; }}

2> MvcInterceptorConfig 设置MVC拦截器会拦截哪些路径的请求,这里是所有的请求全部拦截。
package com.saint.feign.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * MVC拦截器配置 * * @author Saint */ @Configuration public class MvcInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MvcInterceptor()) .addPathPatterns("/**"); } }

2)结果验证
1> 执行请求:
微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片
微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片

2> feign-consumer中的日志:
微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片

3> feign-server中的日志:
微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片

结果显示,RequestInterceptor生效了
三、结合Hystrix限流使用时的坑(仅做记录) 此处OpenFeign依赖的SpringCloud版本是2020.X之前。
【微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)】在application.yaml文件中做如下配置开启了Hystrix限流:
feign: hystrix: enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 30000

做完上述配置后,Feign接口的熔断机制为:线程模式
如果我们自定义了一个RequestInterceptor实现类,就会导致hystrix熔断机制失效,接口调用异常(404、null);
1、原因分析
  • 在feign调用之前,会走RequestInterceptor拦截器,拦截器中使用了ServletRequestAttributes获取请求数据;
  • 默认feign使用的是线程池模式,当开启熔断的时候,负责熔断的线程和执行Feign接口的线程不是同一个线程,ServletRequestAttributes取到的将会是空值。
2、解决方案 将hystrix熔断方式从线程模式改为信号量模式;
feign: hystrix: enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 30000 strategy: SEMAPHORE

3、Hystrix线程和信号量隔离区别 微服务|【云原生&微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
文章图片

4、线程和信号量隔离的使用场景?
1> 线程池隔离
  • 请求并发量大,并且耗时长(一般是计算量大或者读数据库);
  • 采用线程池隔离,可以保证大量的容器线程可用,不会由于其他服务原因,一直处于阻塞或者等待状态,快速失败返回。
    2> 信号量隔离
  • 请求并发量大,并且耗时短(一般是计算量小,或读缓存);
  • 采用信号量隔离时的服务的返回往往非常快,不会占用容器线程太长时间;
  • 其减少了线程切换的一些开销,提高了缓存服务的效率 。

    推荐阅读