上次,我们说过了mybatis+springboot时的启动与执行流程,也介绍过mybatis的执行器和缓存,今天,我们来看看mybatis 的另一个大功能 —— plugin
MyBatis的plugin插件是用来拦截SQL执行的,对SQL进行增强的一种机制。
MyBatis的Plugin实现基于JDK动态代理机制,在MyBatis初始化过程中,可以为指定的拦截对象生成代理对象,当拦截对象执行某个方法时,代理会先执行插件中的逻辑,再执行原有逻辑。插件可以在原有逻辑前后添加自己的逻辑或者完全替换原有逻辑
如果你使用过spring的话,会自然的想到spring的AOP特性,两者都是利用代理来实现功能的增强
这是一个旧项目,在后期对接Oracle后,有很多sql报了错,其原因是使用 instr() 函数时,由于参数是外部传入的,有时候可能会传来一个几千长度的字符串,从而导致instr 超长报错。因为这样的sql还有很多,不可能一一去改,所以必须使用功能增强的方式来解决
在直接上示例之前,我们先看看官方提供的接口 Interceptor.java ,只要实现了该接口,就可以在指定位置发挥作用
package org.apache.ibatis.plugin;
public interface Interceptor {
/**
* intercept方法就是要进行拦截的时候要执行的方法
*/
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
当然,这个接口还需要配合另一个注解 @Intercepts 使用,我们结合案例写一个插件看看
@Component
@Intercepts({ @Signature(
type = StatementHandler.class,
method = "prepare",
args = { Connection.class, Integer.class }) })
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
String newSql = null;
// 改造sql部分省略,主要是将 instr() 拆分成 instr() or instr() 的形式以降低每个括号内 的长度...
// newsql = sql.replace(...)
// 把新的sql通过反射重新设置回去
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql , newSql);
Object returnVal = invocation.proceed();
return returnVal;
}
}
不难看出,配上注解后,该插件的意思就是针对 StatementHandler.prepare(Connection, Integer) 方法进行增强,我们实际运行下看看:
如图,最终走到了我们写的插件的 intercept 方法中
需要注意的是 @Intercepts 注解内支持配置 @Signature 数组,并以逗号分割。也就是说一个拦截器其实可以拦截多个类的方法,如下
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}),
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
其实从上面,案例看出,我们对插件的设置主要是通过 @Intercepts 内的 @Signature 注解实现的
@Intercepts({ @Signature(
type = StatementHandler.class,
method = "prepare",
args = { Connection.class, Integer.class }) })
其中,type 就是作用的接口,method 和 args 则能确定唯一方法(单用方法名,可能有方法重载的情况)
但是,是不是这些属性可以随便填呢?其实不是的,mybatis没有做的那么自由,其更像Spring中的postProcessor机制,只在固定的几个位置有预留点,让你可以自定义增强,而不是开放所有位置。
这里的Pulgin只针对以下四个接口有增强预留点,它们分别是
即我们自己创建了个 Interceptor 实现类,也使用了 @Intercepts 注解,但这个类是如何被mybatis加载的呢?
我们仍以上篇文章的springboot+mybatis为例,那么此处便又要提到spring-boot的自动配置了,我们看下 MybatisAutoConfiguration (mybatis-spring-boot-autoconfigure2.1.4版本)这个自动配置类,看其构造方法
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
// 省略部分代码
public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
this.interceptors = interceptorsProvider.getIfAvailable();
this.typeHandlers = typeHandlersProvider.getIfAvailable();
this.languageDrivers = languageDriversProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = databaseIdProvider.getIfAvailable();
this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}
}
其构造方法的第二个参数 :ObjectProvider
ObjectProvider 是在spring 4.3 引入的一种注入方式,它可以检索指定的类型。
然后通过 getIfAvailable 和 getIfUnique 从spring容器中检索出对应对象
因为我们已经在自定义的 ExamplePlugin 上使用了@Component 的注解,所以此处使用自动注入,能获取到我们的插件理所当然。
而后再把该值赋给 sqlSessionFactoryBean, 然后再赋给 mybatis 真正的配置类 Configuration。至此,我们的插件就被 mybatis 系统所成功加载了。
如果还没有使用上spring-boot,没有所谓的自动配置,那也无妨,只是需要手动额外配置一点参数也是同样的。
如:已经在 application.properties 配置了mybatis 配置文件
mybatis.config.location: classpath:/mybatis-config.xml
然后在mybatis-config.xml 里加上如下配置
<configuration>
<plugins>
<plugin interceptor="com.zhanfu.spring.demo.utils.ExamplePlugin"/>
plugins>
configuration>
这样也能达到,将指定插件放入 mybatis 框架的效果
上面我们讲了,如何写一个插件,以及插件是怎么交给 myBatis框架的,现在要谈最重要的内容了。即myBatis 是如何利用插件的。
上文我们已经了解到了,所有的插件实例都被放入了 myBatis 的总配置类 Configuration 去管理,成为了该类的一个属性interceptorChain ,该类详情如下:
public class InterceptorChain {
// 所有的插件都存在这个 List 中
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);
}
}
所以我们只要看该类拿这些插件做了什么即可,可以看到,该类对所有新建的目标对象,都进行了 pluginAll 操作,结合上图,我们不难看出,该方法其实就是遍历所有插件,然后调用每个插件的 plugin() 方法 生成一个新对象,然后下一个插件拿这个新对象再 plugin() 生成一个新对象,实际上构成了一套链式的嵌套
那么plugin() 方法到底做了什么呢?我们来看看回头再来看看 Interceptor 接口里,该方法的默认实现
// Interceptor.java
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// Plugin.java
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代理并返回。注意,interceptor是我们的插件对象,signatureMap是插件注释解析到的类与方法
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 如果没有,则返回原对象
return target;
}
综上,不难看出,只有生成指定的四种实例时,才会进入上述代码生成代理,最后返还的其实就是代理对象。需要注意的是,此时的代理是能够代理这些接口的所有方法的,要想实现指定方法才使用代理,还得依靠代理的 invoke 方法内去筛选
// Plugin.java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 只有注解上指定方法才能走插件对象的 intercept 方法
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);
}
}