Mybatis自定义插件及原理

为了让开发者更灵活使用mybatis,官方给开发者提供了自定义插件对四大组件:Executor、StatementHandler、ParameterHandler、ResultSetHandler执行提供了方法级别的插拔式拦截。

1.自定义插件示例代码

java 复制代码
package com.xrd.learn;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Intercepts({@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class ExecutorInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("进入Executor拦截代码");
        /**
         * 这里调用了target对象,由于存在多个插件类的情况,所以target对象可能还是插件代理对象,
         * 是的话调用会进行Plugin类的invoke,判断下一个插件类是否执行intercept
         */
        Object proceed = invocation.proceed();
        System.out.println("结束Executor拦截代码");
        return proceed;
    }
}

2.自定义插件步骤

2.1创建一个类实现mybatis plugin包下的Interceptor接口

可以看到,intercept方法是需要自定义插件去实现的,plugin,setProperties是default方法,可以不进行实现。

2.2 将当前类添加进插件配置中,如用xml读取mybatis配置时,插件类的配置格式如下

xml 复制代码
<configuration>
    
<!--以上配置省略-->
    
    <!--配置插件-->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
        </plugin>
        <plugin interceptor="com.xrd.learn.ExecutorInterceptor"></plugin>
<!--        <plugin interceptor="com.xrd.learn.ResultHandlerInterceptor"></plugin>-->
<!--        <plugin interceptor="com.xrd.learn.StatementHandlerInterceptor"></plugin>-->
<!--        <plugin interceptor="com.xrd.learn.ParameterHandlerInterceptor"></plugin>-->
    </plugins>
</configuration>    

2.3 添加类注解@Intercepts

该注解中定义Signature注解数组,数组表示一个自定义的插件,可以同时拦截多个方法。

可以看到,Signature注解中有几个属性

  1. type: 需要拦截的类,即四大类Executor、StatementHandler、ParameterHandler、ResultSetHandler
  2. method: 需要拦截方法名称,支持四大类中存在的方法
  3. args: Class数组,设置拦截方法的参数类型。由于同一类下存在同名不同参数的重载方法,只设置方法名称还不能确定拦截的是哪个方法,将方法参数类型也设置后即可确定具体的拦截方法。

如上示例

java 复制代码
@Intercepts({@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class ExecutorInterceptor implements Interceptor {
.......
}

这里定义了两个Signature,表示当前插件类拦截两个方法。

一个方法是Executor下的query,入参类按顺序为MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class

另一个方法是Executor下的query,入参类按顺序为MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class

关于拦截方法名称跟入参直接看对应组件类即可,如Executor接口,可以拦截这些方法

2.4 实现intercept方法

指定完成需要拦截的方法后,需要在interceptor方法里面实现拦截逻辑

java 复制代码
public class ExecutorInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("进入Executor拦截代码");
        /**
         * 这里调用了target对象,由于存在多个插件类的情况,所以target对象可能还是插件代理对象,
         * 是的话调用会进行Plugin类的invoke,判断下一个插件类是否执行intercept
         */
        Object proceed = invocation.proceed();
        System.out.println("结束Executor拦截代码");
        return proceed;
    }
}

该示例代码执行查询方法时,效果如下

可以看到的确拦截生效了

我们看看invocation对象里面的结构

可以看到有三个属性

  1. target:
    拦截对象,类似代理中的原对象,这里可能是四大组件对象,由于示例拦截的是Executor类,所以案例的target也是Executor的实现类。
  2. method: 被拦截的方法对象
  3. args: 被拦截方法对象的参数

可以看到这里被拦截到的是第一个Signature里定义的query方法

方法的参数也可以通过args看到

小结: 既然能拿到执行过程中的各种参数,也代表我们能通过插件拦截的形式,对mybatis四大组件的流程进行对象修改,步骤增加等操作。

3.插件原理

先给出两张流程图,后续进行详细说明

插件配置读取

插件代理、拦截流程

以Executor为例

Interceptor接口下的另外两个方法先进行说明

plugin

默认方法为调用Plugin.wrap,该方法能够对被拦截对象进行处理,返回。被拦截对象为四大组件对象,具体类即用户在@Signature中所定义的。

看看Plugin方法在哪里被调用到

可以看到InterceptorChain的pluginAll调用到了Interceptor的plugin方法,PluginTest为mybatis提供的单元测试类方便开发进行方法自测,可以忽略。

继续看Interceptor的plugin方法。循环遍历interceptors调用plugin方法。

interceptors又是什么,通过debug发现正是我自定义的插件。所以做出初步总结:

用户配置自定义的mybatis插件后,插件类的plugin方法会被循环调用,用户没实现plugin方法,则默认调用Plugin.wrap

Plugin.wrap又会做什么事情,我们继续看

先做个初步步骤:

  1. 解析当前插件类的signature注解,按照k:被拦截类Class v:被拦截方法set的形式放进map中
  2. 判断signatureMap key中是否有target类,有则进行代理包装,做到执行intercept方法的目的

总结:

如果当前对象被插件拦截,则创建一个代理对象返回,之后mybtis会使用这个代理对象进行各项操作。

接下来详细说明,首先看getSignatureMap

只是单纯将插件类上的Signature从数组转为map形式而已。

再看getAllInterfaces

总结就是看target对象的接口类是否在插件类的signature的type属性中,有的话以数组的形式进行返回。

重点看这一段代码,这段代码可以看明白为什么拦截的逻辑会在插件的intercept方法中执行

如果interfaces存在,则生成一个代理对象进行返回,代理逻辑类为Plugin。

interfaces通过getAllInterfaces方法的解析可以知道,当前target对象在当前插件类的signature的type属性中,即存在。

举个例子,假如当前的target对象为Executor,配置的一个自定义插件如下。

Signature数组中存在type = Executor.class,则interfaces返回Executor类。

java 复制代码
import org.apache.ibatis.cache.CacheKey;  
import org.apache.ibatis.executor.Executor;  
import org.apache.ibatis.mapping.BoundSql;  
import org.apache.ibatis.mapping.MappedStatement;  
import org.apache.ibatis.plugin.*;  
import org.apache.ibatis.session.ResultHandler;  
import org.apache.ibatis.session.RowBounds;  
  
/**  
* @author xurongde  
* @version 1.0  
* @since 2023/11/19 15:37  
*/  
@Intercepts({@Signature(  
type = Executor.class,  
method = "query",  
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}  
), @Signature(  
type = Executor.class,  
method = "query",  
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}  
)})  
public class ExecutorInterceptor implements Interceptor {  
@Override  
public Object intercept(Invocation invocation) throws Throwable {  
System.out.println("进入Executor拦截代码");  
/**  
* 这里调用了target对象,由于存在多个插件类的情况,所以target对象可能还是插件代理对象,  
* 是的话调用会进行Plugin类的invoke,判断下一个插件类是否执行intercept  
*/  
Object proceed = invocation.proceed();  
System.out.println("结束Executor拦截代码");  
return proceed;  
}  
  
  
@Override  
public Object plugin(Object target) {  
return Plugin.wrap(target, this);  
}  
}

回过来看Plugin类的源码,由于是代理的逻辑类,它实现了InvocationHandler接口,那么代理逻辑就是,invoke方法了。

java 复制代码
/**  
* Copyright ${license.git.copyrightYears} the original author or authors.  
*  
* Licensed under the Apache License, Version 2.0 (the "License");  
* you may not use this file except in compliance with the License.  
* You may obtain a copy of the License at  
*  
* http://www.apache.org/licenses/LICENSE-2.0  
*  
* Unless required by applicable law or agreed to in writing, software  
* distributed under the License is distributed on an "AS IS" BASIS,  
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
* See the License for the specific language governing permissions and  
* limitations under the License.  
*/  
package org.apache.ibatis.plugin;  
  
import org.apache.ibatis.reflection.ExceptionUtil;  
import org.apache.ibatis.util.MapUtil;  
  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.lang.reflect.Proxy;  
import java.util.HashMap;  
import java.util.HashSet;  
import java.util.Map;  
import java.util.Set;  
  
/**  
* @author Clinton Begin  
*/  
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) {  
//解析当前插件类的signature注解,按照k:被拦截类Class v:被拦截方法set的形式放进map中  
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);  
Class<?> type = target.getClass();  
//判断signatureMap key中是否有target类,有则进行代理包装,做到执行intercept方法的目的  
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 {  
//获取代理对象当前执行方法所在类,查询signatureMap中的数据,返回的数据为插件需要拦截的方法  
Set<Method> methods = signatureMap.get(method.getDeclaringClass());  
//如果当前方法被插件拦截了  
if (methods != null && methods.contains(method)) {  
//执行插件的intercept方法  
return interceptor.intercept(new Invocation(target, method, args));  
}  
//当前方法没被插件拦截,反射执行原方法  
return method.invoke(target, args);  
} catch (Exception e) {  
throw ExceptionUtil.unwrapThrowable(e);  
}  
}  
  
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数组  
Signature[] sigs = interceptsAnnotation.value();  
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();  
//将Signature数组转为map k:被拦截类Class v:被拦截方法set  
for (Signature sig : sigs) {  
Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());  
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;  
}  
  
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {  
Set<Class<?>> interfaces = new HashSet<>();  
while (type != null) {  
//获取当前对象的接口类,如果接口类在signatureMap中存在,则进行返回  
for (Class<?> c : type.getInterfaces()) {  
if (signatureMap.containsKey(c)) {  
interfaces.add(c);  
}  
}  
type = type.getSuperclass();  
}  
return interfaces.toArray(new Class<?>[0]);  
}  
  
}

重点看下invoke方法

步骤:

  1. mybatis执行流程中,如果调用了四大组件的方法,且四大组件被Plugin类代理了,则会调用到Plugin的invoke方法。这个是java jdk动态代理的知识
  2. 获取代理对象当前执行方法所在类,查询signatureMap中的数据,返回的数据为插件需要拦截的方法
  3. 如果当前方法被插件拦截了,执行插件的intercept方法,没被插件拦截反射执行原方法

再重新看插件代理、拦截的流程图 也就很清晰了。

plugin方法总结: 能对被拦截的四大组件对象进行装饰,代理等操作,返回的对象会被mybatis继续使用。

setProperties

用于在插件中获取一些用户自定义参数使插件的使用更灵活,分页插件pageHelper就有使用到。

pageHelper分页插件源码

java 复制代码
package com.github.pagehelper;  
  
import com.github.pagehelper.cache.Cache;  
import com.github.pagehelper.cache.CacheFactory;  
import com.github.pagehelper.util.MSUtils;  
import com.github.pagehelper.util.StringUtil;  
import org.apache.ibatis.cache.CacheKey;  
import org.apache.ibatis.executor.Executor;  
import org.apache.ibatis.mapping.BoundSql;  
import org.apache.ibatis.mapping.MappedStatement;  
import org.apache.ibatis.plugin.*;  
import org.apache.ibatis.session.Configuration;  
import org.apache.ibatis.session.ResultHandler;  
import org.apache.ibatis.session.RowBounds;  
  
import java.lang.reflect.Field;  
import java.sql.SQLException;  
import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
import java.util.Properties;  
  
/**  
* Mybatis - 通用分页拦截器<br/>  
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper  
*  
* @author liuzh/abel533/isea533  
* @version 5.0.0  
*/  
@SuppressWarnings({"rawtypes", "unchecked"})  
@Intercepts(  
{  
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),  
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),  
}  
)  
public class PageInterceptor implements Interceptor {  


//省略其他代码

@Override  
public void setProperties(Properties properties) {  
//缓存 count ms  
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);  
String dialectClass = properties.getProperty("dialect");  
if (StringUtil.isEmpty(dialectClass)) {  
dialectClass = default_dialect_class;  
}  
try {  
Class<?> aClass = Class.forName(dialectClass);  
dialect = (Dialect) aClass.newInstance();  
} catch (Exception e) {  
throw new PageException(e);  
}  
dialect.setProperties(properties);  
  
String countSuffix = properties.getProperty("countSuffix");  
if (StringUtil.isNotEmpty(countSuffix)) {  
this.countSuffix = countSuffix;  
}  
  
try {  
//反射获取 BoundSql 中的 additionalParameters 属性  
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");  
additionalParametersField.setAccessible(true);  
} catch (NoSuchFieldException e) {  
throw new PageException(e);  
}  
}  
  
}

pageHelper拦截器的setProperties方法

可以看到,pageHelper插件的setProperties中,获取了msCountCache,dialect,countSuffix等参数进行使用,使用者可以对这些参数进行灵活配置。那这些参数又该如何配置呢?

以如下mybatis-config.xml配置为例,这样这几个参数就可以从Propeities中获取了。当然也能通过java代码,yml配置文件等方式进行配置。

xml 复制代码
    <property name="plugins">
        <array>
            <bean class="com.github.pagehelper.PageInterceptor">
                <property name="properties">
                    <value>
                        helperDialect=mysql
                        reasonable=true
                        supportMethodsArguments=true
                        params=count=countSql
                        autoRuntimeDialect=true
                    </value>
                </property>
            </bean>
        </array>
    </property>

4.其他问题

为什么mybatis插件能拦截,且只拦截了四大组件?

通过上述代码反推,我们找到了插件类Interceptor拦截的逻辑是由Plugin.wrap方法实现的

Plugin.wrap方法默认是由Interceptor的plugin方法调用的

Interceptor的plugin方法是由InterceptorChain的pluginAll方法调用的

继续反推,原来InterceptorChain的pluginAll方法被Configuration下这四个方法调用

newExecutor

可以看到这里是创建完executor对象后,再调用插件list存储对应interceptorChain的pluginAll方法,达成插件类对mybatis的Executor对象进行代理的目的,其他三个组件也是相同的流程。

其他三个组件也是自己的对象创建完成后,再去调用pluginAll进行插件类的代理。这也解释了为什么为什么mybatis插件能拦截,且只拦截了四大组件

mybatis是如何扫描到插件类的?

通过上个模块可以看到,mybatis插件类是从interceptorChain中的interceptors中拿到的,我们进行反推,看看interceptors的add方法被谁调用。

可以看到XMLConfigBuilder#pluginElement跟SqlSessionFactoryBean#buildSqlSessionFactory进行了插件的添加。大胆点,推测XMLConfigBuilder是从Xml文件获取插件配置,SqlSessionFactoryBean因为名称以Bean结尾,推测是从spring相关环境中获取到的插件配置。

以XMLConfigBuilder#pluginElement为例继续看

继续反推pluginElement被谁调用,可以看到被XMLConfigBuilder#parseConfiguration所调用。

XMLConfigBuilder#parseConfiguration被XMLConfigBuilder#parse所调用

mybatis xml配置文件与插件相关的配置如下

总结下来就是:

  1. mybatis启动后,读取到的mybatis xml配置文件,会获取configuration标签下的plugins标签
  2. 再拿plugins下interceptor标签的值,即一个全路径类
  3. 将这个类进行实例化,放入interceptorChain中的interceptors用于后续使用。
相关推荐
组合缺一3 分钟前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·后端·spring·solon
程序猿零零漆5 分钟前
SpringCloud 系列教程:微服务的未来(二)Mybatis-Plus的条件构造器、自定义SQL、Service接口基本用法
java·spring cloud·mybatis-plus
猿来入此小猿7 分钟前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码20 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
带多刺的玫瑰21 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
栗豆包36 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
夜半被帅醒43 分钟前
MySQL 数据库优化详解【Java数据库调优】
java·数据库·mysql
万亿少女的梦1681 小时前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端
醒了就刷牙1 小时前
黑马Java面试教程_P9_MySQL
java·mysql·面试
m0_748233641 小时前
SQL数组常用函数记录(Map篇)
java·数据库·sql