官方文档中提到:Mybatis 允许在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatais 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
插件的使用非常简单,只需要实现 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();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
然后将该插件配置到 Configuration
中:
java形式
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Bean
public MybatisEntityPluginInterceptor mybatisEntityPluginInterceptor(){
ExamplePlugin e = new ExamplePlugin();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(e);
}
return e;
}
或者使用xml配置:
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
插件类最终会被放入到 Configuration
对象的 interceptorChain
成员变量中。
上一篇描述执行过程时,忽略了插件,下面列出的是在上篇文章提到插件机制的地方:
Executor 创建的时候:
executor = (Executor) interceptorChain.pluginAll(executor);StatementHandler 创建的时候:
statementHandler=(StatementHandler)interceptorChain.pluginAll(statementHandler);ParameterHandler 创建的时候:
parameterHandler=(ParameterHandler)interceptorChain.pluginAll(parameterHandler);ResultSetHandler 创建的时候:
resultSetHandler= (ResultSetHandler)interceptorChain.pluginAll(resultSetHandler);
这刚好与上文官方文档给出的插件支持的拦截相吻合,为了支撑拦截,我们能看到的似乎只有这一行代码,而且对于不同的对象,代码居然相同。下面来细谈这个过程。
首先,看到 InterceptorChain
这个类,该类的代码比较简单:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
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);
}
}
假设 interceptors 已正确赋值,我们的插件 ExamplePlugin
已正确的添加了进来。那么在执行过程中,调用的 pluginAll()
方法遍历了拦截器,并调用拦截器的 plugin 方法。
拦截器的类定义为:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
所以,plugin 方法由自己实现,实现一般如下:
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
可以看出做拦截,其实是包装目标对象,并返回新的对象出去。mybatis 提供了一个这样的静态方法:Plugin.wrap()
。
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;
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
这一步所做的操作是根据 Intercepts
注解,获取需要代理的接口和方法,然后根据接口生成反射对象,最终返回的便是这个反射生成的对象。
所以,插件机制的本质便是通过反射需要的 InvocationHandler
类来完成调用拦截,这里 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);
}
}
可以看出只拦截通过注解指定的方法,并且会将目标对象、方法和参数包装为 Invocation
对象,传递给拦截器的 intercept()
方法,由拦截器自己决定是否还需要再调用目标对象的原方法。,这整个过程可总结为如下图(以 Executor 为例):

对象的生成过程一般与它在运行时方法的调用过程顺序相反。
这便是整个拦截过程了。从本质来看,它是通过代理实现。需要注意的是拦截器调用的顺序。比如说拦截器 A,B,C 按照顺序添加到这个 InterceptorChain
中,那么调用的顺序应该是相反的,即 C, B, A, target。当然,是否允许方法的调用蔓延这整个链,需要你主动的在拦截器的 intercept() 方法中调用传入的参数 Invocation 对象的 proceed() 方法。
mybatis 确实使用了较多的反射,并且这还是在执行映射方法的过程中生成的。倘若在实现 Interceptor 的 plugin 方法时,根据拦截器想要拦截的目标类的类型做下判断,然后再决定是否使用 Plugin
进行 wrap 操作,那么,这应该算个小的优化。虽然当类型不匹配时,调用 plugin 方法不会产生任何错误(它返回了target),但它需要执行额外的解析拦截器元数据动作。所以,我们能够提前做出判断来避免这种情况是最好的。
mybatis 的插件机制足够简单,这也决定了它的限制很小,我们可以尽情发挥。不过在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。因为这些都是更底层的类和方法,所以使用插件的时候需要确保自己足够了解,即便如此,也需要特别当心。




