mybatis源码分析——Plugin的使用以及原理
一:插件的使用
以分页插件PageHelper为例,看一下mybatis的插件如何工作
首先添加pageHelper的maven依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
在mybatis-config.xml中配置插件plugins:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入外部资源文件
resource:默认引入classpath路径下的资源文件
url:引入物理路径下的资源文件(如:d:\\jdbc.properties)
-->
<properties resource="application.properties"></properties>
<!-- 设置参数 -->
<settings>
<!-- 开启驼峰匹配:完成经典的数据库命名到java属性的映射
相当于去掉数据中的名字的下划线,和java进行匹配
-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 配置别名 -->
<typeAliases>
<!-- typeAlias:用来配置别名,方便映射文件使用,type:类的全限定类名,alias:别名 -->
<typeAlias type="com.example.mybatis.model.User" alias="User"/>
</typeAliases>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
<!--<plugin interceptor="com.example.mybatis.plugin.MyFirstPlugin">
<property name="someProperty" value="100"/>
</plugin>-->
</plugins>
<!-- 配置环境:可以配置多个环境,default:配置某一个环境的唯一标识,表示默认使用哪个环境 -->
<environments default="development">
<!-- 配置环境,id:环境的唯一标识 -->
<environment id="development">
<!-- 事务管理器,type:使用jdbc的事务管理器 -->
<transactionManager type="JDBC" />
<!-- 数据源,type:池类型的数据源 -->
<dataSource type="POOLED">
<!-- 配置连接信息 -->
<property name="driver" value="${jdbc.driverClass}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<!-- 配置映射文件:用来配置sql语句和结果集类型等 -->
<mappers>
<mapper resource="UserMapper.xml" />
</mappers>
</configuration>
在使用的上一行语句中写上PageHelper.startPage(pageNo,pageSize) 页码,每页页数
PageHelper.startPage(3,2);
List<User> list = userMapper.selectUser("hello105");
这样就可以工作了,下面我们测试一下

通过日志可以看到,可以实现正常的分页工作了,下面我们来研究一下它的工作原理
二:插件工作原理
1:插件的注册,我们在第一节分析XMLConfigBuilder解析mybatis-config.xml的时候看过解析mappers,这里重点
看一下如何解析plugins元素

看一下解析plugins元素下面的plugin元素,

最后注册到configuration中的interceptorChain中


到这里,解析mybatis-config.xml时注册插件的过程就完成了。
2:对数据库操作做增强
看一下PageInterceptor这个类,这是一个拦截器类,从注解数据可以看出它主要拦截Executor的query方法

这个类里有个plugin方法,入参是被代理对象,通过静态方法wrap包装,返回代理对象

首先读取拦截Interceptor注解上的信息,判断代理类型是否匹配注解拦截信息,如果匹配则代理,不匹配则直接返回原对象
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;
}
PageInterceptor类的具体的拦截动作是在intercept这个方法里

看完这个类,我们看一下到底是在哪里对Executor做的增强,一定是在创建executor对象的时候,创建executor是在创建DefaultSqlSession的时候,
那来看一下SqlSessionFactory类的方法

创建Executor后,会通过拦截链对Executor进行增强,如果interceptor为空,或者拦截链不匹配executor是就会返回原来的executor

注册插件的时候我们看到过这个类,addInterceptor被调用过,现在就是用到第一步注册时候的插件来拦截
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
这个plugin一般就是对target进行代理,在上面看PageInterceptor这个类的时候,我们已经分析过,这里PageInterceptor是可以
匹配Executor的,所以会被拦截,增强类Plugin,内部维护了PageInterceptor这个对象,所以当Executor对象调用query方法时,
会调用到Plugin的Invoke方法,然后会被委托给PageInterceptor对象的intercept方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}

这样又回到了这个主要的方法里。
我们来看一下查询的地方,这个查询的地方,四个入参的在selectList中,DefaultSqlSession中的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
调用这个方法,最终会调到intercept方法,这个方法里面是怎么分页的逻辑,这里忽略
3:自定义一个拦截插件
这里我们自定义一个拦截的插件,只是在拦截的时候把信息拿出来打印一下
/**
* 告诉MyBatis当前插件用来拦截哪个对象的哪个方法
*/
@Intercepts({@Signature(
type = StatementHandler.class,
method = "query",
args = {Statement.class,ResultHandler.class}
)
})
public class MyFirstPlugin implements Interceptor {
/**
*
* 拦截目标对象的目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
Object target = invocation.getTarget();
System.out.println("当前拦截到的对象:"+target);
//拿到target的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("sql语句用的参数是:"+value);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}
/**
*
*包装目标对象的:为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
//我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
Object wrap = Plugin.wrap(target, this);
//返回为当前target创建的动态代理
return wrap;
}
/**
*
*将插件注册时 的property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息:"+properties);
}
}
定义好插件后,要在mybatis-config.xml中配置一下,这样才能在解析xml的时候实现注册缓存到configuration中
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
<plugin interceptor="com.example.mybatis.plugin.MyFirstPlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
看一下运行结果:

具体是在哪里调用的呢,那就要找到创建statement的地方
SimpleExecutor类中有doQuery这个方法,方法里面有创建statementHandler对象的方法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
newStatementHandler方法,会使用拦截链过滤这个statementHandler,看是否和拦截链中的interceptor匹配,如果匹配就会生成代理。

如果匹配,那么返回的statementHandler对象就是代理对象,statementHandler调用query时,调用的是Plugin的invoke方法,
然后委托给MyFirstPlugin这个拦截器的intercept方法执行。
总结:
插件的使用可以在不修改原有逻辑的基础上,对功能进行增强,这也是动态代理的特性,在mybatis中可以支持插件拦截的地方有四个,上面已经分析,executor、statementHandler、parameterHandler、resultHanlder
,原理就是在mybatis-config配置插件信息,在解析mybatis-config.xml的时候会注册拦截信息到configuration的拦截链,然后在创建上面四个对象的时候实现增强,在具体调用拦截方法的时候,会
调用到Plugin的invoke方法,在invoke中委托给插件处理。

浙公网安备 33010602011771号