diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index d8d982ba3..980e09705 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -40,6 +40,7 @@
7.2.6.RELEASE
0.1.16
+ 3.6.28
1.18.20
1.4.1.Final
@@ -312,6 +313,13 @@
cn.iocoder.boot
yudao-spring-boot-starter-test
${revision}
+ test
+
+
+
+ org.mockito
+ mockito-inline
+ ${mockito-inline.version}
@@ -323,6 +331,10 @@
asm
org.ow2.asm
+
+ org.mockito
+ mockito-core
+
diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java
index c3cc4d9d9..e7fd69bac 100644
--- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java
+++ b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
@@ -48,6 +49,7 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne
private final DataPermissionRuleFactory ruleFactory;
+ @Getter
private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
@Override // SELECT 场景
@@ -442,13 +444,14 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne
*
* @author 芋道源码
*/
- private static final class MappedStatementCache {
+ static final class MappedStatementCache {
/**
- * 无需重写的映射
+ * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存
*
* value:{@link MappedStatement#getId()} 编号
*/
+ @Getter
private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>();
/**
@@ -467,7 +470,7 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne
// 任一规则不在 noRewritableMap 中,则说明可能需要重写
for (DataPermissionRule rule : rules) {
Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
- if (!CollUtil.contains(mappedStatementIds, ms.getId())) { // 不存在,则说明可能要重写
+ if (!CollUtil.contains(mappedStatementIds, ms.getId())) {
return false;
}
}
@@ -491,6 +494,14 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne
}
}
+ /**
+ * 清空缓存
+ * 目前主要提供给单元测试
+ */
+ public void clear() {
+ noRewritableMappedStatements.clear();
+ }
+
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptorTest.java
new file mode 100644
index 000000000..3946f5a49
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptorTest.java
@@ -0,0 +1,190 @@
+package cn.iocoder.yudao.framework.datapermission.core.interceptor;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.schema.Column;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+
+import java.sql.Connection;
+import java.util.*;
+
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link DataPermissionInterceptor} 的单元测试
+ * 主要测试 {@link DataPermissionInterceptor#beforePrepare(StatementHandler, Connection, Integer)}
+ * 和 {@link DataPermissionInterceptor#beforeUpdate(Executor, MappedStatement, Object)}
+ * 以及在这个过程中,ContextHolder 和 MappedStatementCache
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionInterceptorTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private DataPermissionInterceptor interceptor;
+
+ @Mock
+ private DataPermissionRuleFactory ruleFactory;
+
+ @BeforeEach
+ public void setUp() {
+ // 清理上下文
+ DataPermissionInterceptor.ContextHolder.clear();
+ // 清空缓存
+ interceptor.getMappedStatementCache().clear();
+ }
+
+ @Test // 不存在规则,且不匹配
+ public void testBeforeQuery_withoutRule() {
+ try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) {
+ // 准备参数
+ MappedStatement mappedStatement = mock(MappedStatement.class);
+ BoundSql boundSql = mock(BoundSql.class);
+
+ // 调用
+ interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+ // 断言
+ pluginUtilsMock.verify(never(), () -> PluginUtils.mpBoundSql(boundSql));
+ }
+ }
+
+ @Test // 存在规则,且不匹配
+ public void testBeforeQuery_withMatchRule() {
+ try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) {
+ // 准备参数
+ MappedStatement mappedStatement = mock(MappedStatement.class);
+ BoundSql boundSql = mock(BoundSql.class);
+ // mock 方法(数据权限)
+ when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
+ .thenReturn(singletonList(new DeptDataPermissionRule()));
+ // mock 方法(MPBoundSql)
+ PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+ pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+ // mock 方法(SQL)
+ String sql = "select * from t_user where id = 1";
+ when(mpBs.sql()).thenReturn(sql);
+ // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
+
+ // 调用
+ interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+ // 断言
+ verify(mpBs, times(1)).sql(
+ eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100"));
+ // 断言缓存
+ assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
+ }
+ }
+
+ @Test // 存在规则,但不匹配
+ public void testBeforeQuery_withoutMatchRule() {
+ try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) {
+ // 准备参数
+ MappedStatement mappedStatement = mock(MappedStatement.class);
+ BoundSql boundSql = mock(BoundSql.class);
+ // mock 方法(数据权限)
+ when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
+ .thenReturn(singletonList(new DeptDataPermissionRule()));
+ // mock 方法(MPBoundSql)
+ PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+ pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+ // mock 方法(SQL)
+ String sql = "select * from t_role where id = 1";
+ when(mpBs.sql()).thenReturn(sql);
+ // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
+
+ // 调用
+ interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+ // 断言
+ verify(mpBs, times(1)).sql(
+ eq("SELECT * FROM t_role WHERE id = 1"));
+ // 断言缓存
+ assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
+ }
+ }
+
+ @Test
+ public void testAddNoRewritable() {
+ // 准备参数
+ MappedStatement ms = mock(MappedStatement.class);
+ List rules = singletonList(new DeptDataPermissionRule());
+ // mock 方法
+ when(ms.getId()).thenReturn("selectById");
+
+ // 调用
+ interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
+ // 断言
+ Map, Set> noRewritableMappedStatements =
+ interceptor.getMappedStatementCache().getNoRewritableMappedStatements();
+ assertEquals(1, noRewritableMappedStatements.size());
+ assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class));
+ }
+
+ @Test
+ public void testNoRewritable() {
+ // 准备参数
+ MappedStatement ms = mock(MappedStatement.class);
+ // mock 方法
+ when(ms.getId()).thenReturn("selectById");
+ // mock 数据
+ List rules = singletonList(new DeptDataPermissionRule());
+ interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
+
+ // 场景一,rules 为空
+ assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null));
+ // 场景二,rules 非空,可重写
+ assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule())));
+ // 场景三,rule 非空,不可重写
+ assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules));
+ }
+
+ private static class DeptDataPermissionRule implements DataPermissionRule {
+
+ private static final String COLUMN = "dept_id";
+
+ @Override
+ public Set getTableNames() {
+ return SetUtils.asSet("t_user");
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+ LongValue value = new LongValue(100L);
+ return new EqualsTo(column, value);
+ }
+
+ }
+
+ private static class EmptyDataPermissionRule implements DataPermissionRule {
+
+ @Override
+ public Set getTableNames() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ return null;
+ }
+
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-test/pom.xml b/yudao-framework/yudao-spring-boot-starter-test/pom.xml
index 9500403d9..0b211f3cd 100644
--- a/yudao-framework/yudao-spring-boot-starter-test/pom.xml
+++ b/yudao-framework/yudao-spring-boot-starter-test/pom.xml
@@ -22,6 +22,10 @@
+
+ org.mockito
+ mockito-inline
+
org.springframework.boot
spring-boot-starter-test
diff --git a/更新日志.md b/更新日志.md
index 266eacbbd..29f109c3d 100644
--- a/更新日志.md
+++ b/更新日志.md
@@ -22,11 +22,12 @@
### ⚠️ Warning
-这是一个多租户的预览版本,涉及的改动较大。
+这个版本新增了多租户与数据权限两个重量级的功能,建议花点时间进行了解与学习。
### ⭐ New Features
* 【新增】多租户,支持 Web、Security、Job、MQ、Async、DB、Redis 组件
+* 【新增】数据权限,内置基于部门过滤的规则
* 【新增】用户前台的昵称、头像的修改
### 🐞 Bug Fixes
@@ -35,7 +36,7 @@
### 🔨 Dependency Upgrades
-暂无
+* 【引入】mockito-inline 3.6.28:Mockito 提供对 final、static 的支持
### 📝 TODO