mybatis定义了一个接口 org.apache.ibatis.plugin.Interceptor,任何自定义插件都需要实现这个接口。原理类似于拦截器。
拦截范围
自定义插件可以拦截mybatis的4大对象ParameterHandler、ResultSetHandler、StatementHandler、Executor,但并不是所有的方法都可以拦截。
Interceptor 接口
package org.apache.ibatis.plugin;
import java.util.Properties;
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
Interceptor接口提供了三个方法。
1:intercept
拦截方法,里面是具体的拦截逻辑。通过参数Invocation 可获得拦截的对象、方法、参数。
2:plugin
接口提供默认实现,为拦截对象创建代理
3:setProperties
为拦截器设置属性
PageHelper实现原理
PageHelper 实现了Interceptor 接口,拦截Executor对象的query(MappedStatement mappedStatement, Object obj, RowBounds rowBounds, ResultHandler resultHandler) 方法
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {
……
}
测试代码
@Test
public void testPageHelper () throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
PageHelper.startPage(2,3);
List list = mapper.findAll();
for (User user : list) {
System.out.println(user);
}
sqlSession.close();
}
1.读取配置文件,解析plugins标签。为拦截器创建实例,添加到interceptorChain中
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
执行上面代码时,会解析配置文件,每个标签都进行解析,这里看解析plugins标签
配置文件里,plugins标签要解析的内容
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//取出配置文件里plugin标签里的interceptor属性值,这里为com.github.pagehelper.PageHelper
String interceptor = child.getStringAttribute("interceptor");
//取出该interceptor的property
Properties properties = child.getChildrenAsProperties();
//通过反射创建拦截器实例,强转为Interceptor 类型
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
//为该拦截器实例设置属性
interceptorInstance.setProperties(properties);
//将拦截器实例添加到configuration中的interceptorChain中(ArrayList)
configuration.addInterceptor(interceptorInstance);
}
}
}
2.SqlSession实例化时,创建executor对象,然后在遍历plugins的时候,代理嵌套增强executor
SqlSession sqlSession = sqlSessionFactory.openSession();
Configuration中创建Executor,StatementHandler,parameterHandler,ResultSetHandler时调用对应的newXXX方法,方法中都会调用Configuration中的属性interceptorChains的pluginAll方法
创建executor时,调用pluginAll方法对其进行增强(JDK动态代理)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
循环对目标对象进行层层代理,生成最终的代理对象。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
PageHelper重写了plugin方法,只拦截Executor
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
Plugin的wrap方法,通过动态代理增强
public static Object wrap(Object target, Interceptor interceptor) {
Map, Set> 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;
}
Plugin类实现了 InvocationHandler 接口,覆盖了invoke方法。
当动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。
Plugin类的invoke方法的执行逻辑为:
如果是定义的拦截的方法 就执行拦截器的intercept方法;
不是需要拦截的方法 直接执行
3.PageHelper.startPage(2,3);
public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page oldPage = SqlUtil.getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
SqlUtil.setLocalPage(page);
return page;
}
新建Page对象并初始化,并保存到ThreadLoacl中
public class SqlUtil implements Constant {
private static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
……
}
ThreadLocal可以在指定线程内存取数据。每个线程都互不干扰。
4.List
执行查询方法时,先动态创建mapper的代理类,然后底层会执行Executor的query方法,正是PageHelper要拦截的方法。所以此时程序会走到PageHelper的intercept方法中。
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
关键代码
sqlUtil.processPage(invocation);
在出现异常时也可以清空Threadlocal。这也是为什么调用PageHelper.startPage()方法后的第一个查询语句会分页而再次执行的查询语句不会分页。
public Object processPage(Invocation invocation) throws Throwable {
try {
Object result = _processPage(invocation);
return result;
} finally {
clearLocalPage();
}
}
从本地线程中获取page
private Object _processPage(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
Page page = null;
//支持方法参数时,会先尝试获取Page
if (supportMethodsArguments) {
page = getPage(args);
}
//分页信息
RowBounds rowBounds = (RowBounds) args[2];
//支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
if ((supportMethodsArguments && page == null)
//当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
|| (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
return invocation.proceed();
} else {
//不支持分页参数时,page==null,这里需要获取
if (!supportMethodsArguments && page == null) {
page = getPage(args);
}
return doProcessPage(invocation, page, args);
}
}
在doProcessPage(invocation, page, args) 方法中
1.新建查询数据总记录数的MappedStatement,放到缓存中
取出缓存中的ms,放行,获取到数据总记录数
2.还原ms,获取boundSql,设置参数后放行
放行后,执行Excutor的query方法
最终执行了MysqlParser 里的getPageSql方法,拼接了sql语句,然后去执行
public class MysqlParser extends AbstractParser {
@Override
public String getPageSql(String sql) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
sqlBuilder.append(" limit ?,?");
return sqlBuilder.toString();
}
自定义分页插件
1.自定义一个类,实现Interceptor 接口,覆盖三个方法
2.在配置文件中配置插件
如下代码,实现以 "ByPager"结尾的方法,sql语句后拼接limit语句实现分页
package com.myown.interceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.Connection;
import java.util.Map;
import java.util.Properties;
@Intercepts({@Signature(type= StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})
public class MyPageInterceptor implements Interceptor {
private int page;
private int size;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取拦截对象
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");
String mapId = mappedStatement.getId();
if(mapId.matches(".+ByPager$")){
ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
Map params = (Map)parameterHandler.getParameterObject();
page = (int)params.get("page");
size = (int)params.get("size");
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
sql += " limit "+(page-1)*size +","+size;
metaObject.setValue("delegate.boundSql.sql", sql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}