自定义 MyBatis 拦截器,为业务赋能

3,349 阅读4分钟

1. 前言:

前几篇文章分享了下 MyBatis 拦截器的相关知识,这里再分享下自己项目中遇到的一个问题,然后通过自定义的拦截器快速的解决了问题。

2. 项目用到的技术:

SpringBoot,MyBatis.....

3. 业务需求:

最近项目中需要增加「数据权限」功能。所谓的「数据权限」是指不同用户在查询某张表的数据时看到的数据范围是不一样的,有的用户可以看到全部门店的数据,有的用户只能看到部分门店的数据,这就需要给不同角色用户配置不同的数据范围查看权限。比如 “商品信息表” 中有所属门店的 “门店id” 字段,用户能看到哪些门店的商品信息数据是根据配置的数据权限来判断的, 例如A 用户能看到 3 家门店的商品信息数据,B 用户能看到 4 家门店的商品信息数据。具体的实现就是在对应的 SQL 查询语句的 WHERE 条件中加上  tableName.shopId IN (shopId1, shopId2,.....) 这个条件即可。(PS:系统中的十几张表都有 “门店id” 字段,所以对应的就有十几个功能需要加上数据权限)。

4. 实现方案:

具体的实现方式有以下两种:

(1)在每个需要加数据权限的地方加上对应的代码

如果我们的系统刚开始开发,对应的功能需要加数据权限,比如商品信息数据获取我们完全可以在业务代码中这样写:

public List<GoodsInformation> searchGoodsInformation(Integer userId) {
    List<Integer> shopIds = authService.getIds(userId); // 调用权限服务获取用户所能看到的门店
    
    // 1. 如果 shopIds 为空,就是可以查询所有门店数据
    ......
    // 2. 如果 shopIds 不为空,就在查询语句中加上 tableName.shopId IN (shopId1, shopId2,.....)
    ......
}

上面的 1 和 2 两部操作,可以放在 MyBatis 中用 if 判断下即可,这样可以复用一条查询语句。

(2)利用 MyBatis 的拦截器加 AOP 的方式实现

如果我们的项目开发到后期了,这个时候产品说要给系统中十几个查询页面加上数据权限功能,听到这句话我们是有点蒙蔽的。如果按照方法 1,我们需要修改业务代码和 MyBatis 中的查询语句,还是比较耗费时间的。

其实我们分析下方法 1 中的代码可以发现:根据 userId 获取门店集合是通用的代码,因为所有的数据权限功能都是根据 shopId 来判断的,所以这种通用的代码可以用 AOP 来实现,这样就不用修改散落在不同包中的业务代码了。还有给不同表加 tableName.shopId IN (shopId1, shopId2,.....) 这个条件,除了表名不同和字段名可能不同(有的叫shopId,有的叫shop等) 之外,IN (shopId1, shopId2,.....) 这个完全是相同的,所以我们可以自定义 MyBatis 拦截器 通过修改查询语句,添加我们需要的条件即可实现。其中对不同的表名和字段名,我们可以用自定义的注解来配置。

具体实现:

将 userId 放入自定义的 threadlocal 中

@ControllerAdvice
public class ApplicationControllerAdvice {

    @ModelAttribute
    public void addAttributes(@RequestParam(required = false) String userId) {            if(userId != null) {            
       UserContext userContext = new UserContext(); 
           
       userContext.setUserId(userId);  
         
       // 将 userId 放入 threadlocal 中
       userContextHolder.userContextThreadLocal.set(userContext);        
    }    

  }

}

然后定义 AOP 切面,根据 userId 获取 shopIds 集合以及方法上自定义注解中的 tableName 和 field 信息,并存入 threadlocal 中:

 @Before(value = "execution(* my.study.dataauthplugin.demo.*(..)) && @annotation(dataAuthentication)") public void getDataAuth(DataAuthentication dataAuthentication) throws Throwable {
  UserContext uc = UserContextHolder.userContextThreadLocal.get();
  if (uc != null && !"".equals(uc.getUserId()) && dataAuthentication != null) {
      // 获取 shopId 集合
      List<Integer> ids = authService.getIds(uc.getUserId());
      if (ids != null && !ids.isEmpty()) {
          List<String> fields = new ArrayList<>();
          List<String> tableNames = new ArrayList<>();
          // 获取自定义注解中的 tableNames 和 fields
          SqlSignature[] signatures = dataAuthentication.value();
          if(signatures.length > 0) {
               for (int i = 0; i < signatures.length; i++) {
                   fields.add(signatures[i].field());
                   tableNames.add(signatures[i].tableName());
                }
           }
           // 将 shopIds ,fields,tableNames 存入 threadlocal 中
           uc.init(ids, fields, tableNames);
       }
    }
}

}

然后定义 MyBatis 插件,根据 threadlocal 中的值,修改对应的查询语句。

具体实现见:github.com/qianhongxin…

(3)这个插件的代理对象是否创建在 DataAuthenticationPlugin 的 plugin 方法中做判断的。在实际的 intercept 拦截中并不做是否拦截判断了,和 PageHelper 分页插件的关于是否拦截的实现思想不同。具体见 SqlUtil 类的 doIntercept 方法。

5. 感悟:

对自己项目中用到的相关技术,我们需要深入去学习。比如 MyBatis 提供了插件这种扩展机制,那我们就要充分去用好它,提高开发效率,为业务赋能。

人说脱离业务谈技术是耍牛氓!话说回来,想要更好的支持业务,不深入的学习好业务用到的技术,拿什么来支持业务呢?个人觉得深入学习技术源码是很有必要的,虽说暂时用不上,但是通过持续的阅读技术源码,也能给我们开发业务代码提供更好的实现方案。