Mybatis plugin的使用及原理示例解析
作者:战斧
前言
上次,我们说过了mybatis+springboot时的启动与执行流程,也介绍过mybatis的执行器和缓存,今天,我们来看看mybatis 的另一个大功能 —— plugin
一、Mybatis Plugin 是什么
MyBatis的plugin插件是用来拦截SQL执行的,对SQL进行增强的一种机制。
MyBatis的Plugin实现基于JDK动态代理机制,在MyBatis初始化过程中,可以为指定的拦截对象生成代理对象,当拦截对象执行某个方法时,代理会先执行插件中的逻辑,再执行原有逻辑。插件可以在原有逻辑前后添加自己的逻辑或者完全替换原有逻辑
如果你使用过spring的话,会自然的想到spring的AOP特性,两者都是利用代理来实现功能的增强
二、Mybatis Plugin 的实例
这是一个旧项目,在后期对接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}) })
三、Mybatis Plugin 原理
1. Mybatis 支持哪些 Plugin
其实从上面,案例看出,我们对插件的设置主要是通过 @Intercepts 内的 @Signature 注解实现的
@Intercepts({ @Signature( type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
其中,type 就是作用的接口,method 和 args 则能确定唯一方法(单用方法名,可能有方法重载的情况)
但是,是不是这些属性可以随便填呢?其实不是的,mybatis没有做的那么自由,其更像Spring中的postProcessor机制,只在固定的几个位置有预留点,让你可以自定义增强,而不是开放所有位置。
这里的Pulgin只针对以下四个接口有增强预留点,它们分别是
- Statementhandler:
用于处理JDBC Statement对象的相关操作,将SQL语句中的占位符进行替换,然后使用Statement对象执行SQL语句 - Resultsethandler:
主要负责将JDBC返回的ResultSet结果集转化为Java对象,然后返还给调用方 - ParameterHandler:
主要用于处理Java对象与JDBC参数的映射,并将其转化为JDBC参数。 - Executor:
更顶层的设计,能对上三种类进行调用,执行SQL语句,并获取执行结果
其具体调用链路如下:
2. myBatis 如何加载 Plugin
即我们自己创建了个 Interceptor 实现类,也使用了 @Intercepts 注解,但这个类是如何被mybatis加载的呢?
2.1 springboot 项目
我们仍以上篇文章的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<Interceptor[]>
ObjectProvider 是在spring 4.3 引入的一种注入方式,它可以检索指定的类型。
然后通过 getIfAvailable 和 getIfUnique 从spring容器中检索出对应对象
因为我们已经在自定义的 ExamplePlugin 上使用了@Component 的注解,所以此处使用自动注入,能获取到我们的插件理所当然。
而后再把该值赋给 sqlSessionFactoryBean, 然后再赋给 mybatis 真正的配置类 Configuration。至此,我们的插件就被 mybatis 系统所成功加载了。
2.2 spring 项目
如果还没有使用上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 框架的效果
3. Plugin 生效原理
上面我们讲了,如何写一个插件,以及插件是怎么交给 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); } }
以上就是 Mybatis plugin 的使用及原理解析的详细内容,更多关于 Mybatis plugin使用原理的资料请关注脚本之家其它相关文章!