大神,都是能够主导自己生活的人;要做一个引导生活的人,不要做一个被生活引导的人。
1.Mybaits 插件1.1拦截目标1.2原理2.PageHelper 插件2.1使用2.2原理2.2.1处理参数对象2.2.2调用方言获取分页 sql3.总结
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进行具体的增强。
如果本文任何错误,请批评指教,不胜感激 !
微信公众号:源码行动
享学源码,行动起来,来源码行动
