前言
本文一把大部分源码罗列出来了,收录至我的GitHub精选文章,欢迎Star:https://github.com/Java-Ling/Java-Interview-guide
最近在研读MyBatis的源码,刚好看到了插件扩展这一块,所以就此分享一下阅读体会以及插件的原理;
概述
可拦截接口
MyBatis允许在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
通过MyBatis提供的强大机制,使用插件是非常简单的,只需实现Interceptor接口,并指定想要拦截的方法签名即可;
由下图,我们也可以推断出,其可拦截的接口有如下4类;
简单示例
新建插件类ExamplePlugin,实现:org.apache.ibatis.plugin.Interceptor接口;
插件将会拦截在Executor实例中所有的update方法调用, 这里的Executor是负责执行底层映射语句的内部对象;
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
//实现你的拦截逻辑
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
上面代码逻辑实现后,要想改拦截器生效,则还需要在全局配置文件中配置,方能使其生效
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<!-- 设置属性,可以在插件中通过properties获取 -->
<property name="someProperty" value="100"/>
</plugin>
</plugins>
原理剖析
拦截顺序:
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
通过上面的示例,我们可以实现一个插件的开发,扩展MyBatis的功能,那么他到底是如何实现增强的呢?接下来,我们瞜一眼源码:
犹记得,我们分享MyBatis初始化的时候,提到过这个方法:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
初始化解析<plugins>节点
org.apache.ibatis.builder.xml.XMLConfigBuilder类中,执行配置文件解析时,pluginElement(XNode)方法执行了配置的<plugins>节点;
//节点数据解析
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));//properties
Properties settings = settingsAsProperties(root.evalNode("settings"));//settings
loadCustomVfs(settings);//虚拟文件系统(VFS),用来读取服务器里的资源
loadCustomLogImpl(settings);//指定 MyBatis 所用日志的具体实现,未指定时将自动查找
typeAliasesElement(root.evalNode("typeAliases"));//实体别名设置
pluginElement(root.evalNode("plugins"));//插件扩展
objectFactoryElement(root.evalNode("objectFactory"));//对象工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));//对象包装工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);//设置
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));//环境
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));//类型处理器
mapperElement(root.evalNode("mappers"));//MappedStatement对象的初始化
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
解析了<plugins>节点数据之后,将其加入了拦截器链中,(此处使用了责任链模式),添加到Configuration对象中的InterceptorChain属性中;
//插件扩展,自定义插件会影响MyBatis底层逻辑,使用时应注意
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
//调用InterceptorChain#addInterceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
————————————————
SqlSession执行器的创建
初始化解析逻辑完成后,我们使用获取到的SqlSessionFactory开启一个SqlSession会话,会话会持有一个Excutor执行器;
当执行到此处是时,org.apache.ibatis.session.Configuration#interceptorChain中已经包含了你所声明的所有插件,由于底层逻辑实现是给需要执行的插件使用JDK动态代理生成一个代理,所以插件执行的顺序刚好和加载顺序相反;比如:插件加载顺序1、2、3,那么执行顺序是3、2、1(责任链模式);
//org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//通过事务工厂来产生一个事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//生成一个执行器(事务包含在执行器里)
final Executor executor = configuration.newExecutor(tx, execType);
//然后产生一个DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
处理拦截逻辑
在创建执行器:final Executor executor = configuration.newExecutor(tx, execType);时执行处理插件逻辑;
//org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
//构建执行器Executor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;//二次保护,防止有人将将defaultExecutorType设成null
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);//批处理的执行器
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);//可重用的执行器
} else {
//简单执行器
//默认SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
//二级缓存开关,settings中cacheEnabled默认为true
if (cacheEnabled) {
//如果需要缓存,生成CachingExecutor(默认有缓存),装饰者模式,所以默认都是返回CachingExecutor
executor = new CachingExecutor(executor);
}
//将该执行器加入到拦截器链中
//植入插件逻辑,至此,四大可拦截对象已全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
executor = (Executor) interceptorChain.pluginAll(executor);植入插件逻辑;
//org.apache.ibatis.plugin.InterceptorChain
//拦截器链
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
//遍历所有的插件,调用插件
for (Interceptor interceptor : interceptors) {
//调用插件的plugin方法
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
调用org.apache.ibatis.plugin.Interceptor#plugin方法生成代理类;
//org.apache.ibatis.plugin.Interceptor
//拦截器,我们所有扩展点插件都必须实现改接口
public interface Interceptor {
//实现具体的拦截逻辑
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
//默认的Plugin.wrap方法,使用JDK动态代理生成代理类,可自定义实现
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
//获取初始化插件时的相应属性
// NOP
}
生成的代理类,其实他的本质还是一个执行器,最终执行query等方法时,会调用代理类的invoke方法;
//org.apache.ibatis.plugin.Plugin
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//JDK动态代理
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 {
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);
}
}
小结
插件的实现使用代理模式、责任链模式;
插件执行逻辑
分页插件PageHelper的使用
官网地址
https://pagehelper.github.io
Maven依赖
SSM项目
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.8</version>
</dependency>
SpringBoot项目
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
配置文件配置
SSM项目MyBatis全局配置文件mybatis-conf.xml配置
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<!--分页插件的注册-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不设置该参数 ,可以自动识别
<property name="dialect" value="mysql"/> -->
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=start;pageSize=limit;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="true"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="check"/>
</plugin>
</plugins>
SpringBoot配置文件application.properties或application.yml配置
# 分页配置
pagehelper.helper-dialect=mysql
pagehelper.reasonable=true
pagehelper.support-methods-arguments: true
pagehelper.params=count=countSql
具体的参数配置,可以参考官网介绍,此处就不再赘述。
使用
分页插件支持以下几种调用方式:
//第一种,RowBounds方式的调用
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectLike(country);
}
});
//lambda
total = PageHelper.count(()->countryMapper.selectLike(country));
结语
本文介绍了MyBatis的插件原理及简单示例,以及分页插件PageHelper的简单介绍及使用,后续我将持续分享Java相关技术栈博文,推荐关注小编。
网友评论