7

项目开发过程中,有没有很想定义一个全局变量,作用域针对于单次 request请求,在整个请求过程中都可以随时获取。当使用feign、dubbo等做服务调用时,如果该变量的作用域还能传递到整个微服务链路,那就更好了。这就是本文想实现的效果,刚工作时基于 Oracle ADF 开发,就可以定义基于 RequestScope 作用域的变量。

在前面《微服务的全链路日志(Sleuth+MDC)》文章中,我们实现了日志的全链路,原理是基于 spring cloud sleuthMDC 的框架来实现 traceId 等值的全程传递。本文算是姊妹篇,但一些实现的框架有所不同,本文是基于 spring 自带的 RequestContextHolder,和 servlet 的 HttpServletRequest 来实现的。

1. 单服务单线程实现

如果只希望实现单个服务内的作用域,而且整个API的逻辑内都是单线程,那么最容易想到的方案就是 ThreadLocal。定义一个 ThreadLocal 变量,在每一个API请求的时候赋值,在请求结束后清除,我们很多框架在AOP中处理这段逻辑。

但现在更容易,Spring框架自带的 RequestContextHolder 天然支持这么做。存放变量总要有提供 Getter/Setter 方法的容器吧,下面就介绍 HttpServletRequest

1.1. HttpServletRequest

HttpServletRequest 大家应该都不陌生,一次 API 请求中,所有客户端的请求都被封装成一个 HttpServletRequest 对象里。这个对象在 API 请求时创建,响应完成后销毁,就很适合作为 Request 作用域的容器。

1、Attribute 和 Header、Parameter

而往容器中投放和获取变量的方法,则可以用 HttpServletRequest 对象的 setAttribute/getAttribute 方法来实现。如今大家可能都对 Attribute比较陌生,它在早期 JSP 开发时用的比较多,用于 Servlet之间的值传递,不过用于当前场景也十分契合。

有人说那为啥不用 Header、Parameter 呢?它们也是 Request 作用域内的容器。简单有两点:

  1. HeaderParameter 设计之初就不是用于做服务端容器的,所以它们通常只能在客户端赋值,在服务端 HttpServletRequest 也只提供了 Getter接口,而没有 Setter接口。但 Attribute 就同时提供了 Getter/Setter 接口。
  2. HeaderParameter 存储对象的 Value 都是 String 字符串,也是方便客户端数据基于 HTTP 协议传输时方便。但 Attribute 存储对象的 ValueObject,也就更适合存放各种类型的对象。

那么在Web开发中,我们日常是如何获取 HttpServletRequest 对象的呢?

2、获取 HttpServletRequest 的三种方法
  1. 在 Controller 的方法参数上写上 HttpServletRequest,这样每次请求过来得到就是对应的 HttpServletRequest。当 Service 等其他层需要用到时,就从 Controller 开始层层传递。很明显,保险,但代码看起来不太美观。

    @GetMapping("/req")
    public void req(HttpServletRequest request) {...}
  2. 使用 RequestContextHolder,直接在需要用的地方使用如下方式取HttpServletRequest即可:

     public static HttpServletRequest getRequestByContext() {
         HttpServletRequest request = null;
         RequestAttributes ra = RequestContextHolder.getRequestAttributes();
         if (ra instanceof ServletRequestAttributes) {
             ServletRequestAttributes sra = (ServletRequestAttributes) ra;
             request = sra.getRequest();
         }
         return request;
     }
  3. 直接通过 @Autowired 获取 HttpServletRequest。

    @Autowired
    HttpServletRequest request;

其中,第2、第3种方式的原理是一致的。是因为 Spring框架在动态生成 HttpServletRequest Bean 的源码中,也是通过 RequestContextHolder.currentRequestAttributes() 来获取值,从而可以通过 @Autowired 注入。

下面就详细介绍一下 RequestContextHolder

1.2. RequestContextHolder

1、RequestContextHolder 工具类

我们先来看一下 RequestContextHolder 的源码:

public abstract class RequestContextHolder {
    private static final boolean jsfPresent = ClassUtils.isPresent("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");

    public RequestContextHolder() {
    }

    public static void resetRequestAttributes() {
        requestAttributesHolder.remove();
        inheritableRequestAttributesHolder.remove();
    }

    public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
        setRequestAttributes(attributes, false);
    }

    public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        } else if (inheritable) {
            inheritableRequestAttributesHolder.set(attributes);
            requestAttributesHolder.remove();
        } else {
            requestAttributesHolder.set(attributes);
            inheritableRequestAttributesHolder.remove();
        }

    }

    @Nullable
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
        if (attributes == null) {
            attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
        }

        return attributes;
    }

    public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
        RequestAttributes attributes = getRequestAttributes();
        if (attributes == null) {
            if (jsfPresent) {
                attributes = RequestContextHolder.FacesRequestAttributesFactory.getFacesRequestAttributes();
            }

            if (attributes == null) {
                throw new IllegalStateException("No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
            }
        }

        return attributes;
    }

    private static class FacesRequestAttributesFactory {
        private FacesRequestAttributesFactory() {
        }

        @Nullable
        public static RequestAttributes getFacesRequestAttributes() {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            return facesContext != null ? new FacesRequestAttributes(facesContext) : null;
        }
    }
}

可以关注到两个重点:

  1. RequestContextHolder 也是基于 ThreadLocal 实现的,基于本地线程提供了Getter/Setter方法,但如果跨线程则丢失变量值。
  2. RequestContextHolder 可以基于 InheritableThreadLocal 实现,从而实现也可以从子线程中获取当前线程的值。

这和上一篇文章中讲的 MDC 很像。RequestContextHolder 的工具类很简单,那么 Spring 框架是在哪里存放 RequestContextHolder 值,又在哪里销毁的呢?

2、Spring MVC 实现

我们看下 FrameworkServlet 这个类,里面有个 processRequest 方法,根据方法名称我们也可以大概了解到这个是方法用于处理请求的。
FrameworkServlet.java

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = this.buildLocaleContext(request);
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
        this.initContextHolders(request, localeContext, requestAttributes);

        try {
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
            failureCause = var17;
            throw new NestedServletException("Request processing failed", var17);
        } finally {
            this.resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }

            this.logResult(request, response, (Throwable)failureCause, asyncManager);
            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
        }

    }

this.doService(request, response); 是执行具体的业务逻辑,而我们关注的两个点则在这个方法的前后:

  • 设置当前请求 RequestContextHolder 值,this.initContextHolders(request, localeContext, requestAttributes); 对应方法代码如下:

      private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
          if (localeContext != null) {
              LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
          }
    
          if (requestAttributes != null) {
              RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
          }
    
      }
  • 当执行完成或抛出异常,则需要重置 RequestContextHolder 值,即清除掉当前 RequestContextHolder 值,设置为以前的值,this.resetContextHolders(request, previousLocaleContext, previousAttributes); 对应方法代码如下:

      private void resetContextHolders(HttpServletRequest request, @Nullable LocaleContext prevLocaleContext, @Nullable RequestAttributes previousAttributes) {
          LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);
          RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
      }

2. 单服务多线程实现

单个服务内,当我们有多线程的开发,如果希望在子线程内依然可以通过 RequestContextHolder 来获取 HttpServletRequest 该怎么办呢?

有人讲,前面 RequestContextHolder 的工具类中,不就提供 InheritableThreadLocal 的实现方式吗,不就可以实现需求了嘛。

这里明确不建议使用 InheritableThreadLocal 的实现方式,其实在上一篇文章中,也就提到过不建议用 InheritableThreadLocal 实现 MDC 的多线程传递。这里也一样,建议还是用 线程池的装饰器模式 来替代 InheritableThreadLocal。下面做一下对比,说明原因。

1、InheritableThreadLocal 的局限性

ThreadLocal 的局限性,就是不能在父子线程之间传递。 即在子线程中无法访问在父线程中设置的本地线程变量。 后来为了解决这个问题,引入了一个新的类 InheritableThreadLocal。

使用该方法后,子线程可以访问在 创建子线程时 父线程当时的本地线程变量,其实现原理就是在父线程创建子线程时将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

大家关注上文中加粗的几个字 “创建子线程时”,这就是 InheritableThreadLocal 的局限性。

众所周知,线程池的一大特点就是线程在创建后可回收,重复使用。这就意味着如果使用线程池创建线程,当使用 InheritableThreadLocal 时,只有新创建的线程可以正确的继承父线程的值,而后续重复使用的线程则不会更新值。

2、线程池的装饰模式

ThreadPoolTaskExecutor 类的 setTaskDecorator(TaskDecorator taskDecorator) 方法则没有上述的问题,因为它本身不是和线程 Thread 挂钩的,而是和 Runnable 挂钩。方法的官方注释是:

Specify a custom TaskDecorator to be applied to any Runnable about to be executed.

因此,对于想实现单服务多线程的传递时,建议仿照下列方式自定义线程池(还结合了 MDC的上下文继承):

    @Bean("customExecutor")
    public Executor getAsyncExecutor() {
        final RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.CallerRunsPolicy() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                log.warn("LOG:线程池容量不够,考虑增加线程数量,但更推荐将线程消耗数量大的程序使用单独的线程池");
                super.rejectedExecution(r, e);
            }
        };
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(7);
        threadPoolTaskExecutor.setMaxPoolSize(42);
        threadPoolTaskExecutor.setQueueCapacity(11);
        threadPoolTaskExecutor.setRejectedExecutionHandler(rejectedHandler);
        threadPoolTaskExecutor.setThreadNamePrefix("Custom Executor-");
        threadPoolTaskExecutor.setTaskDecorator(runnable -> {
            try {
                Optional<RequestAttributes> requestAttributesOptional = ofNullable(RequestContextHolder.getRequestAttributes());
                Optional<Map<String, String>> contextMapOptional = ofNullable(MDC.getCopyOfContextMap());
                return () -> {
                    try {
                        requestAttributesOptional.ifPresent(RequestContextHolder::setRequestAttributes);
                        contextMapOptional.ifPresent(MDC::setContextMap);
                        runnable.run();
                    } finally {
                        MDC.clear();
                        RequestContextHolder.resetRequestAttributes();
                    }
                };
            } catch (Exception e) {
                return runnable;
            }
        });
        return threadPoolTaskExecutor;
    }
3、sleuth 的 LazyTraceThreadPoolTaskExecutor 是否也会传递线程值

还记得上篇文章中 MDC 的子线程传递,当引入 sleuth 框架后,Spring 默认的线程池被替换为 LazyTraceThreadPoolTaskExecutor。此时不需要做上述装饰器的操作,默认线程池中的子线程就能继承 MDC 中 traceId 等值。

那么 LazyTraceThreadPoolTaskExecutor 能不能也让子线程继承父线程 RequestContextHolder 的值呢?

亲身试验过,不能!

3. 全链路多线程实现

全链路是针对微服务调用的场景,虽然原则上来讲,HttpServletRequest 应该只针对单次服务的请求到响应。但是由于现在微服务的流行,一次服务请求的链路往往会横跨多个服务。

基于上面的方法,我们是否可以实现请求作用域的变量,跨微服务传播?

1、传递方式探讨

但这里就有个矛盾,我们前面拿 AttributeHeader、Parameter做比较时就说过。前者适合在服务端容器内部传递值(Setter/Getter)。而后两者应该在客户端存放值(Setter),而在服务端获取(Getter)。

所以我的理解是:如果需要实现服务间的数据传递,建议数据量小的字符串可以通过Header传递(如:traceId等)。实际的数据,还是应该通过常规的 API 参数 Parameter 或请求体 Body 传递。

2、Header 传递的例子

这边有一个 Header 通过 Feign 拦截器传递的例子。Feign 支持自定义拦截器配置,可以在该配置类中读取上一个请求的值,然后再塞到下一个请求中。

HelloFeign.java

@FeignClient(
    name = "${hello-service.name}",
    url = "${hello-service.url}",
    path = "${hello-service.path}",
    configuration = {FeignRequestInterceptor.class}
)
public interface HelloFeign { ... }

FeignRequestInterceptor.java

@ConditionalOnClass({RequestInterceptor.class})
public class FeignRequestInterceptor implements RequestInterceptor {
    private static final String[] HEADER_KEYS = new String[]{"demo-key-1", "demo-key-2", "demo-key-3"};

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ofNullable(this.getRequestByContext())
                .ifPresent(request -> {
                    for (int i = 0; i < HEADER_KEYS.length; i++) {
                        String key = HEADER_KEYS[i];
                        String value = request.getHeader(key);
                        if (!Objects.isNull(value)) {
                            requestTemplate.header(key, new String[]{value});
                        }
                    }
                });
    }

    private HttpServletRequest getRequestByContext() {
        HttpServletRequest request = null;
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        if (ra instanceof ServletRequestAttributes) {
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            request = sra.getRequest();
        }
        return request;
    }
}

KerryWu
641 声望159 粉丝

保持饥饿


引用和评论

0 条评论