此文章介绍了mybatis-plus几个比较常用的插件或功能,可以提升开发效率,也使得代码更加规范化。主要介绍:多租户插件中获取租户ID以及设置忽略的表,乐观锁插件的配置即统一处理影响条数为0时抛出异常,通用枚举的配置让字典值的处理更方便。

引入mybatis-plus依赖包

注:系列文章二已经添加过,更详细使用请参见mybatis-plus官网

<!-- mybatis-plus依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>

引入多租户插件

新建mybatis-plus配置文件MybatisPlusConfig.java,源码如下:

@Configuration
public class MybatisPlusConfig {
    @Resource
    private CommonManager commonManager;
    @Resource
    private CustomizerConfig customizerConfig;

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 多租户插件
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            /**
             * 获取租户ID
             */
            @Override
            public Expression getTenantId() {
                return new LongValue(commonManager.getCurrentUser().getTenantId());
            }

            /**
             * 设置忽略的表
             */
            @Override
            public boolean ignoreTable(String tableName) {
                return customizerConfig.getTenantIgnoreTables().contains(tableName);
            }
        }));

        return interceptor;
    }

}

获取租户ID说明

用户登录时,将用户信息存储到redis中,返回userToken,以后的所有请求都必须在请求头中携带userId和userToken信息。每个请求在进入controller层之前,通过拦截器统一拦截请求,将用户信息存储到线程变量中,此处可以从线程变量中获取用户信息,再获取其中的租户ID。若线程变量中找不到用户信息,通过请求头中的userId去redis中获取用户信息。

不启用租户控制的表(ignoreTable)

customizerConfig类是自定义的配置信息类,把需要忽略的表配置到配置文件中,方便修改;

/**
 * 自定义配置文件类
 */
@Component
@ConfigurationProperties(prefix = "customizer")
@Data
public class CustomizerConfig {
    /**
     * 多租户插件忽略的表
     */
    @Value("customizer.tenant-ignore-tables")
    private List<String> tenantIgnoreTables;

    /**
     * 包名
     */
    @Value("customizer.package-name")
    private String packageName;
}

yml添加配置如下:

# 自定义配置项
customizer:
  # 多租户插件忽略的表
  tenant-ignore-tables: >
    sys-user
    ,sys_action

分页插件

直接在mybatis-plus配置文件中增加配置:

interceptor.addInnerInterceptor(new PaginationInnerInterceptor());

分页插件比较简单,在此就不详细介绍了。

乐观锁插件

配置文件中继续添加插件:

interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

在乐观锁的字段上面添加注解@Version,此处目前还不能统一配置,只能每个entity实体的乐观锁字段都添加此注解。
乐观锁更新失败时,并不会抛出异常(目前官方也没有修改的计划),我们可以通过AOP切面来统一处理。
引入切面依赖:

<!-- aop切面依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

增加切面配置类AspectConfig.java,如下:

/**
 * 切面配置
 * AOP通知种类:
 * 1、前置通知(@Before):在方法调用之前执行
 * 2、后置通知(@After):在方法正常调用之后执行
 * 3、环绕通知(@Around):在方法调用之前和之后,都分别可以执行的通知
 * 4、异常通知(@AfterThrowing):如果在方法调用过程中发生异常,则通知
 * 5、最终通知(@AfterReturning):在方法调用之后执行
 */
@Aspect
@Component
public class AspectConfig {
    // mp baseMapper类全路径
    private static final String MP_BASE_MAPPER_NAME = "com.baomidou.mybatisplus.core.mapper.BaseMapper";

    // 需要检查影响条数的方法列表
    private static final List<String> CHECK_AFFECTED_ROWS_METHOD_LIST = Arrays.asList("insert", "update", "updateById", "deleteById");

    /**
     * mapper层最终通知:
     * execution 代表所要执行的表达式主体
     * 第一处 * 代表方法返回类型 *代表所有类型
     * 第二处 包名代表aop监控的类所在的包
     * 第三处 .. 代表该包以及其子包下的所有类方法
     * 第四处 * 代表类名,*代表所有类
     * 第五处 *(..) *代表类中的方法名,(..)表示方法中的任何参数
     *
     * @param joinPoint 连接点
     * @param result    返回结果
     */
    @AfterReturning(value = "execution(* com.yd.sysjava.mapper..*.*(..))",
            returning = "result")
    public void mapperAfterReturning(JoinPoint joinPoint, Object result) {
        /*
            相关方法影响条数必须大于0
            insert update updateById deleteById
         */
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        boolean checkFailed = CHECK_AFFECTED_ROWS_METHOD_LIST.contains(methodName)
                && (result == null || (Integer) result == 0);
        if (checkFailed) {
            // 抛出自定义异常
            throw new BusinessException(ResultMsg.FAILED_AFFECTED_ZERO.getMsg());
        }
    }
}

效果:先调用insert方法新增一条记录,会有一个默认的version为1,调用更新方法,正常操作的时候能够更新成功,并且version变为2。通过断点,查询完记录之后断点停住,手动去修改数据库的version字段值为3,跳过断点之后会更新失败,AOP切面会抛出异常。

@Override
    public void modifyUser(ModifyUserSO so) {
        Integer id = so.getId();
        // 先查询
        SysUser sysUser = sysUserMapper.selectById(id);
        // 赋值
        BeanUtils.copyProperties(so, sysUser);
        // 更新-打断点
        sysUserMapper.updateById(sysUser);
    }

springboot通过mybatis 实现多租户 mybatis 多租户插件_ide


springboot通过mybatis 实现多租户 mybatis 多租户插件_ide_02


打印的更新语句如下:

UPDATE sys_user SET full_name = '潘松1', login_name = 'pansong1', password = '', user_type = '001', tenant_id = 1, version = 3, created_by = 1, created_time = '2022-04-16T11:00:36', updated_by = 1, updated_time = '2022-04-16T11:00:36' WHERE tenant_id = 1 AND id = 1 AND version = 2

此处详细的配置大家可以根据自己的实际情况进行适当调整,我在此处判断的insert等方法返回影响条数也是必须大于0的,和乐观锁没有关系。此处如果需要单独对乐观锁进行限制,则还需要进行更细致的判断。

通用枚举

相信大多数同学都有这样的经验,有些查询会关联很多表,但是其中大多数都是关联查询字典值的。现在通过通用枚举的配置,就无需在查询时关联字典表进行查询了,而且也省去了维护字典表的麻烦。只需简单几步即可搞定:

  1. 创建一个枚举,实现IEnum接口,此处使用userType字段举例,传入的泛型和value类型一致即可。
/**
 * 用户类型:正式工、临时工、外包工、派遣工
 */
public enum UserTypeEnum implements IEnum<String> {
    FORMAL("001", "正式工"),
    TEMPORARY("002", "临时工");

    private final String value;
    private final String desc;

    UserTypeEnum(String value, String desc) {
        this.value = value;
        this.desc = desc;
    }

    @Override
    public String getValue() {
        return this.value;
    }

    @Override
    public String toString() {
        return this.desc;
    }
}
  1. entity实体中userType字段类型使用此枚举:
/**
     * 用户类型
     */
    private UserTypeEnum userType;
  1. mybatis-plus配置类中增加bean:
@Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2Object(){
        return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    }

以后的枚举字段只需复制UserTypeEnum进行修改,然后进行步骤二的调整即可,步骤三只需配置一次。效果如下:

springboot通过mybatis 实现多租户 mybatis 多租户插件_中间件_03


springboot通过mybatis 实现多租户 mybatis 多租户插件_ide_04

springboot通过mybatis 实现多租户 mybatis 多租户插件_配置文件_05


这里会存在一个问题,通过此种方式,可以获取到对应的字典值,但是原始数据库中的value值(此处的001)查询不到了,如果有时候需要此值做判断,只能使用001对应的’正式工’进行判断。