Mybatis通过插件机制,提供扩展性。
Mybatis的插件机制,是拦截器的思想,不同于Filter,interceptor之类的拦截器。Mybatis插件使用动态代理+责任链模式来实现。
- 动态代理: 负责对目标对象,进行某一个方面的增强
- 责任链模式: 负责组织所有的代理增强链式调用。
1.Mybaits 插件
1.1拦截目标
插件的插入点在Configuration类中。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); //拦截 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); //拦截 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); //拦截 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } 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; }
拦截的目标对象:
- Executor: 执行增删改查操作
- StatementHandler:处理sql语句预编译,设置参数等相关工作;
- ParameterHandler: 参数处理器
- ResultSetHandler:结果处理器
1.2原理
插件两个重要点
- 要有一个 实现了
org.apache.ibatis.plugin.Interceptor
接口的拦截器 - plugin方法里,需要调用
Plugin.wrap(Object target, Interceptor interceptor)
把目标类与拦截器包装成一个插件
Mybaits通过责任链的方式将,插件层层作用在目标对象上。
executor = (Executor) interceptorChain.pluginAll(executor); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public Object plugin(Object target) { return Plugin.wrap(target, this); }
作用过程发生在Plugin.wrap
方法上,将目标对象与Mybatis拦截器包装成一个插件。
public class Plugin implements InvocationHandler { private Object target; private Interceptor interceptor; private Map<Class<?>, Set<Method>> signatureMap; public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取签名Map Class<?> type = target.getClass(); // 拦截目标 (ParameterHandler|ResultSetHandler|StatementHandler|Executor) Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 获取目标接口 if (interfaces.length > 0) { return Proxy.newProxyInstance( // 生成代理 type.getClassLoader(), interfaces,//目标接口 new Plugin(target, interceptor, signatureMap));//创建一个插件 } return target; } }
JDK动态代理两要素:
- 目标接口
- InvocationHandler 增强
Plugin类实现了InvocationHandler接口,说明Plugin其实就是这个增强器。
所以
Plugin.wrap
方法其实就是把目标对象与Mybatis拦截器包装成一个JDK动态代理增强器
。
我们看生成代理的那个地方
return Proxy.newProxyInstance( // 生成代理 type.getClassLoader(), interfaces,//目标接口 new Plugin(target, interceptor, signatureMap));//增强
根据目标对象,插件interceptor,签名方法集,三个参数创建一个Plugin 作为一个增强器,去增强target的代理对象。
当执行代理对象的方法时,执行增强器Plugin 的invoke方法。在invoke方法中调用Mybatis拦截器。
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); } }
这样插件就借助JDK动态代理,完美的实现了对目标对象的拦截与增强。
执行流程
代理对象方法---> (InvocationHandler)代理增强Plugin对象#invoke方法--->Mybaits拦截器#intercept方法--->....--->目标对象方法
小结:
- Mybatis插件Plugin的本质是一个JDK动态增强器InvocationHandler
- Mybatis 拦截器作为Plugin的一个属性,借助Plugin(即JDK动态代理增强)实现对目标方法的拦截
2.PageHelper 插件
PageHelper是我们平时常用的分页插件。下面我们看看其原理
2.1使用
PageHelper.startPage(1, 10); List<User> list = userMapper.selectIf(1);
2.2原理
PageHelper 根据参数在 ThreadLocal 中设置了 Page 对象,能取到就代表需要分页,在分页完成后在移除,这样就不会导致其他方法分页。
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page<E>(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当已经执行过orderBy的时候 Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; }
在PageHelper分页插件中,PageInterceptor 是对Mybatis查询分页查询的拦截器。直接看其intercept方法
public class PageInterceptor implements Interceptor { //方法比较长,取分页查询部分 public Object intercept(Invocation invocation) throws Throwable { ... //判断是否需要进行分页查询 if (dialect.beforePage(ms, parameter, rowBounds)) { //生成分页的缓存 key CacheKey pageKey = cacheKey; //处理参数对象 parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); //调用方言获取分页 sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter); //设置动态参数 for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //执行分页查询 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不执行分页的情况下,也不执行内存分页 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } } }
这里只提两个点,具体的可以去看源码
2.2.1处理参数对象
目的就是把设置分页参数,设置到参数集上
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
Mysql方言类MySqlDialect
public class MySqlDialect extends AbstractHelperDialect { @Override public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) { paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());//设置起始页 paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());//设置分页大小 //处理pageKey pageKey.update(page.getStartRow()); pageKey.update(page.getPageSize()); //处理参数配置 if (boundSql.getParameterMappings() != null) { List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(); if (boundSql != null && boundSql.getParameterMappings() != null) { newParameterMappings.addAll(boundSql.getParameterMappings()); } if (page.getStartRow() == 0) { newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build()); } else { newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, Integer.class).build()); newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build()); } MetaObject metaObject = MetaObjectUtil.forObject(boundSql); metaObject.setValue("parameterMappings", newParameterMappings); } return paramMap; } }
可以看到:
从线程本地变量中取出当前Page对象,获取到起始行与分页大小设置到参数集中
2.2.2调用方言获取分页 sql
目的把分页语句拼接到SQL上
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
Mysql方言类MySqlDialect
public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT ? "); } else { sqlBuilder.append(" LIMIT ?, ? "); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
可以看出:
将分页语句 LIMIT 拼接到SQL上 。
3.总结
- Mybatis插件运用了JDK动态代理和责任链设计模式
- 插件Plugin 本质是一个JDK动态代理增强器
- Mybatis拦截器借助Plugin 对 Mybatis进行具体的增强。