一、简介

在微服务架构中,我们会有这样的需求,A服务调用B服务,B服务调用C服务,ABC服务都需要用到当前用户上下文信息(userId、orgId等),那么如何实现呢?

方案一: 拦截器加上ThreadLocal实现,但是如果在这次请求中创建了一个新的线程就拿不到了,也就是无法跨线程传递数据。

方案二: 使用拦截器加上 HystrixRequestContext 这个 request level 的 context实现,即保存到HystrixRequestContext中的数据在整个请求中都能访问。

二、使用

2.1代码示例

首先需要在pom文件引入依赖hystrix

<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.12</version>
</dependency>

保存上下文信息的对象ServiceContextHolder

package cn.sp.context;

import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault; /**
* Created by 2YSP on 2019/7/28.
*/
public class ServiceContextHolder { private static final HystrixRequestVariableDefault<ServiceContext> context = new HystrixRequestVariableDefault<>(); public static ServiceContext getServiceContext() {
initServiceContext();
return context.get();
} public static void setServiceContext(ServiceContext serviceContext) {
initServiceContext();
context.set(serviceContext);
} private static void initServiceContext() {
if (!HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.initializeContext();
}
} public static void destroy() {
if (HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.getContextForCurrentThread().shutdown();
}
}
}

ServiceContextInterceptor的作用是将请求头中的userId保存到上下文对象中。

@Slf4j
public class ServiceContextInterceptor extends HandlerInterceptorAdapter { @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
initServiceContext(request, request.getRequestURL().toString());
return true;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
ServiceContextHolder.destroy();
} private void initServiceContext(HttpServletRequest request, String url) {
ServiceContext serviceContext = new ServiceContext();
String userId = request.getHeader("userId");
serviceContext.setUserId(Long.valueOf(userId));
ServiceContextHolder.setServiceContext(serviceContext);
}
}

添加拦截器配置

@Configuration
@EnableWebMvc
@Import(value = {RestResponseBodyAdvice.class})
public class MvcConfig implements WebMvcConfigurer { @Bean
public ServiceContextInterceptor getServiceContextInterceptor() {
return new ServiceContextInterceptor();
} @Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getServiceContextInterceptor()).addPathPatterns("/request-context/**");
} }

用于测试的RequestContextTestController

@RestController
@RequestMapping("request-context")
@Slf4j
public class RequestContextTestController { @RequestMapping(value = "test", method = RequestMethod.GET)
public String test() {
System.out.println("请求的用户id:" + ServiceContextHolder.getServiceContext().getUserId() + ""); HystrixContextRunnable runnable =
new HystrixContextRunnable(() -> {
//从新的线程中获取当前用户id
ServiceContext context = ServiceContextHolder.getServiceContext();
System.out.println("新线程的用户id:" + context.getUserId());
context.setUserId(110L);
}); new Thread(runnable).start(); try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ServiceContextHolder.getServiceContext().getUserId() + "";
}
}

注意: 只有使用HystrixContextRunnable或HystrixContextCallable创建线程才能在线程间传递数据,JDK自带的是无效的。

2.2测试

使用postman发送请求

请求示例

请求头中的userId是22,返回结果却变成110,说明在新线程中改变了ServiceContextHolder中保存的userId。

控制台日志如下:

请求的用户id:22
2019-08-31 14:25:29.787 [http-nio-80-exec-1] WARN c.n.c.sources.URLConfigurationSource - No URLs will be polled as dynamic configuration sources.
2019-08-31 14:25:29.787 [http-nio-80-exec-1] INFO c.n.c.sources.URLConfigurationSource - To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
2019-08-31 14:25:29.798 [http-nio-80-exec-1] INFO c.n.config.DynamicPropertyFactory - DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@a6f6807
新线程的用户id:22

说明新线程也能获取到ServiceContextHolder中的数据,这种又是怎么实现的呢?下面介绍原理。

三、原理

上下文信息其实是保存在HystrixRequestVariableDefault类型的变量中,所以先看看这个类的源码。

HystrixRequestVariableDefault是HystrixRequestVariable接口的实现类,HystrixRequestVariable接口表示request level的属性,仅提供了get()来获取属性。

public interface HystrixRequestVariable<T> extends HystrixRequestVariableLifecycle<T> {

    public T get();

}

HystrixRequestVariableDefault和ThreadLocal一样,提供了 T get() 和 set(T value) 两个工具方法。

public class HystrixRequestVariableDefault<T> implements HystrixRequestVariable<T> {
static final Logger logger = LoggerFactory.getLogger(HystrixRequestVariableDefault.class); @SuppressWarnings("unchecked")
public T get() {
if (HystrixRequestContext.getContextForCurrentThread() == null) {
throw new IllegalStateException(HystrixRequestContext.class.getSimpleName() + ".initializeContext() must be called at the beginning of each request before RequestVariable functionality can be used.");
}
// 拿到当前线程的存储结构,以自己为key索引数据
ConcurrentHashMap<HystrixRequestVariableDefault<?>, LazyInitializer<?>> variableMap = HystrixRequestContext.getContextForCurrentThread().state; // short-circuit the synchronized path below if we already have the value in the ConcurrentHashMap
LazyInitializer<?> v = variableMap.get(this);
...
} public void set(T value) {
// 拿到当前线程的存储结构,以自己为key来存储实际的数据。
HystrixRequestContext.getContextForCurrentThread().state.put(this, new LazyInitializer<T>(this, value));
} }

set/get方法都调用了HystrixRequestContext的方法完成的,HystrixRequestContext的部分源码如下:

public class HystrixRequestContext implements Closeable {

   //每个线程的ThreadLocal将保存HystrixRequestVariableState
private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>(); // 当前线程是否初始化了HystrixRequestContext
public static boolean isCurrentThreadInitialized() {
HystrixRequestContext context = requestVariables.get();
return context != null && context.state != null;
} // 从当前线程获取HystrixRequestContext
public static HystrixRequestContext getContextForCurrentThread() {
HystrixRequestContext context = requestVariables.get();
if (context != null && context.state != null) { return context;
} else {
return null;
}
} public static void setContextOnCurrentThread(HystrixRequestContext state) {
requestVariables.set(state);
} // 在每个请求开始的时候调用此方法,创建一个HystrixRequestContext,并与当前线程关联
public static HystrixRequestContext initializeContext() {
HystrixRequestContext state = new HystrixRequestContext();
requestVariables.set(state);
return state;
} ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>> state = new ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>>(); }

可以看出实际数据是存储在state这个ConcurrentHashMap中的,每个线程关联一个HystrixRequestContext,每个HystrixRequestContext有个Map结构存储数据,key就是HystrixRequestVariableDefault。

如何实现request level context?

HystrixContextRunnable源码如下:

// HystrixContextRunnable是个Runnable,一个可用于执行的任务
public class HystrixContextRunnable implements Runnable { private final Callable<Void> actual;
private final HystrixRequestContext parentThreadState; public HystrixContextRunnable(Runnable actual) {
this(HystrixPlugins.getInstance().getConcurrencyStrategy(), actual);
} public HystrixContextRunnable(HystrixConcurrencyStrategy concurrencyStrategy, final Runnable actual) {
// 获取当前线程的HystrixRequestContext
this(concurrencyStrategy, HystrixRequestContext.getContextForCurrentThread(), actual);
} // 关键的构造器
public HystrixContextRunnable(final HystrixConcurrencyStrategy concurrencyStrategy, final HystrixRequestContext hystrixRequestContext, final Runnable actual) { // 将原始任务Runnable包装成Callable, 创建了一个新的callable
this.actual = concurrencyStrategy.wrapCallable(new Callable<Void>() {
@Override
public Void call() throws Exception {
actual.run();
return null;
}
});
// 存储当前线程的hystrixRequestContext
this.parentThreadState = hystrixRequestContext;
} @Override
public void run() {
// 运行实际的Runnable之前先保存当前线程已有的HystrixRequestContext
HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread();
try {
// 设置当前线程的HystrixRequestContext,来自上一级线程,因此两个线程是同一个HystrixRequestContext
HystrixRequestContext.setContextOnCurrentThread(parentThreadState);
try {
actual.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
} finally {
// 还原当前线程的HystrixRequestContext
HystrixRequestContext.setContextOnCurrentThread(existingState);
}
}
}

代码地址

HystrixRequestContext实现Request级别的上下文的更多相关文章

  1. Flask Markup 上下文,request

    在模板渲染中,使用Markup转换变量中的特殊字符 from flask import Markup Markup函数对字符串进行转移处理再传递给render_template()函数 在浏览器中显示 ...

  2. Flask 上下文管理-- (session,request,current_app的传递)--类似本地线程实现,以及多app应用

    Flask session,request,current_app的传递 请求上下文的作用 -- 封装请求相关得数据(request,session) 请求上下文 request session re ...

  3. Flask 上下文(Context)原理解析

    :first-child { margin-top: 0; } blockquote > :last-child { margin-bottom: 0; } img { border: 0; m ...

  4. 第三节:EF Core上下文DbContext相关配置和生命周期

    一. 配置相关 1. 数据库连接字符串的写法 (1).账号密码:Server=localhost;Database=EFDB01;User ID=sa;Password=123456; (2).win ...

  5. python2.7高级编程 笔记一(Python中的with语句与上下文管理器学习总结)

    0.关于上下文管理器上下文管理器是可以在with语句中使用,拥有__enter__和__exit__方法的对象. with manager as var: do_something(var) 相当于以 ...

  6. Request中的各种方法

    前言 Request中方法众多,对于Java Web程序员来说,种种方法都会在工作中常常用到.Request由于不是JDK的一部分,这些方法的用法也没有专门的API可以查,所以在工作中遇到Reques ...

  7. 15.SpringMVC和Spring上下文关系(为什么SpringMVC可以调用到Spring)

    springmvc上下文继承于spring, 也就是springmvc的上下文可访问spring上下文,在springmvc的上下文中可取得spring bean. spring上下文是spring启 ...

  8. spring和springMVC的上下文

    上下文可以替代注解, 但是注解更方便 package com.tgb.web.controller; import javax.annotation.Resource; import javax.se ...

  9. request 的介绍使用属性

    上下文:相当于一个容器,保存了 Flask 程序运行过程中的一些信息. Flask中有两种上下文,请求上下文和应用上下文 请求上下文(request context) 在 flask 中,可以直接在视 ...

随机推荐

  1. String、StringBUffer和StringBuilder的区别与使用

    一.区别 String是一个不可变的类,即创建String对象后,该对象中的字符串是不可变的,平时我们改变String对象中的字符串实际上是通过StringBuffer实现的,所以StringBuff ...

  2. 2016 ACM/ICPC ECNA Regional I.Waif Until Dark(最大流)

    这是一道ECNA的16年题,问有n个小朋友,m个玩具,不同孩子有不同喜好的玩具,每个玩具可能属于一个类别,同一类别的玩具最多只能用一定次数,问最大匹配 这个就很裸的二分图,掏出dinic板子,首先最后 ...

  3. 关于moviepy打包报错AttributeError: module audio/video.fx.all has no attribute fadein、crop文章的纠错和抄袭

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 老猿前面有篇文章<moviepy应用pyin ...

  4. 转:解析HTTP协议六种请求方法,get,head,put,delete,post有什么区别

    解析HTTP协议六种请求方法,get,head,put,delete,post有什么区别 标准Http协议支持六种请求方法,即: 1.GET 2.POST 3.PUT 4.Delete 5.HEAD ...

  5. PyQt学习随笔:ListView控件增加列表项

    ListView控件如果需要增加列表项,就是在对应数据存储中插入项,这又分两种情况,一种是已知列表数据存储,一种是未知数据存储.如果是未知数据存储,可以通过: ListView控件名.model() ...

  6. hihocoder 1489(微软2017, 数学,模拟)

    题目链接:http://hihocoder.com/problemset/problem/1489?sid=1587434 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 ...

  7. tomcat-1-介绍篇

    java语言分为三个体系: javase javaee,是javase的基础 一般就是指jdk javaee java的企业版本 其实是一套规范,就是用java语言做企业开发(目前看来就是开发一些动态 ...

  8. Vue开发中的移动端适配(px转换成vw)

    1.项目根目录下,创建 .postcssrc.js 文件. 2.安装插件. -D (开发依赖) postcss-import postcss-url cssnano-preset-advanced - ...

  9. KafkaMirrorMaker 的不足以及一些改进

    背景 某系统使用 Kafka 存储实时的行情数据,为了保证数据的实时性,需要在多地机房维护多个 Kafka 集群,并将行情数据同步到这些集群上. 一个常用的方案就是官方提供的 KafkaMirrorM ...

  10. Navicat Primium连接数据库报ORA-28547错误

    这个问题主要是Navicat Primium与orecal中的oci.dll版本不一致造成的,无论是本地数据库或者网络数据库. 解决方法:在数据库orecal安装目录中搜索oci.dll文件,找到后将 ...