若依框架 @DataScope 注解导致定时任务异常的原因与优雅解决方案

🧐 定时任务遇到数据权限限制问题?这样优雅解决!

最近写需求时遇到 若依框架 的数据权限限制场景,大致如下:

@DataScope(deptAlias = "company", userAlias = "company")
public List<CsCompany> selectCompanyList(Company company) {
    // 代码逻辑肯定不会给你看
}

这个 @DataScope 注解的作用是 限制数据范围,通过获取当前用户信息,在 SQL 后拼接部门权限条件,做到数据隔离。


❗ 问题出现了

场景:在写一个定时任务时,需要批量获取公司信息,运行时突然报错:

@Scheduled
public void scheduleTest() {
    List<CsCompany> csCompanies = selectCompanyList(new CsCompany());
    // 处理逻辑
}

🚨 报错信息:

获取用户信息异常

奇怪:查询逻辑明明没写获取用户信息,怎么就异常了?


🔍 真相揭秘

跟踪源码后发现,@DataScope 背后有个切面逻辑,会默认获取当前用户信息

@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) {
    clearDataScope(point);
    handleDataScope(point, controllerDataScope);
}

protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
    // 获取当前用户信息
    LoginUser loginUser = SecurityUtils.getLoginUser();
    ...
}

进一步看 getLoginUser() 方法源码:

public static LoginUser getLoginUser() {
    try {
        return (LoginUser) getAuthentication().getPrincipal();
    } catch (Exception e) {
        throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
    }
}

原因总结:

原因说明
没有登录上下文定时任务场景下没有用户登录态
SecurityContext 为空SecurityUtils.getLoginUser() 获取失败

🛠️ 常见解决方案

方案优点缺点
1️⃣ 模拟登录用户手动设置用户信息,快速解决侵入性强,易遗忘,维护成本高
2️⃣ 重写方法写一个不带 @DataScope 的版本方法重复,逻辑分散,后续不好维护
3️⃣ 动态控制通过参数动态控制权限逻辑灵活 ✅,优雅 ✅,推荐使用 ✅

🚀 推荐方案 —— 动态控制数据权限

设计目标:

外部接口:需要权限过滤
内部调用(如定时任务):可以跳过权限过滤


🎨 改造 @DataScope 注解

新增一个 needDataScope 参数,支持 SpEL 动态绑定

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {

    String deptAlias() default "";

    String userAlias() default "";

    /**
     * 是否需要进行数据权限限制,支持 SpEL 表达式
     */
    String needDataScope() default "true";
}

✅ 使用示例

@DataScope(deptAlias = "company", userAlias = "company", needDataScope = "#needDataScope")
public List<CsCompany> selectCompanyList(Company company, boolean needDataScope) {
    // 查询逻辑
}

说明:

名称说明
needDataScope是否需要权限限制,通过参数 #needDataScope 动态传入

🗂️ 切面逻辑改造

protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
    if (needDataScope(joinPoint, controllerDataScope)) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNotNull(loginUser)) {
            SysUser currentUser = loginUser.getUser();
            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
                                controllerDataScope.userAlias(), loginUser);
            }
        }
    }
}

✨ 动态参数解析


private boolean needDataScope(JoinPoint point, DataScope controllerDataScope) {
    // 获取方法的参数名称
    MethodSignature signature = (MethodSignature) point.getSignature();
    String[] paramNames = signature.getParameterNames();

    // 获取目标方法的参数值
    Object[] args = point.getArgs();

    // 创建 Spring 表达式上下文,将参数名和对应的值绑定进去
    StandardEvaluationContext context = new StandardEvaluationContext();
    for (int i = 0; i < paramNames.length; i++) {
        context.setVariable(paramNames[i], args[i]);
    }

    // 解析 needDataScope 参数(SpEL 表达式),返回解析结果(Boolean 类型)
    return parser.parseExpression(controllerDataScope.needDataScope())
                 .getValue(context, Boolean.class);
}


🔎 什么是 SpEL?

名称说明
SpELSpring Expression Language,Spring 表达式语言
支持 #参数名 动态绑定参数
支持逻辑判断、三目表达式、方法调用等

常见场景:

@Cacheable(key = "#id")
@PreAuthorize("hasRole('ADMIN')")

📌 优势总结

优势说明
✅ 灵活根据实际需求动态决定是否校验权限逻辑
✅ 复用性强只维护一个方法版本,无需写多个重载函数
✅ 低侵入性不破坏原有业务逻辑,对已有接口完全兼容

🔚 结语

遇到问题,不慌不忙,先理解框架原理,再找优雅方案。

这次通过:

  • ✅ 注解扩展
  • ✅ 切面增强
  • ✅ SpEL 参数绑定

实现了优雅又实用的权限动态控制。


📌 觉得有帮助?点个 👍 收藏一下,分享给你身边的程序员吧 👏

### 若依框架数据权限实现原理 #### 1. @DataScope 注解的作用 @DataScope 是若依框架中用于实现数据权限的核心注解之一。它通过定义部门表别名 (`deptAlias`) 和用户表别名 (`userAlias`) 来指定 SQL 查询中的关联字段[^2]。 该注解的主要功能如下: - 提供灵活的配置选项,允许开发者自定义部门和用户的表别名。 - 在方法级别标注,明确哪些查询需要进行数据权限过滤。 #### 2. AOP 切面拦截逻辑 AOP(Aspect-Orientated Programming)是 Spring 框架的重要特性之一,在若依的数据权限实现中起到了关键作用。当某个服务方法被标记为 `@DataScope` 后,AOP 切面会自动拦截这些方法调用,并动态修改其对应的 SQL 查询语句[^4]。 具体流程如下: - **切点匹配**:通过扫描带有 `@DataScope` 注解的方法,将其纳入到 AOP 的处理范围。 - **参数解析**:提取注解中的属性值(如 `deptAlias` 和 `userAlias`),并结合当前登录用户的权限信息生成动态 SQL 片段。 - **SQL 修改**:将生成的动态 SQL 插入到原始查询语句中,从而完成数据权限的过滤。 以下是典型的 AOP 切面代码片段: ```java @Aspect @Component public class DataScopeAspect { @Around("@annotation(dataScope)") public Object around(ProceedingJoinPoint point, DataScope dataScope) throws Throwable { // 获取当前用户的角色ID或其他权限标识 Set<Long> roleIds = SecurityUtils.getRoleList(); // 构建动态SQL StringBuilder sqlString = new StringBuilder(); if (roleIds.contains(RoleConstants.ADMIN_ROLE_ID)) { // 超级管理员不过滤任何数据 sqlString.append(" AND 1=1 "); } else { // 根据角色构建具体的过滤条件 sqlString.append(" AND ").append(dataScope.deptAlias()).append(".id IN (...) "); } // 将动态SQL注入到参数中 Map<String, Object> params = (Map<String, Object>) point.getArgs()[0]; params.put("dataScope", sqlString.toString()); return point.proceed(); } } ``` 上述代码展示了如何利用 AOP 技术捕获带 `@DataScope` 注解的服务方法,并向其中注入动态生成的 SQL 字符串[^3]。 #### 3. 动态 SQL 拼接机制 在 MyBatis 中,最终执行的 SQL 并不是直接写死在 Mapper 文件里的静态部分,而是可以通过传递额外的参数来扩展或替换某些子句。这种灵活性使得动态 SQL 成为了可能。 例如,在若依项目的 Mapper XML 文件中可能会看到这样的占位符: ```xml <select id="selectUserList" parameterType="map" resultType="com.ruoyi.system.domain.SysUser"> SELECT u.* FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id WHERE 1=1 ${dataScope} </select> ``` 这里的 `${dataScope}` 即是由 AOP 切面计算得出的具体过滤条件字符串。这样设计的好处在于既保持了原有 SQL 结构清晰易读,又实现了高度可定制化的权限控制需求。 --- ### 总结 综上所述,若依框架通过对 `@DataScope` 自定义注解的支持、借助 Spring AOP 对目标方法实施横切关注以及巧妙运用 MyBatis 的动态 SQL 特性三者相结合的方式完成了复杂而高效的数据权限管理方案[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值