通过分析拦截器的基本原理和配置类的读取,最终在拦截器中实现覆盖配置类。
1. 拦截器的简介
Mybatis将拦截器定义为插件,在执行过程中的某一点进行拦截调用,在这一点上加上自己的代码逻辑。
默认情况下,Mybatis拦截的方法调用包括
- Executor (update, query, flushStatements, commit, rollback,
getTransaction, close, isClosed)
——执行器,负责增删改查以及事务的提交和回滚,默认使用 SimpleExecutor - ParameterHandler (getParameterObject, setParameters)
——参数处理器,负责读取和存储参数 - ResultSetHandler (handleResultSets, handleOutputParameters)
——负责封装结果集 - StatementHandler (prepare, parameterize, batch, update, query)
——负责操作statement对象对数据库执行:声明SQL语句->向SQL语句传入参数->执行SQL语句->使用ResultSetHandler对参数映射再封装结果
根据官方文档的介绍,这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
2. 配置类的读取
Mybatis所有的配置信息都存放在Configuration中,而Mybatis对Configuration配置类数据的封装大致分为以下阶段:
- 第一步读取配置文件中的信息,将其封装为XNode类,原理为:mybatis采用DOM解析的方式一次性把整个xml文档加载进内存,然后在内存中构建了一颗Document的对象树,通过Document对象,得到树上的节点对象,而这个节点对象就是org.w3c.dom.Node对象,通过节点对象访问到xml文档中的内容,而mybatis为了方便处理Node对象,将其封装成XNode对象。
为了方便阅读,我将Mybatis中读取配置文件的操作代码抽取出来单独展示
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(Resources.getResourceAsStream("Mybatis-Config.xml")));
XPathParser xPathParser = new XPathParser(document);
XNode xNode = xPathParser.evalNode("/configuration");
System.out.println(xNode);
//XNode类在toString方法中循环遍历子节点进行拼接
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
——打印结果如下
<configuration>
<properties resource="JDBCUtils.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="mapper"/>
</mappers>
</configuration>
最终得到的为XNode节点对象,不过通过结果可以发现此时enviroment标签下的datasource标签中的property标签,其value值仍然是字符串连接符
- 第二步将XNode节点对象中的取出含properties的子节点对象进行读取,最后封装到Configuration配置类中
通过调用org.apache.ibatis.builder.xml.XMLConfigBuilder对象的parseConfiguration(XNode root)方法对XNode对象进行读取加工然后封装到Configuration对象中。当它检测到properties节点对象后,通过一系列循环取值,其实最终走的还是java.util.Properties类中的getProperty方法,一个常规根据key读取properties文件中对应value的方法,将它封装成一个properties对象返回,最后将这个对象存入Configuration配置类中,此时datasource中的properties才算赋值完成。此时我在properties配置文件中并没有为password赋值。
3.如何覆盖配置类
根据Mybatis官方文档中的提示: 除了用插件(拦截器)来修改 MyBatis
核心行为以外,还可以通过完全覆盖配置类来达到目的。只需继承配置类后覆盖其中的某个方法,再把它传递到
SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会极大影响 MyBatis 的行为,务请慎之又慎。
- 通过对配置类读取的分析可以发现,在XNode对象将信息读取到Configure配置类的过程中,对其修改信息是很困难。而根据官方文档的提示,可以在Mybatis分析执行SQL的之前对其拦截然后对Configuration进行覆盖,而这一步对应拦截那个类型呢?正是Executor执行器——通过拦截Executor,在执行之前对其配置进行覆盖。那需要拦截的参数对应的类呢?这个类既要满足有Configuration对象对于全局的配置,也需要有sql语句和类型相关的封装,更需要有缓存等字段…等等信息的包装,因为这样它才可以完整的去执行这条sql语句。而Mybatis中的这个类正是org.apache.ibatis.mapping.MappedStatement
public final class MappedStatement {
private String resource; //当前MappedStatement对象是由那个XML映射文件解析出来的
private Configuration configuration; //全局配置文件
private String id; //sql语句的id
private Integer fetchSize; //ResultSet.next()获取取数据时客户端缓存行数大小
private Integer timeout; //超时时长
private StatementType statementType; //操作SQL的对象类型,STATEMENT(直接赋值)|PREPARED(预处理)|CALLABLE(执行存储过程)
private ResultSetType resultSetType; //ResultSet结果集的类型
private SqlSource sqlSource;
private Cache cache; //二级缓存
private ParameterMap parameterMap; //请求参数类型
private List<ResultMap> resultMaps; //返回结果类型
private boolean flushCacheRequired; //是否刷新缓存
private boolean useCache; //是否开启二级缓存
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator; //生成主键
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog; //内部日志
private LanguageDriver lang;
private String[] resultSets;
...
}
-
拦截的类型和拦截的参数确认之后,剩下的就是拦截方法,对于拦截方法的配置,大致分为两类,“query"和"update”,简单的说,query就是负责拦截SELECE查询的方法,update负责就是拦截INSERTE/DELETE/UPDATE,增加/删除/修改的方法,UPDATE更新操作本质就是先删除后添加。
-
代码实现
第一步:导入mybatis相关依赖
第二步:在Mybatis的配置文件中配置拦截器
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<setting name="useGeneratedKeys" value="true"/>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 配置拦截器 -->
<plugins>
<plugin interceptor="com.hnjd.Interceptor.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
<environments default = "development">
<environment id = "development">
<transactionManager type = "JDBC" />
<dataSource type = "POOLED">
<property name = "driver" value="${jdbc.driver}" />
<property name = "url" value="${jdbc.url}" />
<property name = "username" value="${jdbc.username}" />
<property name = "password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.hnjd.mapper"/>
</mappers>
</configuration>
第三步:实现拦截器
package com.hnjd.Interceptor;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import javax.sql.DataSource;
import java.lang.reflect.Constructor;
import java.util.Properties;
@Intercepts({
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class})
})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object object = invocation.getArgs()[0];//获取拦截的MappedStatement对象
MappedStatement mappedStatement = (MappedStatement) object; // 向下转型,引用类型的强制转换改变的只是引用的类型,而object因为是超类,所以可以引用任何类型的对象,由于拦截器把MappedStatement转型为Object 这里重新向下转型为原对象 是安全可行的。
DataSource dataSource = mappedStatement.getConfiguration().getEnvironment().getDataSource();
PooledDataSource pooledDataSource = (PooledDataSource) dataSource; //Configuration配置种指定的是POOLED连接池
pooledDataSource.setPassword("root"); //成功覆盖配置类
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
public void setProperties(Properties properties) {
}
}
——补充,MappedStatement不仅只封装了Configuration配置类,还有SQL
语句以及数据库连接相关的配置等等,这样我们不仅可以在拦截器中覆盖配置类,还能编写日志,以及实现分页等等功能。
4.后言
该文章基于实验角度出发,限于博主目前知识面有限,对于拦截器原理还只是浅尝辄止。初次编写博客,也是对自己最近学习Mybatis源码的总结和心得,您的建议和支持是对博主最好的支持,谢谢阅读。