插件机制
源码解释
在四大对象创建的时候,每个创建出来的对象不是直接返回的,而是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)
插件开发演示
-
编写插件实现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);
}
}
|
-
在全局配置文件中注册插件
1
2
3
|
<plugins>
<plugin interceptor="com.eh.eden.mybatis.plugin.TestPlugin1"></plugin>
</plugins>
|
-
测试
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...intercept:public 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属性的值

再来一个插件
全局配置文件
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 begin:public 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 begin:public 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 end:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
TestPlugin2...intercept end:public 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);
|