⼤多数框架,都⽀持插件,⽤户可通过编写插件来⾃⾏扩展功能。Mybatis 也不例外,我们可以从插件配置、插件编写、插件运⾏原理、插件注册与执⾏拦截的时机、初始化插件、分⻚插件的原理等六个⽅⾯来对 Mybatis 插件进行阐述。
Mybatis 的插件配置在 configuration 内部,初始化时,会读取这些插件,保存于 Configuration 对象的 InterceptorChain 中。
org.apache.ibatis.session.Configuration 源码如下:
public class Configuration {
// 插件
protected final InterceptorChain interceptorChain = new InterceptorChain();
}
org.apache.ibatis.plugin.InterceptorChain 类定义如下:
public class InterceptorChain {
// 插件集合
private final List<Interceptor> interceptors = new ArrayList<>();
// 目标 target 执行插件
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);
}
}
上⾯的for循环代表了只要是插件,就会以责任链的⽅式逐⼀执⾏(别指望它能跳过某个节点),所谓插件,其实就类似于拦截器。
编写 Mybatis 插件必须实现org.apache.ibatis.plugin.Interceptor
接⼝。org.apache.ibatis.plugin.Interceptor
接⼝定义如下:
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
}
}
下面我们自定义一个插件:
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MyBatisInterceptor implements Interceptor {
private Integer value;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 这里就可以添加自己的业务逻辑
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
System.out.println(value);
if (target instanceof Executor || target instanceof StatementHandler) {
// Plugin类是插件的核⼼类,⽤于给target创建⼀个JDK的动态代理对象,触发intercept()⽅法
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 自定义 value
value = Integer.valueOf((String) properties.get("value"));
}
}
上面是一个简单的插件代码,主要有几个问题:
Mybatis 规定插件必须编写 Annotation 注解,是必须,⽽不是可选。
@Intercepts注解:装载⼀个 @Signature 列表,⼀个 @Signature 其实就是⼀个需要拦截的⽅法封装。那么,⼀个拦截器要拦截多个⽅法,⾃然就是⼀个 @Signature 列表。
type 含义:要拦截 Executor 接⼝内的 query() ⽅法,参数类型为 args 列表。
type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
使⽤ JDK 的动态代理,给 target 对象创建⼀个 delegate 代理对象,以此来实现⽅法拦截和增强功 能,它会回调 intercept() ⽅法。
org.apache.ibatis.plugin.Plugin 源码分析:
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 创建JDK动态代理对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@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)) {
// 回调intercept()⽅法
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// ......
return signatureMap;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
// ......
}
}
methods != null && methods.contains(method)
:这里判断是否是需要拦截的⽅法,这里很重要,⼀旦忽略了,都不知道Mybatis 是怎么判断是否执⾏拦截内容的。Mybatis 的插件配置在 configuration 内部,要探究其插件运行原理,还是要看 Configuration 类。
org.apache.ibatis.session.Configuration 源码如下:
public class Configuration {
// ......
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler =
mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 1、注册插件
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);
// 2、注册插件
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
// 根据路由规则,设置不同的StatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
rowBounds, resultHandler, boundSql);
// 3、注册插件
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);
}
// 4、注册插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
// ......
}
从源码中可以看出 Mybatis 只能拦截 ParameterHandler
、ResultSetHandler
、StatementHandler
、Executor
这4个接⼝对象内的⽅法。
重新审视interceptorChain.pluginAll()
⽅法:该⽅法在创建上述4个接⼝对象时调⽤,其含义为给这些接⼝对象注册
拦截器功能,注意是注册,⽽不是执⾏拦截。
拦截器执⾏时机:plugin()
⽅法注册拦截器后,那么,在执⾏上述4个接⼝对象内的具体⽅法时,就会⾃动触发拦截器的执⾏,也就是插件的执⾏。 所以,⼀定要分清,何时注册,何时执⾏。切不可认为 pluginAll() 或 plugin() 就是执⾏,它只是注册。
前面已经提到,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;
}
}
这里,对插件集合做循环,然后调用插件的 plugin() 方法:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
// 创建JDK动态代理对象
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
这里的 intercept 就是执行插件,它需要 Invocation 参数,org.apache.ibatis.plugin.Invocation 类定如下:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
org.apache.ibatis.plugin.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)) {
// 回调intercept()⽅法
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse 中,初始化了插件。
/**
* 全局配置文件的解析器
*/
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 从全局配置文件根节点开始解析,加载的信息设置到Configuration对象中
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
// 初始化插件
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
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();
// 这⾥就是setProperties()⽅法的调⽤时机
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
对于Mybatis来说,它并不区分是何种拦截器接⼝,所有的插件都是Interceptor。Mybatis完全依靠 Annotation 去标识对谁进⾏拦截,所以,具备接⼝⼀致性。
由于Mybatis采⽤的是逻辑分⻚,⽽⾮物理分⻚。那么,市场上就出现了可以实现物理分⻚的Mybatis的分⻚插件。
要实现物理分⻚,就需要对String sql进⾏拦截并增强,Mybatis通过BoundSql对象存储String sql,⽽ BoundSql 则由StatementHandler 对象获取。
public interface StatementHandler {
// 增强该接口
// String sql = getBoundSql();
// 分⻚语句: sql+"limit 语句"
// 查询总数语句:"SELECT COUNT(1) "" +sql.substring(from语句之后)
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
BoundSql getBoundSql();
}
因此,就需要编写⼀个针对 StatementHandler 的 query ⽅法拦截器,然后获取到sql,对sql进⾏重写增强。