Commit 86137492b86680cdcc9d1b5ac37e749fd6903e1e
1 parent
e585349a
fix(data-permission): 隔离并发任务的权限上下文
Showing
2 changed files
with
168 additions
and
1 deletions
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java
| @@ -15,9 +15,13 @@ public class DataPermissionContextHolder { | @@ -15,9 +15,13 @@ public class DataPermissionContextHolder { | ||
| 15 | 15 | ||
| 16 | /** | 16 | /** |
| 17 | * 使用 List 的原因,可能存在方法的嵌套调用 | 17 | * 使用 List 的原因,可能存在方法的嵌套调用 |
| 18 | + * | ||
| 19 | + * 1. 直接创建子线程时,不继承,使用空 List。避免线程池中懒创建的线程,永久保留创建时的 DataPermission | ||
| 20 | + * 2. TtlRunnable、TtlCallable 传递时,复制一份 List。避免父子线程共享同一个 List,导致子线程的入栈、出栈影响父线程 | ||
| 18 | */ | 21 | */ |
| 19 | private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS = | 22 | private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS = |
| 20 | - TransmittableThreadLocal.withInitial(LinkedList::new); | 23 | + TransmittableThreadLocal.withInitialAndCopier(LinkedList::new, |
| 24 | + parentValue -> new LinkedList<>(), parentValue -> new LinkedList<>(parentValue)); | ||
| 21 | 25 | ||
| 22 | /** | 26 | /** |
| 23 | * 获得当前的 DataPermission 注解 | 27 | * 获得当前的 DataPermission 注解 |
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java
| 1 | package cn.iocoder.yudao.framework.datapermission.core.aop; | 1 | package cn.iocoder.yudao.framework.datapermission.core.aop; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; | 3 | import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
| 4 | +import com.alibaba.ttl.TtlRunnable; | ||
| 4 | import org.junit.jupiter.api.BeforeEach; | 5 | import org.junit.jupiter.api.BeforeEach; |
| 5 | import org.junit.jupiter.api.Test; | 6 | import org.junit.jupiter.api.Test; |
| 6 | 7 | ||
| 8 | +import java.util.ArrayList; | ||
| 9 | +import java.util.Arrays; | ||
| 10 | +import java.util.List; | ||
| 11 | +import java.util.concurrent.Callable; | ||
| 12 | +import java.util.concurrent.CountDownLatch; | ||
| 13 | +import java.util.concurrent.ExecutorService; | ||
| 14 | +import java.util.concurrent.Executors; | ||
| 15 | +import java.util.concurrent.Future; | ||
| 16 | +import java.util.concurrent.FutureTask; | ||
| 17 | +import java.util.concurrent.TimeUnit; | ||
| 18 | +import java.util.concurrent.atomic.AtomicReference; | ||
| 19 | + | ||
| 20 | +import static java.util.Collections.singletonList; | ||
| 7 | import static org.junit.jupiter.api.Assertions.assertEquals; | 21 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 22 | +import static org.junit.jupiter.api.Assertions.assertNotSame; | ||
| 23 | +import static org.junit.jupiter.api.Assertions.assertNull; | ||
| 8 | import static org.junit.jupiter.api.Assertions.assertSame; | 24 | import static org.junit.jupiter.api.Assertions.assertSame; |
| 25 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 9 | import static org.mockito.Mockito.mock; | 26 | import static org.mockito.Mockito.mock; |
| 10 | 27 | ||
| 11 | /** | 28 | /** |
| @@ -15,6 +32,11 @@ import static org.mockito.Mockito.mock; | @@ -15,6 +32,11 @@ import static org.mockito.Mockito.mock; | ||
| 15 | */ | 32 | */ |
| 16 | class DataPermissionContextHolderTest { | 33 | class DataPermissionContextHolderTest { |
| 17 | 34 | ||
| 35 | + private static final long TIMEOUT_SECONDS = 5; | ||
| 36 | + | ||
| 37 | + private static final DataPermission ENABLE = EnableClass.class.getAnnotation(DataPermission.class); | ||
| 38 | + private static final DataPermission DISABLE = DisableClass.class.getAnnotation(DataPermission.class); | ||
| 39 | + | ||
| 18 | @BeforeEach | 40 | @BeforeEach |
| 19 | public void setUp() { | 41 | public void setUp() { |
| 20 | DataPermissionContextHolder.clear(); | 42 | DataPermissionContextHolder.clear(); |
| @@ -63,4 +85,145 @@ class DataPermissionContextHolderTest { | @@ -63,4 +85,145 @@ class DataPermissionContextHolderTest { | ||
| 63 | assertEquals(1, DataPermissionContextHolder.getAll().size()); | 85 | assertEquals(1, DataPermissionContextHolder.getAll().size()); |
| 64 | } | 86 | } |
| 65 | 87 | ||
| 88 | + @Test | ||
| 89 | + public void testTtlRunnable_preStartedExecutor() throws Exception { | ||
| 90 | + ExecutorService executor = Executors.newSingleThreadExecutor(); | ||
| 91 | + CountDownLatch entered = new CountDownLatch(1); | ||
| 92 | + CountDownLatch release = new CountDownLatch(1); | ||
| 93 | + try { | ||
| 94 | + // 准备参数:预启动线程池的线程,此时父线程还没有 DataPermission,不会通过 InheritableThreadLocal 继承 | ||
| 95 | + executor.submit(() -> {}).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 96 | + DataPermissionContextHolder.add(ENABLE); | ||
| 97 | + AtomicReference<List<DataPermission>> childStack = new AtomicReference<>(); | ||
| 98 | + | ||
| 99 | + // 调用:子任务入栈 enable = false 后暂停 | ||
| 100 | + Future<?> future = executor.submit(TtlRunnable.get(() -> { | ||
| 101 | + DataPermissionContextHolder.add(DISABLE); | ||
| 102 | + childStack.set(DataPermissionContextHolder.getAll()); | ||
| 103 | + entered.countDown(); | ||
| 104 | + try { | ||
| 105 | + await(release); | ||
| 106 | + } finally { | ||
| 107 | + DataPermissionContextHolder.remove(); | ||
| 108 | + } | ||
| 109 | + })); | ||
| 110 | + await(entered); | ||
| 111 | + // 断言:子任务继承父线程的 DataPermission,但父线程不受子任务入栈影响 | ||
| 112 | + assertNotSame(DataPermissionContextHolder.getAll(), childStack.get()); | ||
| 113 | + assertEquals(Arrays.asList(ENABLE, DISABLE), childStack.get()); | ||
| 114 | + assertEquals(singletonList(ENABLE), DataPermissionContextHolder.getAll()); | ||
| 115 | + assertTrue(DataPermissionContextHolder.get().enable()); | ||
| 116 | + | ||
| 117 | + // 调用:子任务出栈并结束 | ||
| 118 | + release.countDown(); | ||
| 119 | + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 120 | + // 断言:父线程不受子任务出栈影响 | ||
| 121 | + assertEquals(singletonList(ENABLE), DataPermissionContextHolder.getAll()); | ||
| 122 | + // 断言:线程池的线程,执行完后恢复为原上下文 | ||
| 123 | + assertNull(executor.submit((Callable<DataPermission>) DataPermissionContextHolder::get) | ||
| 124 | + .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); | ||
| 125 | + } finally { | ||
| 126 | + release.countDown(); | ||
| 127 | + executor.shutdownNow(); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + @Test | ||
| 132 | + public void testTtlRunnable_captureSnapshot() throws Exception { | ||
| 133 | + ExecutorService executor = Executors.newSingleThreadExecutor(); | ||
| 134 | + try { | ||
| 135 | + // 准备参数:预启动线程池的线程 | ||
| 136 | + executor.submit(() -> {}).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 137 | + DataPermissionContextHolder.add(ENABLE); | ||
| 138 | + AtomicReference<List<DataPermission>> childValues = new AtomicReference<>(); | ||
| 139 | + Runnable task = TtlRunnable.get(() -> childValues.set(new ArrayList<>(DataPermissionContextHolder.getAll()))); | ||
| 140 | + | ||
| 141 | + // 调用:捕获后,父线程嵌套入栈,再执行子任务 | ||
| 142 | + DataPermissionContextHolder.add(DISABLE); | ||
| 143 | + executor.submit(task).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 144 | + // 断言:子任务使用捕获时的 DataPermission,不受父线程后续入栈影响 | ||
| 145 | + assertEquals(singletonList(ENABLE), childValues.get()); | ||
| 146 | + | ||
| 147 | + // 调用 + 断言:父线程嵌套出栈,出栈至空后清理上下文 | ||
| 148 | + assertSame(DISABLE, DataPermissionContextHolder.remove()); | ||
| 149 | + assertSame(ENABLE, DataPermissionContextHolder.remove()); | ||
| 150 | + assertTrue(DataPermissionContextHolder.getAll().isEmpty()); | ||
| 151 | + } finally { | ||
| 152 | + executor.shutdownNow(); | ||
| 153 | + } | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + @Test | ||
| 157 | + public void testChildThread() throws Exception { | ||
| 158 | + // 准备参数 | ||
| 159 | + DataPermissionContextHolder.add(ENABLE); | ||
| 160 | + CountDownLatch entered = new CountDownLatch(1); | ||
| 161 | + CountDownLatch release = new CountDownLatch(1); | ||
| 162 | + AtomicReference<List<DataPermission>> childStack = new AtomicReference<>(); | ||
| 163 | + FutureTask<Void> task = new FutureTask<>(() -> { | ||
| 164 | + // 断言:直接创建的子线程,不继承父线程的 DataPermission,即默认开启数据权限 | ||
| 165 | + assertNull(DataPermissionContextHolder.get()); | ||
| 166 | + DataPermissionContextHolder.add(DISABLE); | ||
| 167 | + childStack.set(DataPermissionContextHolder.getAll()); | ||
| 168 | + entered.countDown(); | ||
| 169 | + await(release); | ||
| 170 | + // 出栈至空,清理子线程的上下文 | ||
| 171 | + assertSame(DISABLE, DataPermissionContextHolder.remove()); | ||
| 172 | + assertNull(DataPermissionContextHolder.get()); | ||
| 173 | + return null; | ||
| 174 | + }); | ||
| 175 | + | ||
| 176 | + // 调用:直接创建子线程,子线程入栈 enable = false 后暂停 | ||
| 177 | + new Thread(task).start(); | ||
| 178 | + try { | ||
| 179 | + await(entered); | ||
| 180 | + // 断言:父线程不受子线程入栈影响 | ||
| 181 | + assertNotSame(DataPermissionContextHolder.getAll(), childStack.get()); | ||
| 182 | + assertEquals(singletonList(ENABLE), DataPermissionContextHolder.getAll()); | ||
| 183 | + assertTrue(DataPermissionContextHolder.get().enable()); | ||
| 184 | + } finally { | ||
| 185 | + release.countDown(); | ||
| 186 | + } | ||
| 187 | + task.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 188 | + // 断言:父线程不受子线程出栈、清理影响 | ||
| 189 | + assertEquals(singletonList(ENABLE), DataPermissionContextHolder.getAll()); | ||
| 190 | + assertTrue(DataPermissionContextHolder.get().enable()); | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + @Test | ||
| 194 | + public void testUnmanagedWorker_notRetainParent() throws Exception { | ||
| 195 | + ExecutorService executor = Executors.newSingleThreadExecutor(); | ||
| 196 | + try { | ||
| 197 | + // 准备参数:在 enable = false 的作用域内,懒创建线程池的线程,且任务没有使用 TtlRunnable 包装 | ||
| 198 | + DataPermissionContextHolder.add(DISABLE); | ||
| 199 | + assertNull(executor.submit((Callable<DataPermission>) DataPermissionContextHolder::get) | ||
| 200 | + .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); | ||
| 201 | + // 调用:父线程退出作用域 | ||
| 202 | + assertSame(DISABLE, DataPermissionContextHolder.remove()); | ||
| 203 | + | ||
| 204 | + // 断言:线程池的线程,没有永久保留创建时的 enable = false,仍为默认开启数据权限 | ||
| 205 | + assertNull(executor.submit((Callable<DataPermission>) DataPermissionContextHolder::get) | ||
| 206 | + .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); | ||
| 207 | + } finally { | ||
| 208 | + executor.shutdownNow(); | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + private static void await(CountDownLatch latch) { | ||
| 213 | + try { | ||
| 214 | + assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); | ||
| 215 | + } catch (InterruptedException e) { | ||
| 216 | + Thread.currentThread().interrupt(); | ||
| 217 | + throw new IllegalStateException(e); | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + @DataPermission | ||
| 222 | + private static class EnableClass { | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + @DataPermission(enable = false) | ||
| 226 | + private static class DisableClass { | ||
| 227 | + } | ||
| 228 | + | ||
| 66 | } | 229 | } |