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用于后续使用。
相关推荐
吾日三省吾码38 分钟前
JVM 性能调优
java
弗拉唐2 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi772 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀3 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20203 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深3 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong3 小时前
slice介绍slice查看器
java·ubuntu
牧竹子3 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar