mybatis 官网: mybatis.org/mybatis-3/z…
Mybatis 中的插件机制非常的方便,我们可以用它来完成很多操作 例如:分页、数据权限、参数处理 等。今天我们来从源码的角度认识一下 mybatis 的插件是如何设计 以及 如何工作的。
插件简介
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor : update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
- ParameterHandler : getParameterObject, setParameters)
- ResultSetHandler : handleResultSets, handleOutputParameters
- StatementHandler : prepare, parameterize, batch, update, query
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。
插件源码设计
插件的源码就在 mybatis 的 plugin 包下,我们来看看里面都包含了什么
这里的类其实很少,除了几个已经用过的类以外,其中最重要的就是 Plugin 类,就是通过这个类使得我们的插件可以达到扩展目标类的指定方法的目的。
Plugin.wrap
在实现 Interceptor 接口的时候,我们可以看到这里面有一个 default 方法 调用了 Plugin.wrap 方法 返回了一个Object。Plugin.wrap(target, this)
这个方法是用于创建目标对象的代理对象,也就是使得这个插件生效的关键步骤。 我们来看看这个 wrap 方法中都写了什么
public static Object wrap(Object target, Interceptor interceptor) {
// 获得interceptor配置的@Signature的type
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 当前代理类型
Class<?> type = target.getClass();
// 根据当前代理类型 和 @signature指定的type进行配对, 配对成功则可以代理
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
入参:
- target:被代理的类(例如:Executor、ParameterHandler..)
- interceptor: 插件类(用户自定义的插件实现类)
这个方法大致的步骤是:
- 获取用户配置的需要代理的类和方法的Map(signatureMap),key 是类,value 是 方法 (
getSignatureMap(interceptor)) - 通过目标类匹配 signatureMap 中存储的类,通过
getSuperclass()方法不断循环匹配,匹配上则到Set中,最终返回Array。(getAllInterfaces(type, signatureMap)) - 如果 getAllInterfaces 方法有匹配上,则响应代理类,未匹配上则响应正常的target 类(不做代理)。
plugin.invoke
如果注解配置的信息与 target 类可以匹配上的话,则响应一个由 Plugin 代理的代理类,既然如此 那我们再一起来看看 这个类的 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);
}
}
这个类其实很简单,如果能匹配上 signatureMap 中的方法,则调用用户自定义插件的方法。否则则正常执行目标方法。
info:根据这个插件体系的设计架构,我们可以通过它来代理所有类的所有方法,如果通过它代理我们自己的类的话,则需要自己去初始化插件。
InterceptorChain
插件链条类,通过这个类可以将系统中的插件组成一条插件链,并且可以通过 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);
}
}
插件加载与初始化
拆解完了 插件的源码设计,我们来看看插件是如何加载到 mybatis 中 和 如何代理目标类的。
加载
在build SqlSessionFactory 的时 new SqlSessionFactoryBuilder().build(inputStream); 会构建出一个 Configuration ,所有的 xml 配置信息都会被解析到其中。
插件也不例外,解析插件的方法是:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
我们来看看这个解析方法中都干了些什么
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);
}
}
}
-
这里做了一个小举动,获取配置中的
Properties并调用用户插件类的setProperties方法。可以通过这个属性设置机制,在 xml 中给自己的插件设置不同的属性,然后在代码中使用。 -
configuration.addInterceptor(interceptorInstance);方法最后会调用interceptorChain.addInterceptor(interceptor);最后组成了一条插件链
这里有一个小的疑惑点,我看到这里时会担心所有插件都放到同一个
interceptorChain对象中,最终调用interceptorChain.pluginAll的话,岂不是所有插件都会生效?最后发现是我多虑了 , 因为
pluginAll方法实际会调用Plugin.wrap来生成目标类,而Plugin.wrap对传入的目标代理方法和当前类的注解进行校验,最终判断是否代理。并不是传入什么类就代理什么类。
初始化
默认把所有的插件都加载到 Configuration 类中,那 mybatis 是如何达到向 Executor、ParameterHandler、ResultSetHandler、StatementHandler 这四个类加上插件的呢?
这个问题的答案就在这四个类的初始化代码里面。
Executor
Executor :在默认的 openSession 时,会调用 final Executor executor = configuration.newExecutor(tx, execType);
在 configuration.newExecutor 方法中,则能看到 interceptorChain.pluginAll(executor); 最终将代理后的 executor 返回。
StatementHandler( ParameterHandler、ResultSetHandler)
构建 StatementHandler 的时机在 Executor 执行具体的数据库操作时,例如 doQuery、doUpdate 之类的 ( org.apache.ibatis.executor.BatchExecutor )
在执行行具体数据操作时,会从 configuration 中初始化 StatementHandler 对象:configuration.newStatementHandler(...);
我们来看看 newStatementHandler 的源码:
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;
}
StatementHandler statementHandler = new RoutingStatementHandler(...);
这行代码会根据 StatementType 构建对应的 StatementHandler,源码如下:
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
这里的三个 statementType 类都继承自BaseStatementHandler ,BaseStatementHandler 的构造方法中则初始化了 ParameterHandler、ResultSetHandler
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
至此 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四个类的初始化都交代了一遍,我总结一下:
- Executor:在 openSession 时构建,并通过
configuration.newExecutor初始化目标类 以及 加入插件代理。 - StatementHandler:在 Executor 执行数据库操作时构建,并通过
configuration.newStatementHandler初始化目标类 以及 加入插件代理。 - ParameterHandler:在构建 StatementHandler 时通过父类
BaseStatementHandler都构造方法构建,并通过configuration.newParameterHandler初始化目标类 以及 加入插件代理。 - ResultSetHandler:在构建 StatementHandler 时通过父类
BaseStatementHandler都构造方法构建,并通过configuration.newResultSetHandler初始化目标类 以及 加入插件代理。