共计 10771 个字符,预计需要花费 27 分钟才能阅读完成。
我的项目开发过程中,有没有很想定义一个全局变量,作用域针对于单次 request 申请,在整个申请过程中都能够随时获取。当应用 feign、dubbo 等做服务调用时,如果该变量的作用域还能传递到整个微服务链路,那就更好了。这就是本文想实现的成果,刚工作时基于 Oracle ADF 开发,就能够定义基于 RequestScope 作用域的变量。
在后面《微服务的全链路日志(Sleuth+MDC)》文章中,咱们实现了日志的全链路,原理是基于 spring cloud sleuth
和 MDC
的框架来实现 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 作用域内的容器。简略有两点:
Header
、Parameter
设计之初就不是用于做服务端容器的,所以它们通常只能在客户端赋值,在服务端 HttpServletRequest 也只提供了Getter
接口,而没有Setter
接口。但Attribute
就同时提供了Getter/Setter
接口。Header
、Parameter
存储对象的Value
都是String
字符串,也是不便客户端数据基于 HTTP 协定传输时不便。但Attribute
存储对象的Value
是Object
,也就更适宜寄存各种类型的对象。
那么在 Web 开发中,咱们日常是如何获取 HttpServletRequest 对象的呢?
2、获取 HttpServletRequest 的三种办法
-
在 Controller 的办法参数上写上 HttpServletRequest,这样每次申请过去失去就是对应的 HttpServletRequest。当 Service 等其余层须要用到时,就从 Controller 开始层层传递。很显著,保险,但代码看起来不太好看。
@GetMapping("/req") public void req(HttpServletRequest request) {...}
-
应用 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; }
-
间接通过 @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;
}
}
}
能够关注到两个重点:
RequestContextHolder
也是基于ThreadLocal
实现的,基于本地线程提供了Getter/Setter
办法,但如果跨线程则失落变量值。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、传递形式探讨
但这里就有个矛盾,咱们后面拿 Attribute
和 Header、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;
}
}