背景描述
需求對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
最合理方案 但是涉及各個項目組都要改動 影響大 難以推動