Mybatis的自定义插件原理

1.插件简介

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例, 我们可基于MyBatis插件机制实现分页、分表, 监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。

2.Mybatis插件介绍

Mybatis作为一个应用广泛的优秀的ORM开源框架, 这个框架具有强大的灵活性, 在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler) 处提供了简单易用的插件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截, 对mybatis而言所谓的插件就是拦截器, 用来增强核心对象的功能, 增强功能本质上是借助于底层的动态代理实现的, 换句话说, MyBatis中的四大对象都是代理对象。代理对象当调用执行方法时候都会调用getMapper中的invoke方法。然后返回的都是经过层层代理的代理对象。
image.png
换句话说:mybatis允许自定义插件对上图的四大核心对象进行拦截。

MyBatis所允许拦截的方法如下:

所允许拦截的方法,也就是这四大核心对象里面的方法。

  1. 执行器Executor(update、query、commit、rollback等方法) ;
  2. SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);
  3. 参数处理器ParameterHandler(getParameterObject、setParameters方法) ;
  4. 结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法) ;

3.Mybatis插件原理

上面说到四大核心对象返回的都是代理对象,那么这些代理对象如何产生的呢?如何来完成方法的动态增强呢?

在四大对象创建的时候
1、每个创建出来的对象不是直接返回的, 而是经过interceptorChain.pluginAll(parameterHandler);来对你创建出来的对象进行处理。
2、获取到所有的Interceptor(拦截器)(插件需要实现的接口);调用interceptor.plugin(target) ; 返回target包装后的对象
3、插件机制, 我们可以使用插件为目标对象创建一个代理对象; AOP(面向切面) 我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;

拦截 : 插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来举例说明:

  1. // 1.这个newParameterHandler方法是借助createParameterHandler来生成ParameterHandler的原生对象
  2. // 但是返回的不是创建出来的parameterHandler,而是经过了interceptorChain.pluginAll后返回的。
  3. public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
  4. Object object, BoundSql sql, InterceptorChain interceptorChain){
  5. ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
  6. parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);
  7. return parameterHandler;
  8. }
  9. //pluginAll里面就是对所有的interceptor进行遍历,取出每一个interceptor的target(也就是上面产生的
  10. //parameterHandler对象),经过interceptor.plugin()方法进行处理。那么如何处理的呢?
  11. //就是通过jdk动态代理,来为当前的target对象产生一个代理对象,返回的就是代理对象
  12. public Object pluginAll(Object target) {
  13. for (Interceptor interceptor : interceptors) {
  14. target = interceptor.plugin(target);
  15. }
  16. return target;
  17. }

具体的拦截过程分析

image.png

interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链 中的拦截器依次的对目标进行拦截或增强。
interceptor.plugin(target)中的target就可以理解为mybatis 中的四大对象。返回的target是被重重代理后的对象。底层用的是动态代理,最终拿到的四大核心对象也是代理对象。

底层是动态代理,最终拿到的是代理对象,那么代理对象调用接口中的任意方法,则都会调用代理对象中的InvocationHandler接口中的invoke方法,那么在invoke方法中,可以在原方法进行调用的前/后都可以进行逻辑的增强。

image.gif

自定义拦截Executor的query方法

1、先创建一个自定义类,继承拦截器
2、通过intercepts和signature两个注解
3、指定拦截的类型,需要拦截的方法,以及这个被拦截方法需要的参数
4、还需要将自定义的配置类添加到数据库配置文件中

 @Intercepts({
      @Signature(
           type = Executor.class,// 要拦截的类型是四大对象的那个
           method = "query",// 具体要拦截的方法
           args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
          // 这个要拦截方法的参数,也就是入参,因为可能有多个方法,所以通过参数来识别
      )
 })
 public class ExeunplePlugin implements Interceptor { // 自定义拦截器类继承Interceptor接口
     //省略逻辑
 }

具体的解析如下:
Mybatis的插件原理/使用/源码 - 图4image.gif

当将自定义的插件配置到数据库配置文件中,这样MyBatis在启动时可以加载插件, 并保存插件实例到相关对象(InterceptorChain, 拦截器链)中。待准备工作做完后, MyBatis处于就绪状态。
我们在执行SQL时, 需要先通过DefaultSqlSessionFactory创建SqlSession。Executor实例会在创建SqlSession的过程中被创建,Executor实例创建完毕后, MyBatis会通过JDK动态代理为实例生成代理类。这样, 插件逻辑即可在Executor相关方法被调用前执行

调用代理对象中的InvocationHandler接口中的invoke方法,那么在invoke方法中,可以在原方法进行调用的前/后都可以进行逻辑的增强。

以上就是MyBatis插件机制的基本原理image.gif,就是通过JDK动态代理,为原生对象来生成一个代理对象。
其实也就是对四大核心对象里面的方法进行增强

Mybatis的自定义插件

4.1插件接口

Mybatis插件接口-Interceptor
Intercept方法, 插件的核心方法
plugin方法, 生成target的代理对象
setProperties方法, 传递插件所需参数

4.2自定义插件

设计实现一个自定义插件

  • 1、创建一个MyPlugin类实现Interceptor接口,并重写里面的方法。
  • 2、借助@Intercepts、@Signature注解来明确当前自定义插件要拦截那个核心对象里面的那个方法
  • 3、将自定义的插件在sqlMapConfig.xml进行注册

StatementHandler是sql的预处理,因为当SQL语句执行的时候,肯定要进行sql语句的预处理。

//注意看这个大花括号,在这个拦截器中,可以订阅多个@Signature对多处进行拦截
@Intercepts({
        @Signature(type = StatementHandler.class,// 具体拦截那个核心对象的class
                   method = "prepare",// 指定要拦截接口内的方法
                   args = { Connection.class, Integer.class}), // 这是拦截方法的需要的入参
})
public class MyPlugin implements Interceptor {
    private final Logger logger = (Logger) LoggerFactory.getLogger(this.getClass());

    // 1、拦截方法:只要被拦截的目标对象的目标方法被执行时,都会执行这个intercept方法
    //    每次执行prepare方法操作时都会进到intercept这个拦截器的方法内
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 其实这两还可以实现分页、监控等
        System.out.println("增强逻辑");
        return invocation.proceed();//执行原方法
    }

    // 2、把当前拦截器生成的代理对象存到拦截链中
    //   这是三个方法中第一个执行的方法,因为它把拦截器生成一个代理对象注册到拦截器链中
    @Override
    public Object plugin(Object target) {
        System.out.println( "将要包装的目标对象"+target);
        // 通过Plugin.wrap()方法来吧原目标方法生成代理对象,然后存入到拦截器中
        // target就是要处理的对象,this就是当前的拦截器MyPlugin
        Object wrap = Plugin.wrap(target, this);
        return wrap;
    }

    // 3、获取配置文件的参数
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数"+properties);
    }
}

4.3给插件配置文件设置参数

plugins一定要放在typeAliases的下面

<typeAliases>
   <package name="com.lagou.pojo"/>
</typeAliases>
<plugins>
   <plugin interceptor="com.lagou.plugin.MyPlugin">
      <property name="name" value="tom"/>
   </plugin>
</plugins>

Mybatis的插件原理/使用/源码 - 图7image.gif

Mybatis插件的源码分析

执行插件逻辑

Plugin实现了InvocationHandler接口, 实现接口肯定会实现接口里面的invoke方法,在invoke方法里会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

1、先看自定义框架中getMapper的动态代理方法

Mybatis的插件原理/使用/源码 - 图9

2、Mybatis的插件也是实现InvocationHandler接口

image.png

3、通过signatureMap获取被拦截的方法列表

image.png

4、插件的方法是在注解时候就明确了

image.png

5、就是调用intercept方法进行增强

image.png

Mybatis可引入第三方插件

page Helper分页插件

1、添加依赖

<!-- 分页助手 -->
 <dependency>
     <groupId>com.github.pagehelper</groupId>
     <artifactId>pagehelper</artifactId>
     <version>3.7.5</version>
 </dependency>
 <dependency>
     <groupId>com.github.jsqlparser</groupId>
     <artifactId>jsqlparser</artifactId>
     <version>0.9.1</version>
 </dependency>

2、可以查看分页助手的源码

image.png

3、在Mybatis的核心配置文件sqlMapConfig.xml中配置PageHelper

<plugins>
    <!--注意:分页助手的插件配置在通用mapper之前-->
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <!--指定方言-->
         <property name="dialect" value="mysql"/>
    </plugin>
</plugins>

4、测试代码

    @Test
    public void testPageHelper(){
        IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
        // 从第一页开始,每页两条记录
        PageHelper.startPage(1,2);
        List<User> users = mapper.selectUser();
        for(User user : users){
            System.out.println(user);
        }
        //其他分页数据
        PageInfo<User> pageInfo = new PageInfo<User>(users);
        System.out.println("总条数"+pageInfo.getTotal());
        System.out.println("总页数"+pageInfo.getPages());
        System.out.println("当前页"+pageInfo.getPageNum());
        System.out.println("每页显示长度"+pageInfo.getPageSize());
        System.out.println("是否第一页"+pageInfo.isIsFirstPage());
        System.out.println("是否最后一页"+pageInfo.isIsLastPage());
    }

image.png image.png