MyBatis 是现在比较流行的 ORM 框架,得益于它简单易用,虽然增加了开发者的一些操作,但是带来了设计和使用上的灵活,得到广泛的使用。之前的一篇文章MyBatis 初始化之XML解析详解中我们已经知道了 MyBatis 的加载 XML 配置文件和加载 mappers 配置文件的流程,最终都是封装到了 Configuration 对象中。而MyBatis 插件功能也是 MyBatis 的一块重要功能。我们知道其可以在 DAO 层进行拦截,如实现分页、SQL语句执行的性能监控、公共字段统一赋值等功能。但对其内部实现机制,涉及的软件设计模式,编程思想往往没有深入的理解。本文对 MyBatis 加载 XML 配置文件中 plugins 插件流程以及对 MyBatis 插件实现原理进行深入分析,并实现自己的简单分页插件。
MyBatis 插件主要用到的类再 org.apache.ibatis.plugin 包下,如下图所示:
可以看到,叫 MyBatis 拦截器可能更合适些,实际上它就是一个拦截器,使用 JDK 动态代理方式,实现在方法级别上进行拦截。支持拦截的方法有以下几种:
插件配置信息也是配置在 XML 配置文件中,所以我们直接从 XMLConfigBuilder 的 parseConfiguration 方法开始,可以看到 pluginElement(root.evalNode("plugins")) 用于解析 XML配置中的 plugins 节点标签。而 pluginElement 方法实现的主要功能就是遍历 plugins 节点中的 plugin 子节点,获取配置的 interceptor 属性为具体的插件实现,并将其添加到 Configuration 中的 interceptorChain中。interceptorChain 是拦截器链,将拦截器存在 interceptors (是一个 Interceptor 数组)中。
private void parseConfiguration(XNode root) {
try {
...
// 解析<plugins>节点
pluginElement(root.evalNode("plugins"));
...
// 解析<mappers>节点
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
至此,插件加载流程结束,相对比较简单,具体流程图如下图所示:
Mybatis插件的实现机制主要是基于动态代理实现的,其中最为关键的就是代理对象的生成,所以有必要来了解下这些代理对象是如何生成的。
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) {
return newExecutor(transaction, defaultExecutorType);
}
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;
}
观察源码,发现这些可拦截的类对应的对象生成都是通过InterceptorChain的pluginAll方法来创建的,进一步观察pluginAll方法,如下:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
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,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码;
接下来看一下我们在编写拦截器的时候,一个典型的plugin方法实现方式,如下:InvocationHandler
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;
}
由于真正去执行Executor、ParameterHandler、ResultSetHandler和StatementHandler类中的方法的对象是代理对象(建议将代理对象转为class文件,反编译查看其结构,帮助理解),所以在执行方法时,首先调用的是Plugin类(实现了InvocationHandler接口)的invoke方法,如下:
@Override
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);
}
}
首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合;
判断当前方法需不需要执行拦截逻辑,需要的话,执行拦截逻辑方法(即Interceptor接口的intercept方法实现),不需要则直接执行原方法。
可以关注下Interceptor接口的intercept方法实现,一般需要用户自定义实现逻辑,其中有一个重要参数,即Invocation类,通过改参数我们可以获取执行对象,执行方法,以及执行方法上的参数,从而进行各种业务逻辑实现,一般在该方法的最后一句代码都是invocation.proceed()(内部执行method.invoke方法),否则将无法执行下一个拦截器的intercept方法。
以上逻辑对应的时序图如下,这里我们以执行executor对象的query方法为例,且假设有两个拦截器存在:
这里以分页插件为例,来了解下一般mybatis插件的编写规则,如下所示:
<plugins>
<plugin interceptor="com.hycjack.mybatis.pagehelper.PageInterceptor">
<property name="defaultPageSize" value="10"/>
<property name="defaultPageIndex" value="1"/>
</plugin>
</plugins>
主要需要实现三个方法
intercept:在此实现自己的拦截逻辑,可从Invocation参数中拿到执行方法的对象,方法,方法参数,从而实现各种业务逻辑, 如下代码所示,从invocation中获取的statementHandler对象即为被代理对象,基于该对象,我们获取到了执行的原始SQL语句,以及prepare方法上的分页参数,并更改SQL语句为新的分页语句,最后调用invocation.proceed()返回结果。
plugin:生成代理对象;
setProperties:设置一些属性变量;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
})
public class PageInterceptor implements Interceptor {
/**
* 默认页码
*/
private Integer defaultPageIndex;
/**
* 默认每页数据条数
*/
private Integer defaultPageSize;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = getUnProxyObject(invocation);
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
String sql = getSql(metaObject);
if (!checkSelect(sql)) {
// 不是select语句,进入责任链下一层
return invocation.proceed();
}
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
Object parameterObject = boundSql.getParameterObject();
Page page = getPage(parameterObject);
if (page == null) {
// 没有传入page对象,不执行分页处理,进入责任链下一层
return invocation.proceed();
}
// 设置分页默认值
if (page.getPageNum() == null) {
page.setPageNum(this.defaultPageIndex);
}
if (page.getPageSize() == null) {
page.setPageSize(this.defaultPageSize);
}
// 设置分页总数,数据总数
setTotalToPage(page, invocation, metaObject, boundSql);
// 校验分页参数
checkPage(page);
return changeSql(invocation, metaObject, boundSql, page);
}
@Override
public Object plugin(Object target) {
// 生成代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 初始化配置的默认页码,无配置则默认1
this.defaultPageIndex = Integer.parseInt(properties.getProperty("defaultPageIndex", "1"));
// 初始化配置的默认数据条数,无配置则默认20
this.defaultPageSize = Integer.parseInt(properties.getProperty("defaultPageSize", "20"));
}
}
简单的说,mybatis插件就是对ParameterHandler、ResultSetHandler、StatementHandler、Executor这四个接口上的方法进行拦截,利用JDK动态代理机制,为这些接口的实现类创建代理对象,在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑,所以真正要用好mybatis插件,主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义;
另外,如果配置了多个拦截器的话,会出现层层代理的情况,即代理对象代理了另外一个代理对象,形成一个代理链条,执行的时候,也是层层执行;
本文由 HycJack 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.hycjack.cn/archives/mybatis插件机制详解
最后更新:2021-01-18 22:00:49
Update your browser to view this website correctly. Update my browser now