MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。
默认情况下,可以使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
Mybatis提供的插件很强大,可以由外切入到底层的核心模块,所以用起来要非常小心,至少要熟悉一些底层的原理。
但是使用起来还是很简单,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
接着来剖析下插件的原理。
配置与解析
以开源的某个分页插件为例:
<plugins>
<plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
<property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.MySQLDialect" />
plugin>
plugins>
解析配置文件:
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
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).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
Configuration中持有interceptorChain:
protected final InterceptorChain interceptorChain = new InterceptorChain();
把配置文件中的插件添加到InterceptorChain中:
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
InterceptorChain中持有Interceptor集合,每个Interceptor都被添加到这个集合中。
public class InterceptorChain {
private final List 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 interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
DefaultSqlSessionFactory
final Executor executor = configuration.newExecutor(tx, execType);
使用Configuration的newExecutor方法来创建Executor:
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
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, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
同理还有
- Configuration#newParameterHandler
- Configuration#newResultSetHandler
- Configuration#newStatementHandler
就是对应上面的几处埋点,在Configuration中构造的时候使用了下面的方法挂载插件:
executor = (Executor) interceptorChain.pluginAll(executor);
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
这是一个层层包裹的过程,后面的拦截器会不断包装前面的。最后一个拦截器的方法会优先执行,然后层层代理。
一般在插件的XxInterceptor实现中,会包装一个代理类:
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class OffsetLimitInterceptor implements Interceptor {
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//......
}
Plugin类的包装方法:
public static Object wrap(Object target, Interceptor interceptor) {
Map, Set> 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;
}
这个包装方法首先会获取拦截器的相关注解(Intercepts,Signature),构造一个
Map
private static Class>[] getAllInterfaces(Class> type, Map<Class>, Set<Method>> signatureMap) {
Set> interfaces = new HashSet>();
while (type != null) {
for (Class> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class>[interfaces.size()]);
}
如果指定的type有接口就创建代理,否则返回其本身。
根据Plugin的实现:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set 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);
}
}
首先检查签名集合中是否包含指定的方法,如果没有则直接调用目标方法。
如果匹配上就new一个Invocation传给interceptor的intercept方法,到时候在自定义的interceptor中可以直接通过invocation.proceed()调用目标方法:
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
有点类似过滤器的放行。
插件的实现就是在几处需要拦截的地方用动态代理技术来生成代理类,以拦截器的形式完成方法的增强。根据@Signature注解的type元素的接口有无来判断是否生成代理类,根据指定方法和注解的method是否一致来决定是否调用拦截器方法。
使用动态代理技术让我们可以切入到底层的实现却不用修改底层的代码,就像一个楔子插进去也可以拔出来,这就是所谓的插件。
插件相关的总结
插入
- 插件是通过JDK动态代理实现的
这是插件的原理和核心。
了解了动态代理的机制,再写个例子看下JDK为我们生成的代理类,可以有个更清晰的把握。
- MetaObject在插件中的运用
可以通过MetaObject利用反射完成相关属性的赋值,偷梁换柱达到我们的目的。
不管有多少拦截器,只要满足条件,就是一层层的代理,我们可以层层剥离它,如针对StatementHandler的prepare方法的一个插件:
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 层层剥离
while (metaObject.hasGetter("h")) {
Object object = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(object);
}
只要它含有h属性,就说明它是一个代理(生成的代理类是继承自Proxy的,所以继承了InvocationHandler类型的h)。
具体代码可以点此查看:
模拟分页插件
拔出
- Invocation就是让方法切回去
Invocation封装了让原方法执行的必要参数,并提供了一个proceed方法:
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
当你拦截了指定方法,做了该做的事之后,调用Invocation的proceed方法可以让框架继续执行。
- 方法切回去后是否达到目的
当我们为了达到某个目的写了一个插件,要考虑当前的一系列操作是否会对后面方法的执行造成影响。
有时候我们需要更改某些东西,这些东西改完是否能顺利set进去(几大关键类提供的set方法不多);有时候是想获取某些东西,不能影响后续的执行。这些都要对底层的方法有所了解。