基于mybatis-plugin实现多租户应用

简介: 为了服务阿里云交付场景,满足原有系统转型多租户场景的数据操作需求,我们基于开源SQL增强插件实现多租户数据操作行为统一处理,应用开发者可以更多的关注业务实现。

主要功能

SQL增强插件可以做到一些统一的SQL处理逻辑,比如现在比较流行的基于多租户的PAAS平台,都会涉及到在数据库层的资源隔离,就需要在所有的执行语句中加入一个统一的WHERE条件,就可以使用这个插件来实现,免去重复书写SQL。

实现原理

mybatis向我们提供了plugin扩展能力,也就是拦截器,就是通过动态代理对目标方法拦截,在方法执行前后进行操作。目前在mybatis中可拦截的目标分别是StatementHandler、ParameterHandler、ResultSetHandler、Executor,与拦截器有关的类:plugin、InterceptorChain。

@Intercepts({@Signature(type = Executor.class,
        method ="query",
        args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})
public class CustomPlugins implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //最终plugin插件调用的是这个方法
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
        System.out.println(String.format("plugin output sql = %s , param=%s", boundSql.getSql(),boundSql.getParameterObject()));
        //放行 该方法
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        Object obj= Plugin.wrap(o,this);
        return obj;
    }

    @Override
    public void setProperties(Properties properties) {
        //常用于将配置中的参数赋值给类的实例变量
        String value = (String) properties.get("name");
        System.out.println(value);
    }
}
//InterceptorChain
public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      //这里的intecetor是Plugin 也即是自定义插件中plugin方法 
      //这里的target是Executor、ParameterHandler、ResultSetHandler、StatementHandler接口的实现类 具体是什么 要看@Interceptors中拦截的接口
      //这里的target是通过for循环不断赋值的,也就是说如果有多个拦截器,那么如果我用P表示代理,生成第       //一次代理为P(target),生成第二次代理为P(P(target)),生成第三次代理为P(P(P(target))),不断      //嵌套下去,这就得到一个重要的结论:<plugins>...</plugins>中后定义的<plugin>实际其拦截器方法     //先被执行,因为根据这段代码来看,后定义的<plugin>代理实际后生成,包装了先生成的代理,自然其代理方     //法也先执行.也即是interceptor的执行顺序 后定义先执行
      //这里的plugin方法实际上是调用对应拦截器类的重载方法
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
      //添加拦截器至集合
    interceptors.add(interceptor);
  }
 // 列举所有的拦截器类 
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

参考Spring中的过滤器或者拦截器使用的责任链都发现,处理器接口比较单一,且参数高度封装;但是反观Mybatis中需要使用责任链执行的方法,如Executor、StatementHandler、ParameterHandler、ResultSetHandler方法较多且参数各不统一,如果能统一对这些方法都执行责任拦截,想到的简单方法就是使用动态代理,把每个方法抽象为方法名、方法参数、方法返回值。这样就有统一的抽象实体了,然后责任链统一对抽象实体来进行处理即可。

拦截原理

该插件通过拦截器实现(mybatis/mybatis-plus)对多租户的支持。Mybatis Plugin 使用责任链模式与代理模式实现。

责任链模式

责任链模式的实现包含处理器接口Handler或者抽象处理器类AbsHandler, 还包括一个处理器链的类HandlerChain,这个处理器链类一般包括三个方法,一个是注册处理器的方法void addHandler(Handler handler),还有一个遍历执行处理器的方法void handlerAll(),以及一个返回所有注册了处理器对象的方法List getHandlers(),此方法一般需要放回不可修改的数据类,使用Collections.unmodifiableList进行包装。

对应到Mybatis中的责任链模式的处理器接口就是Interceptor,处理器链就是InterceptorChain,处理器链中的处理器注册是在解析XML配置时进行执行的。对应到源码为XMLConfigBuilder parseConfiguration() 方法中的pluginElement(root.evalNode("plugins")); 处理器接口中暴露了三个方法,实际使用的处理器方法是Object plugin(Object target);

然后处理器链 InterceptorChain pluginAll 方法是分别被Executor、ParameterHandler、ResultSetHandler、StatementHandler这四个对象调用的。

代理模式

代理模式有被代理对象和代理人两个角色,对应到Mybatis的插件模式中就是Plugin是代理人的角色,Plugin中的private Object target属性就是被代理对象,这个代理对象的创建是通过Plugin wrap()方法来创建的;这个方法是在责任链调用的时候通过处理接口中的Object plugin(Object target);方法进行调用的。

SQL解析原理

sql解析是基于开源组件jsqlparser实现。

多租户实现

前提条件

  1. 针对ORM框架使用mybatis的应用系统(使用mybatis-plus的可以使用mybatis-plus自带插件实现)
  2. 应用系统业务表结构要包含对应的租户标识字段,以具备到可以通过SQL拦截器统一处理的条件
  3. 引入依赖
<dependency>
    <groupId>com.mybatis</groupId>
    <artifactId>mybatis-plugin-spring-boot</artifactId>
    <version>1.0.0-REALSE</version>
</dependency>

基于mybatis-plugin 实现对SQL statment拦截处理能力,我们在新的项目应用中可通过简单配置和代码编辑可轻松实现应用多租户场景下租户信息字段的相关操作。通过执行添加拦截器、配置插件、设置规则字段值三步,实现了执行过程中的插件加载、字段处理规则、对齐当前操作租户相关字段值。

SqlSessionFactory添加拦截器

@Component
@DependsOn(value = {"mybatisPluginInterceptor"})
public class MemberMybatisInterceptorConfig {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private MybatisPluginInterceptor mybatisPluginInterceptor;

    public MemberMybatisInterceptorConfig() {
    }

    @PostConstruct
    public void addPrivilegeInterceptor() {

        Iterator sessionFactoryIterator = this.sqlSessionFactoryList.iterator();

        while (sessionFactoryIterator.hasNext()) {
            SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) sessionFactoryIterator.next();
            sqlSessionFactory.getConfiguration().addInterceptor(mybatisPluginInterceptor);
        }
    }
}

配置插件

a.插件参数

参数 示例值 是否必须 描述
guarder.enable true 插件开关
guarder.plugins.name dml_add_merchant_key 插件名称
guarder.plugins.order 0 插件执行顺序,如果没有控制就按照配置的来执行
guarder.plugins.level dml 可以执行sql增强的级别:databases,数据库级别,整个库的 sql 都会执行;table,表级别配置,只针对对应表执行;dml,对应的 select、update、delete、insert 级别
guarder.plugins.value -select
-insert sql增强级别为level为dml时配置
guarder.plugins.rules.name add_merchant_key 规则名称
guarder.plugins.rules.order 1 规则执行顺序,没有控制则按配置顺序来执行
guarder.plugins.rules.value add_where_field 插件规则值类型,定义了几种通用的值类型:add_where_field,添加where字段值插件;add_insert_field,添加插入字段;add_update_field,添加更新字段;add_field,添加插入和更新字段;delete_field,删除 sql 中的字段策略;change_tableName,修改表名
guarder.plugins.rules.fieldPolicy.name conf 规则字段来源策略:threadLocal,字段从线程变量中获取;conf,字段从配置文件中获取;customer,字段通过自定义实现接口获取(如果是自定义策略,对应 value 就是类路径,通过反射自动构造对象,或者通过上下文从 IOC 容器中获取)
guarder.plugins.rules.fieldPolicy.value merchantCode 字段来源策略对应值
guarder.plugins.rules.fieldValuePolicy.name threadLocal 规则字段值来源策略:threadLocal,字段从线程变量中获取;,conf,字段从配置文件中获取;customer,字段通过自定义实现接口获取;system,系统内嵌一些方式(如 now,uuid)
guarder.plugins.rules.fieldValuePolicy.value merchantCode 规则字段来源策略对应值
guarder.plugins.rules.fieldValueFailPolicy run 规则字段值获取失败策略:run,继续运行, 会忽略配置的规则;stop,停止运行,会抛出异常

b.YAML配置

guarder:
  enable: true
  plugins:
    - name: dml_add_merchant_key
      level: dml
      value:
        - select
        - insert
      rules:
        - name: add_merchant_key
          value: add_where_field
          field-policy:
            name: conf
            value: merchant_code
          field-value-policy:
            name: threadLocal
            value: merchantCode
          field-value-fail-policy: run

设置规则字段值

规则字段值来源策略包括_threadLocal、conf、customer、system 四种,插件为我们也提供了对应的值的获取策略实现,如下:_image.png

public class ConfPolicyProcess implements FieldPolicyProcess, FieldValuePolicyProcess {

    ......
    /**
     * @Description: 从配置文件中获取字段值策略,策略中的值就是配置的值
     * @return:
     * @Creator: lengrongfu
     * @Date: 2020/8/15 9:16 上午
     */
    @Override
    public String processFieldValuePolicy(String value) {
        return value;
    }
    ......
}
public class CustomerPolicyProcess implements FieldPolicyProcess, FieldValuePolicyProcess, BeanFactoryAware {
    ......
    @Override
    public String processFieldValuePolicy(String value) {
        RuleFieldValueCustomer instance = fieldValueInstance.get(value);
        if (Objects.isNull(instance)) {
            instance = getInstance(value, RuleFieldValueCustomer.class);
            logger.info("RuleFieldValueCustomer getInstance: {}", instance == null ? null : instance.fieldValueKey());
        }
        if (Objects.isNull(instance)) {
            return null;
        }
        return instance.fieldValueKey();
    }
    ......
 }
public class SystemPolicyProcess implements FieldValuePolicyProcess {
    ......
    /**
     * "uuid"
     * "now"
     *
     * @param value
     * @return
     */
    @Override
    public String processFieldValuePolicy(String value) {
        return function.apply(value);
    }
}
public class ThreadLocalPolicyProcess implements FieldPolicyProcess, FieldValuePolicyProcess {
    ......
    /**
     * 线程缓存变量获取字段值
     * 需要用户提前放入次字段值
     *
     * @param value
     * @return
     */
    @Override
    public String processFieldValuePolicy(String value) {
        Object variable = RuleFieldThreadLocal.getVariable(value);
        return (String) variable;
    }
}

_我们这里将采用threadLocal 策略,将_租户参数"merchantCode"通过切面方式在执行SQL前缓存到线程变量RuleFieldThreadLocal:

@Component
@Aspect
public class AppRequestAspect {

    @Before("@within(com.zsmart.nros.base.annotation.SessionController) || @within(com.zsmart.nros.base.annotation.CenterController)")
    public void before(){
        log.info("lx_merchantCode:"+ RuntimeContext.getValue("merchantCode"));
        RuleFieldThreadLocal.setVariable("merchantCode",RuntimeContext.getValue("merchantCode"));
    }
}

到这里,多租户拦截插件配置已经完成。

功能验证

1.使用一个表进行测试

CREATE TABLE `user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `merchant_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.编写对应实体类

@Data
public class User {

    private Integer id;

    private String name;

    private String phone;

    private String merchantCode;
}

3.编写操作实体类的 Mapper 接口和xml文件


public interface UserMapper  {

    int insert(User user);

    List<User> selectList();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zsmart.nros.sbc.member.dal.dao">
    <select id="insert" resultType="User">
            INSERT INTO user(name) values (#{name})
    </select>

    <select id="findAll" resultType="User">
            SELECT id, name, phone, merchant_code FROM user 
    </select>
</mapper>

4.实体类、Mapper 类都写好了,就可以使用了。在启动类里扫描 Mapper 类,即添加 @MapperScan 注解(已添加忽略此步)

@MapperScan(basePackages = "com.zsmart.nros.sbc.member.dal.dao")
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

5.编写测试代码

@Slf4j
@SpringBootTest
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void add(){
        #此步骤模拟切面设置租户数据
        RuleFieldThreadLocal.setVariable("merchantCode","test");
        
        User user = new User();
        user.setName("张三");
        int insert = userMapper.insert(user);
        log.info("insert value: {}",user.toString());
        List<User> users = userMapper.selectList(null);
        log.info("select  value:{}",users.toString());

    }

}

6.控制台输出日志信息可以看到租户字段数据追加成功

...
==>  Preparing: INSERT INTO user (name, merchant_code) VALUES (?, 'app1')
==> Parameters: 张三(String)
<==    Updates: 1
...
2021-12-17 19:03:50.442  INFO 20208 --- [nio-8080-exec-1] c.e.m.p.demo.controller.DemoController   : insert value: User(id=4, name=张三, phone=null, merchantCode=null)
...
==>  Preparing: SELECT id, name, phone, merchant_code FROM user WHERE user.merchant_code = 'app1'
==> Parameters: 
<==    Columns: id, name, phone, merchant_code
<==        Row: 3, 张三, null, app1
<==        Row: 4, 张三, null, app1
<==      Total: 2
...
2021-12-17 19:03:50.466  INFO 20208 --- [nio-8080-exec-1] c.e.m.p.demo.controller.DemoController   : select  value:[User(id=3, name=张三, phone=null, merchantCode=app1), User(id=4, name=张三, phone=null, merchantCode=app1)]

相关文章
|
4月前
|
SQL Java 数据库连接
MyBatis Plus应用实践总结
MyBatis Plus应用实践总结
52 0
|
2月前
|
Java 数据库连接 数据库
Spring Boot与MyBatis的集成应用
Spring Boot与MyBatis的集成应用
|
3月前
|
SQL Java 数据库连接
深入探索MyBatis Dynamic SQL:发展、原理与应用
深入探索MyBatis Dynamic SQL:发展、原理与应用
|
3月前
|
Java 数据库连接 mybatis
在Spring Boot应用中集成MyBatis与MyBatis-Plus
在Spring Boot应用中集成MyBatis与MyBatis-Plus
97 5
|
3月前
|
Java 数据库连接 数据库
Spring Boot与MyBatis的集成应用
Spring Boot与MyBatis的集成应用
|
3月前
|
SQL Java 数据库连接
JavaWeb基础第三章(MyBatis的应用,基础操作与动态SQL)
JavaWeb基础第三章(MyBatis的应用,基础操作与动态SQL)
|
4月前
|
缓存 Java 数据库连接
MyBatis三级缓存实战:高级缓存策略的实现与应用
MyBatis三级缓存实战:高级缓存策略的实现与应用
88 0
MyBatis三级缓存实战:高级缓存策略的实现与应用
|
12月前
|
存储 Java 数据库连接
Mybatis-plus@DS实现动态切换数据源应用
Mybatis-plus@DS实现动态切换数据源应用
490 0
|
4月前
|
Java 关系型数据库 MySQL
一文彻底搞懂Mybatis系列(六)之在WEB应用中使用Mybatis
一文彻底搞懂Mybatis系列(六)之在WEB应用中使用Mybatis
|
10月前
|
SQL 前端开发 Java
Mybatis的动态SQL分页及特殊字符应用
Mybatis的动态SQL分页及特殊字符应用
36 0