插件简介
MyBatis提供了一种插件(plugin)的功能,虽然叫做插件
,但其实是拦截器
的功能,实现方式就是在拦截器的责任链中添加一个Interceptor
。
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) 拦截执行器的方法;
- ParameterHandler (getParameterObject, setParameters) 拦截参数的处理;
- ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理;
- StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理;
注意:括号中是允许拦截的方法。比如Executor.query()等
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
下面是Mybatis的底层设计,其中除了对外暴露的SqlSession插件不拦截,后面的其他流程都可以利用插件进行拦截。
上图Mybatis框架的整个执行过程。Mybatis插件能够对则四大对象进行拦截,可以包含到了Mybatis一次会议的所有操作。可见Mybatis的的插件很强大。
- Executor是 Mybatis的内部执行器,它负责
调用StatementHandler操作数据库
,并把结果集通过 ResultSetHandler进行自动映射
,另外,他还处理了二级缓存
的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。 - StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。
- ParameterHandler是Mybatis
解析处理SQL入参的处理器
。插件可以改变我们SQL的参数默认设置。 - ResultSetHandler是Mybatis
把ResultSet集合映射成POJO的处理器
。我们可以定义插件对Mybatis的结果集自动映射进行修改。
插件的使用
要想实现一个自定义插件(Mybatis限制查询数据插件),需要实现Interceptor
接口,下面就看下接口定义
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
自定义一个插件只需要实现这个接口,并告诉Mybatis,这个插件拦截的方法是哪个就行了,下面以官网提供的为例:
// 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;
}
}
要使插件生效还需要交给容器管理,所以可以在mybatis-config.xml
配置中添加如下配置:
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
如果使用的Springboot可以使用注解添加到容器:@Component/@Bean
等;
其中@Intercepts
声明为一个Mybatis插件,@Signature
声明需要拦截的签名,可以有多个。比如上面的例子,就说明要拦截的类是Executor
,拦截这个类方法是update
,这个方法的参数类型:{MappedStatement.class,Object.class}
。
源码分析
下面我们分析一下这段代码背后的源码。
首先从源头->配置文件开始分析:
XMLConfigBuilder解析MyBatis全局配置文件mybatis-config.xml
的时候回去解析plugins节点
:
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"));
// mybatis插件
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
// 创建DataSourceFactory
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);
}
}
上面会调用pluginElement(root.evalNode("plugins"))
去解析配置中配置拦截器,并且通过反射实例化,最后添加到configuration.interceptorChain
中,具体看下面的代码:
/**
* 将mybatis插件 添加到拦截器的责任链中
* @param parent
* @throws Exception
*/
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);
}
}
}
添加 拦截器到 configuration.interceptorChain
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
上面看到会解析后 拦截器会添加到InterceptorChain
中,下面就看下它是什么?
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);
}
到此为止,我们已经将配置中配置的插件(也就是拦截器),解析后通过反射实例,最后放入到了configuration.interceptorChain
中。下面就看下为何拦截器会拦截这些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 注意看这里,调用了interceptorChain.pluginAll()
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);
// 注意看这里,调用了interceptorChain.pluginAll()
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);
// 注意看这里,调用了interceptorChain.pluginAll()
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
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);
}
// 注意看这里,调用了interceptorChain.pluginAll()
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
以上4个方法都是Configuration
的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler
。
这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll
方法,下面就看看pluginAll()方法做了什么?
InterceptorChain
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
可以看到只是遍历List<Interceptor>
,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象
最后看一下plugin方法中调用的Plugin.wrap(target, this)
:
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);
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)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
这里Plugin
实现了InvocationHandler
,所以是一个动态代理Handler。wrap
方法会生成代理对象,并且把类和方法签名保存到一个Map中。
最后在执行(Executor,ParameterHandler,ResultSetHandler,StatementHandler)
方法时 ,就会执行Plugin.invoke()
,下面看看invoke的实现:
private final Map<Class<?>, Set<Method>> signatureMap;
@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);
}
}
如果方法签名在Map中存在,就会调用interceptor.intercept
,也就会执行我们自定义的插件功能。
总结
MyBatis拦截器接口提供的3个方法中,plugin方法用于某些处理器(Handler)的构建过程。interceptor方法用于处理代理类的执行。setProperties方法用于拦截器属性的设置。
mybatis的解析xml的时候会把配置中interceptor
插件,保存到configuration.interceptorChain
中。并且在实例化拦截的类时(Executor,ParameterHandler,ResultSetHandler,StatementHandler)
,生成对应的代理对象,并把这些类的签名保存在Map中。最后在执行数据库操作时会执行Plugin.invoke()
,如果方法在Map中,就会调用我们自定义的插件。
注意:自定义插件需要谨慎操作,因为如果使用不正确很可能会中断数据库操作,并且还会破坏Mybatis的整体设计。