为了让开发者更灵活使用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注解中有几个属性
- type: 需要拦截的类,即四大类Executor、StatementHandler、ParameterHandler、ResultSetHandler
- method: 需要拦截方法名称,支持四大类中存在的方法
- 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对象里面的结构
可以看到有三个属性
- target:
拦截对象,类似代理中的原对象,这里可能是四大组件对象,由于示例拦截的是Executor类,所以案例的target也是Executor的实现类。 - method: 被拦截的方法对象
- 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又会做什么事情,我们继续看
先做个初步步骤:
- 解析当前插件类的signature注解,按照k:被拦截类Class v:被拦截方法set的形式放进map中
- 判断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方法
步骤:
- mybatis执行流程中,如果调用了四大组件的方法,且四大组件被Plugin类代理了,则会调用到Plugin的invoke方法。这个是java jdk动态代理的知识。
- 获取代理对象当前执行方法所在类,查询signatureMap中的数据,返回的数据为插件需要拦截的方法
- 如果当前方法被插件拦截了,执行插件的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配置文件与插件相关的配置如下
总结下来就是:
- mybatis启动后,读取到的mybatis xml配置文件,会获取configuration标签下的plugins标签
- 再拿plugins下interceptor标签的值,即一个全路径类
- 将这个类进行实例化,放入interceptorChain中的interceptors用于后续使用。