背景描述
需求对Dubbo Provider的返回的数据做一层数据过滤 即Dubbo Provider 接口返回的数据必须和Web层用户登录的信息相同 实现方案比较简单 使用Dubbo官方提供的Filter 机制即可
实现方案
使用Dubbo官方提供的Filter 机制即可 在Web层自定义Dubbo Filter 过滤Dubbo Provider返回的结果信息 然后获取用户上下文信息 比对返回结果的数据是否和用户上下文一致
问题描述
当使用Java8 parallerStream调用Dubbo Provider时会发生用户下文信息错乱的问题 当Controller中使用parallerStream并行调用Dubbo Provider接口时 Dubbo Filter过滤Provider返回的结果时 发现和当前用户上下文不一致
问题排查&定位
- 首先排查Dubbo Filter中获取用户上下文的方式
- 项目使用的是Shiro框架 其获取用户上下文代码如下
- org.apache.shiro.SecurityUtils#getSubject
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
- org.apache.shiro.SecurityUtils#getSubject
- 看到这里可知Shiro将用户上下文存放在线程上下文ThreadLocal中
? 其实当时看到这段代码我是很疑惑的 因为parallerStream是基于ForkJoinPooll来实现的 如果使用了parallerStream 那么在Dubbo Filter我们获取到的用户上下文应该是空的 因为Shiro框架是运行在Tomcat 线程中的 ForkJoinPoll的线程池肯定和Tomcat的线程池不是同一个
- 带上疑惑继续跟一下Shiro的源码
- org.apache.shiro.util.ThreadContext#getSubject
- org.apache.shiro.util.ThreadContext#get
- org.apache.shiro.util.ThreadContext#getValue
private static Object getValue(Object key) { // 存储用户上下文的容器找到了 resource Map<Object, Object> perThreadResources = resources.get(); return perThreadResources != null ? perThreadResources.get(key) : null; }
- org.apache.shiro.util.ThreadContext#resources
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
- 进入resources 看看它的实现
private static final class InheritableThreadLocalMap<T extends Map<Object, Object>> extends InheritableThreadLocal<Map<Object, Object>> { ... }
- Shiro使用ThreadLocal子类InheritableThreadLocal来存放用户上下文
? 为什么InheritableThreadLocal会导致并行流中ForkJoinPoll池中的线程能获取到用户上下文呢
- 带着疑问查看InheritableThreadLocalMap的源码
public class InheritableThreadLocal<T> extends ThreadLocal<T> { public InheritableThreadLocal() { } protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
只是简单重写了下几个方法但是这个java.lang.Thread#inheritableThreadLocals引起了注意 java.lang.Thread#inheritableThreadLocals 是Thread中的变量 右键使用Find Usages查看那些地方使用了该变量
主要关注Value write 那些地方对变量进行了赋值 同样的方法init方法是创建线程时的初始化方法
- 项目使用的是Shiro框架 其获取用户上下文代码如下
- 分析java.lang.InheritableThreadLocal在线程创建时作用是什么
-
init方法418-420行
java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
// 如果 parent.inheritableThreadLocals 不为空 则将父线程的inheritableThreadLocals赋值给当前线程的inheritableThreadLocals if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
-
init方法374行
//父线程就是调用new Thread(...)构造方法的线程 Thread parent = currentThread();
-
说明Java8 parallerStream使用的ForkJoinPoll池中的线程创建时继承Tomcat线程池中的InheritableThreadLocal即用户上下文信息
-
问题原因
-
至此疑惑解开了
为什么InheritableThreadLocal会导致并行流中ForkJoinPoll池中的线程能获取到用户上下文? 当使用Java8 parallerStream调用Dubbo Provider时 此时Dubbo Filter运行在ForkJoinPoll中 当前线程就是ForkJoinPoll中线程 当取获取用户上下文时 就可能获取到错误的用户上下文信息
-
这里还有一个细节即 Java8 parallerStream使用的ForkJoinPoll是在什么时候创建的 ?
如果直接使用集合.parallerStream... 第一次使用就会创建ForkJoinPoll 并且在同一个JVM进程中共享同一个
// java.util.concurrent.ForkJoinPool#common /** * Common (static) pool. Non-null for public use unless a static * construction exception, but internal usages null-check on use * to paranoically avoid potential initialization circularities * as well as to simplify generated code. */ static final ForkJoinPool common;
问题解决
- 方案1 禁用Java8 parallelStream 调用Dubbo Provider
是强制不让使用倒是满足业务需求 但是ForkJoinPoll线程池 它的工作窃取算法 和 每个Workder 一个 Queue 在并行网络请求上表现不错
- 方案2 在Dubbo Filter中 添加如下代码
//并行流不进行用户上下文过滤逻辑 if(Thread.currentThread() instanceof ForkJoinWorkerThread){ return invoker.invoke(invocation); }
只是做了兼容方法 并不能过滤并行流调用Dubbo Provider 业务上是所有Dubbo Provider接口的返回结果都要过滤
- 方案3
- 在Dubbo Filter中不直接使用Shiro获取用户上下文
- 将UserContext作为一个对象封装到所有Dubbo Provider接口参数中 可封装一个BaseDubboRequestParam
- 然后在Dubbo Filter中从接口参数中中获取UserContext
最合理方案 但是涉及各个项目组都要改动 影响大 难以推动