目录

mybatis插件开发

插件机制

源码解释

在四大对象创建的时候,每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(xxx);

以parameterHandler为例,创建的时候

BaseStatementHandler.java

1
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);

Configuration.java

1
2
3
4
5
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

进入interceptorChain.pluginAll

InterceptorChain.java

扫描所有在全局配置文件中配置好的插件,逐个执行plugin方法

1
2
3
4
5
6
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

Plugin.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static Object wrap(Object target, Interceptor interceptor) {
  // 获得插件的注解签名
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  // 如果插件签名要拦截该对象,则生成基于插件的目标动态代理对象
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

插件机制

我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面),我们的插件可以为四大对象创建出代理对象;代理对象就可以拦截到四大对象的每一个执行;默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

插件开发演示

  1. 编写插件实现Interceptor接口,并使用 @Intercepts注解完成插件签名

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    
    package com.eh.eden.mybatis.plugin;
       
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.reflection.MetaObject;
    import org.apache.ibatis.reflection.SystemMetaObject;
       
    import java.util.Properties;
       
    // 完成插件签名,用于拦截哪个对象的哪个方法,args是为了保证拦截方法的唯一性,防止重载等情况
    @Intercepts({
            @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
    })
    public class TestPlugin1 implements Interceptor {
       
        // 拦截方法
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("TestPlugin1...intercept begin:" + invocation.getMethod());
            Object target = invocation.getTarget();
            System.out.println("当前拦截到的对象:" + target);
            MetaObject metaObject = SystemMetaObject.forObject(target);
            // StatementHandler下有parameterHandler属性
            Object value = metaObject.getValue("parameterHandler.parameterObject");
            System.out.println("sql语句中的参数是:" + value);
            metaObject.setValue("parameterHandler.parameterObject", 9);
            //执行目标方法
            Object proceed = invocation.proceed();
            System.out.println("TestPlugin1...intercept end:" + invocation.getMethod());
            //返回执行后的返回值
            return proceed;
        }
       
        // 包装目标对象,如果有注解拦截签名就会为目标对象创建一个代理对象
        @Override
        public Object plugin(Object target) {
            System.out.println("-->TestPlugin1...plugin,将要包装的对象:" + target);
            Object wrap = Plugin.wrap(target, this);
            //返回为当前target创建的动态代理
            return wrap;
        }
       
        // 将插件注册时的property属性设置进来
        @Override
        public void setProperties(Properties properties) {
            System.out.println("插件配置的信息:" + properties);
        }
    }
    
  2. 在全局配置文件中注册插件

    1
    2
    3
    
     <plugins>
            <plugin interceptor="com.eh.eden.mybatis.plugin.TestPlugin1"></plugin>
        </plugins>
    
  3. 测试

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    @Test
        public void test() throws IOException {
            // 1. 获取SqlSessionFactory
            SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
       
            // 2. 从 SqlSessionFactory 中获取 SqlSession,能直接执行已经映射的sql语句
            try (SqlSession session = sqlSessionFactory.openSession()) {
                // 3. 获取接口实现类对象
                // mybatis会为接口自动创建一个代理对象,代理对象去执行增删改查方法
                EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
                System.out.println(employeeMapper.getEmployeeById(1));
            }
        }
    

    运行结果

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    -->TestPlugin1...plugin将要包装的对象org.apache.ibatis.executor.CachingExecutor@65f095f8
    20201011 14:17:39 [main] DEBUG com.eh.eden.mybatis.orm.dao.EmployeeMapper - Cache Hit Ratio [com.eh.eden.mybatis.orm.dao.EmployeeMapper]: 0.0
    -->TestPlugin1...plugin将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@d29f28
    -->TestPlugin1...plugin将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@212b5695
    -->TestPlugin1...plugin将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@69997e9d
    20201011 14:17:39 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    20201011 14:17:39 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1205445235.
    20201011 14:17:39 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@47d9a273]
    20201011 14:17:39 [main] DEBUG c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - ==>  Preparing: select id, last_name, gender, email from tbl_employee where id = ?
    TestPlugin1...interceptpublic abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
    当前拦截到的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@69997e9d
    sql语句中的参数是1
    20201011 14:17:39 [main] DEBUG c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - ==> Parameters: 9(Integer)
    20201011 14:17:39 [main] TRACE c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - <==    Columns: id, last_name, gender, email
    20201011 14:17:39 [main] TRACE c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - <==        Row: 9, 宋江, 1, sj@sh.com
    20201011 14:17:39 [main] DEBUG c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - <==      Total: 1
    Employee(id=9, lastName=null, gender=1, email=sj@sh.com, dept=null)
    

    可以看到本来要查的是1号员工,但实际查的是9号员工

开发多个插件

  • 按照插件注解声明,按照插件配置顺序调用插件plugin方法,生成被拦截对象的动态代理

  • 多个插件依次生成目标对象的代理对象,层层包裹,先声明的先包裹;形成代理链

  • 目标方法执行时依次从外到内执行插件的intercept方法,也就是后声明的先执行方法前置逻辑,后执行方法后置逻辑,千层饼

  • 多个插件情况就会产生多层代理,我们往往需要在某个插件中分离出目标对象。可以借助MyBatis提供的SystemMetaObject类来进行获取最后一层的h以及target属性的值

    https://gitee.com/lienhui68/picStore/raw/master/null/20201011142536.png

再来一个插件

全局配置文件

1
2
3
4
5
6
<plugins>
    <plugin interceptor="com.eh.eden.mybatis.plugin.TestPlugin1"></plugin>
    <plugin interceptor="com.eh.eden.mybatis.plugin.TestPlugin2">
        <property name="username" value="david"/>
    </plugin>
</plugins>

TestPlugin2.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.eh.eden.mybatis.plugin;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.util.Properties;

// 完成插件签名,用于拦截哪个对象的哪个方法,args是为了保证拦截方法的唯一性,防止重载等情况
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
})
public class TestPlugin2 implements Interceptor {

    // 拦截方法
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("TestPlugin2...intercept begin:" + invocation.getMethod());
        Object target = invocation.getTarget();
        System.out.println("当前拦截到的对象:" + target);
        MetaObject metaObject = SystemMetaObject.forObject(target);
        // StatementHandler -> parameterHandler -> parameterObject
        Object value = metaObject.getValue("parameterHandler.parameterObject");
        System.out.println("sql语句中的参数是:" + value);
        metaObject.setValue("parameterHandler.parameterObject", 10);
        //执行目标方法
        Object proceed = invocation.proceed();
        System.out.println("TestPlugin2...intercept end:" + invocation.getMethod());
        //返回执行后的返回值
        return proceed;
    }

    // 包装目标对象,如果有注解拦截签名就会为目标对象创建一个代理对象
    @Override
    public Object plugin(Object target) {
        System.out.println("-->TestPlugin2...plugin,将要包装的对象:" + target);
        Object wrap = Plugin.wrap(target, this);
        //返回为当前target创建的动态代理
        return wrap;
    }

    // 将插件注册时的property属性设置进来
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的信息:" + properties);
    }
}

测试结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
插件配置的信息{}
插件配置的信息{username=david}
。。。
20201011 14:45:18 [main] DEBUG net.sf.ehcache.Cache - Initialised cache: com.eh.eden.mybatis.orm.dao.EmployeeMapper
20201011 14:45:18 [main] DEBUG net.sf.ehcache.config.ConfigurationHelper - CacheDecoratorFactory not configured for defaultCache. Skipping for 'com.eh.eden.mybatis.orm.dao.EmployeeMapper'.
-->TestPlugin1...plugin将要包装的对象org.apache.ibatis.executor.CachingExecutor@64dafeed
-->TestPlugin2...plugin将要包装的对象org.apache.ibatis.executor.CachingExecutor@64dafeed
20201011 14:45:18 [main] DEBUG com.eh.eden.mybatis.orm.dao.EmployeeMapper - Cache Hit Ratio [com.eh.eden.mybatis.orm.dao.EmployeeMapper]: 0.0
-->TestPlugin1...plugin将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@275fe372
-->TestPlugin2...plugin将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@275fe372
-->TestPlugin1...plugin将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e38a7fe
-->TestPlugin2...plugin将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e38a7fe
-->TestPlugin1...plugin将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@366ef90e
-->TestPlugin2...plugin将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@366ef90e
20201011 14:45:18 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
20201011 14:45:18 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1495161082.
20201011 14:45:18 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@591e58fa]
20201011 14:45:18 [main] DEBUG c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - ==>  Preparing: select id, last_name, gender, email from tbl_employee where id = ?
TestPlugin2...intercept beginpublic abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
当前拦截到的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@366ef90e
sql语句中的参数是1
TestPlugin1...intercept beginpublic abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
当前拦截到的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@366ef90e
sql语句中的参数是10
TestPlugin1...intercept endpublic abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
TestPlugin2...intercept endpublic abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
20201011 14:45:18 [main] DEBUG c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - ==> Parameters: 9(Integer)
20201011 14:45:18 [main] TRACE c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - <==    Columns: id, last_name, gender, email
20201011 14:45:18 [main] TRACE c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - <==        Row: 9, 宋江, 1, sj@sh.com
20201011 14:45:18 [main] DEBUG c.e.e.m.orm.dao.EmployeeMapper.getEmployeeById - <==      Total: 1
Employee(id=9, lastName=null, gender=1, email=sj@sh.com, dept=null)

可以看到先配置的插件后执行,所以查出来的是9号员工信息。

常用代码

从代理链中分离真实被代理对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//1、分离代理对象。由于会形成多次代理,所以需要通过一个 while 循环分离出最终被代理对象,从而方便提取信息
MetaObject metaObject = SystemMetaObject.forObject(target);

while (metaObject.hasGetter("h")) {
  Object h = metaObject.getValue("h");
  metaObject = SystemMetaObject.forObject(h);
}

//2、获取到代理对象中包含的被代理的真实对象
Object obj = metaObject.getValue("target");

//3、获取被代理对象的MetaObject方便进行信息提取
MetaObject forObject = SystemMetaObject.forObject(obj);