微服务架构 - 头部参数传递获取问题解决方案
转自:今日头条作者:老顾聊技术 原文地址:https://www.toutiao.com/i6928950917370888717/
目录
问题一 一次请求中,服务间调用丢失头部参数的问题
问题二 异步调用场景下头部参数获取失败的问题
问题三 异步调用场景下头部参数获取失败的问题2
前言
正常获取头部参数header,就是从request对象中获取头部参数。
Enumeration<String> headerName = request.getHeaderNames();
while (headerNames.hasMoreElement()) {
// 头部参数
String name = headerNames.nextElement();
// 头部参数值
String value = request.getHeader(name);
}
上面的代码是获取头部参数的基本代码。
问题一
只要从事微服务架构开发的人,基本都会遇到的问题。
上图是微服务架构中常见的业务,即consumer服务调用provider服务,那用户调用consumer服务的时候传入的header参数,到最后的provider服务这里是否能够获取?
consumer消费端代码:
@RestController
public class ConsumerController {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerController.class);
@Autowired
private ProviderServiceFeign providerServiceFeign;
@GetMapping("/getHeader")
public String transferHeaders(HttpServletRequest request) throws Exception {
String deviceId = request.getHeader(name: "deviceId");
String token = request.getHeader(name: "token");
LOGGER.info("consumer服务中获取的请求头deviceId={},token={}", deviceId, token);
providerServiceFeign.transferHeaders();
return "success";
}
}
provider提供端代码:
@RestController
public class ProviderController {
private static final Logger LOGGER = LoggerFactory.getLogger(ProviderController.class);
@GetMapping("/transferHeaders")
public String transferHeader(HttpServletRequest request) {
String deviceId = request.getHeader(name: "deviceId");
LOGGER.info("provider服务中从请求头中获取的deviceId={}", deviceId);
String token = request.getHeader(name: "token");
LOGGER.info("provider服务中从请求头中获取的header={}", token);
return "success";
}
}
consumer消费端中的Feign代码:
@FeignClient(name="service-provider")
public interface ProviderServiceFeign {
@getMapping("/transferHeaders")
public String transferHeaders();
}
上面的代码表示客户端传入deviceId和token参数,需要在consumer和provider两个服务端都能获取到,然后打印出来。
启动测试
执行结果
consumer消费端打印结果如下,能够获取到header参数
c.p.q.e.controller.ConsumerController: consumer服务中获取的请求头deviceId==1111 ,token==2222
provider生产端打印结果如下,没有获取到header参数
c.p.q.e.controller.ProviderController: provider服务中从请求头中获取的deviceId===null
c.p.q.e.controller.ProviderController: provider服务中从请求头中获取的token===null
失败原因:Feign调用是远程RPC调用,虽然底层是通过httpClient方式去调用的,但是它并没有把原始的header参数传入。
解决方案
Feign提供了RequestInterceptor请求拦截器,我们只需要在Feign调用前把header参数传入就可以了。
代码:
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(ReuqestTemplate template) {
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// header的name都是小写
if ("deviceId".equals(name) || "token".equals(name)) {
template.header(name, request.getHeader(name));
}
}
}
}
上面代码就是实现RequestInterceptor接口的apply方法,参数template中有对header的封装,只需要在Feign调用前把header参数值传入template中就OK了,这样就顺利的把header参数传递了。
ps:上面的代码我们只传递header参数deviceId和token的值,其他忽略。这个拦截器要在消费端进行注入加载,要做成公共的组件core包,给微服务引用。
启动测试
消费端打印:
c.p.q.e.controller.ConsumerController: consumer服务中获取的请求头deviceId==1111 ,token==2222
生产端打印:
c.p.q.e.controller.ProviderController: provider服务中从请求头中获取的deviceId===1111
c.p.q.e.controller.ProviderController: provider服务中从请求头中获取的token===2222
可以看到,provider生产端能够正常获取header参数了。
问题二
如果consumer端在调用provider的时候,需要异步调用,也就是开启一个子线程去调用provider方法;
业务场景,如果provider方法耗时很长;导致consumer调用方耗时也长;那如果业务认可的情况下,我们可以不需要等待provider的执行结果,继续执行consumer就行了。
@RestController
public class ConsumerController {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerController.class);
@Autowired
private ProviderServiceFeign providerServiceFeign;
@GetMapping("/getHeader")
public String transferHeaders(HttpServletRequest request) throws Exception {
String deviceId = request.getHeader(name: "deviceId");
String token = request.getHeader(name: "token");
LOGGER.info("consumer服务中获取的请求头deviceId={},token={}", deviceId, token);
new Thread(new Runnable() {
@Override
public void run() {
providerServiceFeign.transferHeaders();
}
}).start();
return "success";
}
}
启动测试
provider生产打印结果,没有获取到header参数
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===null
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===null
调试一下,发现FeignRequestInterceptor拦截器ServletRequestAttributes attributes为null,导致header参数传递失败。
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (attributes != null) { // <== 运行结果attributes为null
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElement()) {
String name = headerNames.nextElement();
// header的name都是小写
if ("deviceId".equals(name) || "token".equals(name)) {
template.header(name, request.getName(name));
}
}
}
}
}
了解ServletRequestAttributes获取失败的原因前,需要先了解RequestContextHolder是什么。我们看一下源码:
public abstract class RequestContextHolder {
private static final boolean jsPresent = ClassUtils.isPresent(className: "javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
}
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequsetAttributesHolder.set(inheritable);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttribtesHolder.remove();
}
}
}
我们发现RequestContextHolder本质是通过ThreadLocal进行变量的保存和获取的;也就是header参数值是保存在ThreadLocal中的。那客户端请求过来时,主线程对header参数保存到了主线程的ThreadLocal;但是如果子线程调用feign时,子线程是没法获得主线程的ThreadLocal的,所以获得为null。
解决方案
解决上面的问题,本质就是要解决子线程如何能够获取到父线程的ThreadLocal。这时就需要引出另一个ThreadLocal,即InheritableThreadLocal。
两者之间的区别如下:
- ThreadLocal:单个线程生命周期强绑定,只能在某个线程的生命周期内对ThreadLocal进行存取,不能跨线程存取。
- InheritableThreadLocal:(1)可以无感知替代ThreadLocal的功能,当成ThreadLocal使用。(2)明确父-子线程关系的前提下,继承(拷贝)父线程的线程本地变量缓存过的变量,而这个拷贝的时机是子线程Thread实例化时候进行的,也就是子线程实例化完毕后已经完成了InheritableThreadLocal变量的拷贝,这是一个变量传递的过程。
上面RequestRequestContextHolder源码中,就有InheritableThreadLocal。我们把setRequsetAttributes方法的第二个参数设置为true就好了。
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), ture);
那我们在调用子线程时,调用此方法就行了。修改的代码如下:
@RestController
public class ConsumerController {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerController.class);
@Autowired
private ProviderServiceFeign providerServiceFeign;
@GetMapping("/getHeader")
public String transferHeaders(HttpServletRequest request) throws Exception {
String deviceId = request.getHeader(name: "deviceId");
String token = request.getHeader(name: "token");
LOGGER.info("consumer服务中获取的请求头deviceId={},token={}", deviceId, token);
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
new Thread(new Runnable() {
@Override
public void run() {
providerServiceFeign.transferHeaders();
}
}).start();
return "success";
}
}
启动测试
打印结果:
[nio-8090-exec-1] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
[nio-8090-exec-1] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
[nio-8090-exec-3] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
[nio-8090-exec-3] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
[nio-8090-exec-4] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
[nio-8090-exec-4] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
[nio-8090-exec-5] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
[nio-8090-exec-5] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
[nio-8090-exec-6] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===null // <== 未获取到
[nio-8090-exec-6] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===null // <== 未获取到
[nio-8090-exec-7] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
[nio-8090-exec-7] c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
看起来好像是正确的,provider服务是能够获取到header参数的。但是仔细看仍有获取不到的情况。这就引出了下面的问题。
问题三
这里涉及到比较底层的知识了。
provider是有consumer服务调用的,而且是子线程发起的。我们解决了子线程获取主线程的属性的问题。
而当前的问题,本质原因就是主线程在子线程执行完之前就结束了。底层原理Servlet容器中Servlet属性的生命周期与接收请求的用户线程(父线程)同步,随着父线程执行完destroy()而销毁。
正常的理解,InheritableThreadLocal已经拷贝了变量,父线程销毁了应该不会有什么影响。但是我们再看一下源码RequestContextHolder中的setRequestAttributes()方法:
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequsetAttributesHolder.set(inheritable);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttribtesHolder.remove();
}
}
}
可以看到TreadLocal对象保存的是方法入参之一的RequestAttributes attributes。这里保存的是对象的引用。一旦父线程销毁了,那RequestAttributes也会被销毁,其引用地址的值也就是null了。虽然子线程也有RequestAttribtues的引用,但是引用的值是null。
我们再看一下消费端的代码
@GetMapping("/getHeader")
public String transferHeaders(HttpServletRequest request) throws Exception {
String deviceId = request.getHeader(name: "deviceId");
String token = request.getHeader(name: "token");
LOGGER.info("consumer服务中获取的请求头deviceId={},token={}", deviceId, token);
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
new Thread(new Runnable() {
@Override
public void run() { // <== 子线程调用provider服务
providerServiceFeign.transferHeaders();
}
}).start();
return "success"; // <== 主线程直接返回
}
跟前面讲的原理,我们就知道header的值时有时无的原因了:因为是异步,若主线程在子线程执行前之前就结束,子线程就无法获得header的值了。
解决方案
向子线程拷贝具体的值,而不是对象的引用。
具体设计:
核心思想是把header参数放到另外的ThreadLocal变量中,不采用原生的RequestAttributes。
/**
* 采用阿里的TTL组件,定义请求头部存储对象
* @Author gujiachun
*/
public class RequestHeaderHolder {
private static final ThreadLocal<String, String> COPY_ON_INHERIT_THREAD_LOCAL;
static {
COPY_ON_INHERIT_THREAD_LOCAL = (TransmittableThreadLocal) initiaValue() -> {
return new HashMap<>(50);
};
}
public static Map<String, String> get() {
return COPY_ON_INHERIT_THREAD_LOCAL.get();
}
public static String get(String key) {
return COPY_ON_INHERIT_THREAD_LOCAL.get().get(key);
}
public static void set(String key, String value) {
COPY_ON_INHERIT_THREAD_LOCAL.get().put(key, value);
}
public static void remove() {
COPY_ON_INHERIT_THREAD_LOCAL.remove();
}
}
定义RequestHeaderHolder对象,保存线程本地变量,上面的代码引用了阿里TTL组件中的TransmittableThreadLocal,可以认为是增强版的InheritableThreadLocal。
ps:也可采用原生的InheritableThreadLocal,在头部参数获取的场景中,是一样的。
/**
* 请求拦截器
* @Author gujiachun
*/
public class RequestHandlerInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(RequestHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 拦截请求,设置header到ThreadLocal中
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElement()) {
String name = headerNames.nextElement();
RequestHeaderHolder.set(name, request.getHeader(name));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LOG.info("============== RequestHeaderHolder remove =============")
// 一定要remove,否则有内存溢出隐患
RequestHeaderHolder.remove();
}
}
上面的代码就是请求拦截器把header参数赋值到RequestHeaderHolder对象中,这样就保证了每次的请求头部header值都在RequestHeaderHolder中。注意:一定要在afterCompletion方法中remove值,不然会有内存溢出隐患。
把请求拦截器注册到WebMvc中去:
@Configuration
public class WebConfig implements WebMvcConfigurer {
private static final Logger LOGGER = LoggerFactory.getLogger(WebConfig.class);
@Override
public void addInterceptors(InterceptorRegistry registry) {
LOGGER.info("============ 请求头拦截器注册 =============")
registry.addInterceptor(new RequestHandlerInterceptor()).addPathPatterns("/**");
}
}
注意:此处一定是实现WebMvcConfigurer,而不是网上说的WebMvcConfigurationSupport。使用后者会有个大坑。
我们来改造一下Feign请求拦截器
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Map<String, String> contextHeaderMap = RequestHeaderHolder().get();
if (contextHeaderMap != null && !contextHeaderMap.isEmpty()) {
String headerName = headerEntry.getKey();
if ("deviceId".equals(headerName) || "token".equals(headerName)) {
template.header(headerName, headerEntry.getValue());
}
}
}
}
启动测试
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的deviceId===1111
c.p.q.e.controller.ProviderController : provider服务中从请求头中获取的token===2222
未再出现获取不到的情况。
总结
上面主要介绍了Feign远程调用获取头部参数。可将其拓展到所有父子线程之间共享值的场景中。
阿里TTL组件maven引用:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.4</version>
</dependency>