2021-04-30

微服务架构 - 头部参数传递获取问题解决方案

转自:今日头条作者:老顾聊技术 原文地址: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>

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值