通过Mybatis拦截器实现覆盖配置类

通过分析拦截器的基本原理和配置类的读取,最终在拦截器中实现覆盖配置类。

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配置类中
    通过Mybatis拦截器实现覆盖配置类_第1张图片
    通过调用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
语句以及数据库连接相关的配置等等,这样我们不仅可以在拦截器中覆盖配置类,还能编写日志,以及实现分页等等功能。通过Mybatis拦截器实现覆盖配置类_第2张图片

4.后言

该文章基于实验角度出发,限于博主目前知识面有限,对于拦截器原理还只是浅尝辄止。初次编写博客,也是对自己最近学习Mybatis源码的总结和心得,您的建议和支持是对博主最好的支持,谢谢阅读。

你可能感兴趣的:(mybatis,java,mybatis)