MyBatis 插件原理分析 & 自定义插件

        在之前笔记,我们有介绍到了 MyBatis 的基本使用。接下来我们来介绍一下 MyBatis 中为我们提供的高级功能 ---- MyBatis 插件机制。凡是使用过 MyBatis 的你,肯定都用到过这个 PageHelper 分页插件,如果你对 PageHelper 还不了解,你可以点击链接进去了解一下哈,很简单的。

        此处说到了 MyBatis 的插件,那么我们就从原理来对 MyBatis 的插件机制进行了解。MyBatis 插件原理:是通过 JDK 动态代理来实现的。

1.JDK 动态代理

       代理模式,作为一种常用的设计模式如果你对代理模式还不太了解,请跳转博主【Java设计模式-----代理模式】进行了解,此处不作介绍。

2.MyBatis 插件使用步骤

       此处以 PageHelper 插件来介绍。你也可以进入 PageHelper官网 来了解,此处做一些简单的介绍。

2.1 Maven 添加 PageHelper 依赖
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>
2.2 配置拦截器插件

        在 mybatis-config.xml 核心配置文件中进行注册。

<plugins>
	<plugin interceptor="com.github.pagehelper.PageInterceptor">
		<!-- 一些参数配置 -->
	</plugin>
</plugins>
2.3 分页插件的使用

通过 PageHelper.startPage() 方法,设置 page 和 pageSize,即可使用。(此处未整合 Spring)

@Test
public void testSelect() throws Exception {
    //配置文件所在路径
    String resource = "mybatis/mybatis-config.xml";
    //读取MyBatis配置文件,转换成InputStream
    InputStream readerStream = Resources.getResourceAsStream(resource);
    //创建一个SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(readerStream);
    //创建一个SqlSession会话
    SqlSession sqlSession = sqlSessionFactory.openSession();

    try {
        //使用 PageHelper 进行物理分页
        PageHelper.startPage(2, 2);
        //使用getMapper()方法,执行操作。MyBatis 定义了一个 getMapper() 方法,用来解决框架初期的硬编码问题,
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = userMapper.selectAllUser();
        for (User user : users) {
            System.out.println(user);
        }
    } finally {
        sqlSession.close();
    }
}

3.MyBatis 插件源码分析

      【此处占位 MyBatis笔记(三):MyBatis 源码分析】

3.1 第一步

       我们对 MyBatis 源码分析,了解到在创建 sqlSession 会话的过程中,会对 mybatis-config.xml 核心配置中的所有信息进行解析,并将结果存储在一个 Configuration 对象中。

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //对配置信息进行解析	
    parseConfiguration(parser.evalNode("/configuration"));
    //返回 Configuration 对象
    return configuration;
}
//所有标签解析过程
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      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"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}
3.2 第二步

       此处来分析一下<plugins>的解析过程,我们来进入到 pluginElement() 来进行了解。

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
      	//1.此处会读取到核心配置文件中配置的interceptor属性对应的类(如 2.2 中)
        String interceptor = child.getStringAttribute("interceptor");
        //2.获取到<plugin>下配置的参数信息(2.2中未配置参数)
        Properties properties = child.getChildrenAsProperties();
        //3.将读取到的类,强转成一个 Interceptor(因为MyBatis插件都必须实现预留的 Interceptor 接口,此处强转不会出错)
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //4.对属性进行设置
        interceptorInstance.setProperties(properties);
        //5.最后,将所有配置都存储到 Configuration 这个类中
        configuration.addInterceptor(interceptorInstance);
        //6.继续对 addInterceptor() 方法进行分析(继续向下走)
      }
    }
}

addInterceptor()

public void addInterceptor(Interceptor interceptor) {
	//接上:会将所有的组件类名,放置到叫做 interceptorChain 的容器中。
	//(此处你记住有这个 interceptorChain 就行了,它是一个 List 类型)
	interceptorChain.addInterceptor(interceptor);
}
3.3 第三步

       前2步,已经成功创建 SqlSessionFactory接下来我们就使用 sqlSessionFactory.openSession()来创建一个 sqlSession 会话。了解一下 插件在创建 session 会话的过程中,又做了哪些操作。

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);
      	//1.在 openSession()阶段,有创建一个 Executor 执行器
      	final Executor executor = configuration.newExecutor(tx, execType);
      	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();
    }
}

newExecutor()

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      	executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      	executor = new ReuseExecutor(this, transaction);
    } else {
      	executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      	executor = new CachingExecutor(executor);
    }
    //2.在此处,它通过 pluginAll()方法来加载我们放在 interceptorChain 中的所有组件(步骤3.2中)
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

pluginAll()

public Object pluginAll(Object target) {
	//3.此处通过遍历的形式,拿到所有实现了 Interceptor 接口的拦截器(即:MyBatis组件)
    for (Interceptor interceptor : interceptors) {
    	//4.此处,我们以 PageInterceptor 为例
      	target = interceptor.plugin(target);
    }
    return target;
}

在这里插入图片描述
plugin() & wrap() 方法

public Object plugin(Object target) {
	//5.此处 Plugin 类,是 MyBatis 为我们提供的一个类。
	//方便我们通过 Plugin.wrap() 的方式来创建一个代理类。接下来进入 wrap()方法分析
	return Plugin.wrap(target, this);
}

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) {
    	//6.此处便是 JDK 动态代理的一段代码。到此处你便了解到了 MyBatis 组件的精髓
    	//它的原理:使用的就是 JDK动态代理机制
      	return Proxy.newProxyInstance(
          	type.getClassLoader(),
          	interfaces,
          	//7.Plugin 类便是被代理对象的委托类,之后想要调用被代理类的方法时,都会委托给这个类的invoke()方法
          	//Plugin()类有实现InvocationHandler哦。继续【第四步】分析向下看
          	new Plugin(target, interceptor, signatureMap));
    }
    return target;
}
3.4 第四步

        既然是通过 JDK 动态代理 的方式实现的,接下来就是执行代理类 Plugin 类中的invoke() 方法,继续向下分析。Go Go Go → →

//1.Plugin 类已实现 InvocationHandler 方法
public class Plugin implements InvocationHandler {

  	private Object target;
  	private Interceptor interceptor;
  	private Map<Class<?>, Set<Method>> signatureMap;

	/**
	 * 省略部分代码,只留了 invoke() 方法
	 */

  	@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)) {
      		//此处,便会调用我们自定义的intercept()方法(此处以 PageHelper 分析)
      		//预留:此处 new Invocation(target, method, args) 类的分析
        	return interceptor.intercept(new Invocation(target, method, args));
      	}
      	return method.invoke(target, args);
    } catch (Exception e) {
      	throw ExceptionUtil.unwrapThrowable(e);
    }
}

在这里插入图片描述

3.5 第五步

       现在终于走到了 PageHelper 实现的拦截器,完成对 SQL 部分的操作。PageHelper 是我国的以为大神编写,向大神看起 Go Go Go

       正常逻辑下:代理模式,仅仅是对方法的增强,在方法前后做一些操作,最终还是会执行到被代理类的原方法中。那么这个被代理类的原方法在哪呢??

        在 PageHelper 中的 intercept 方法中,我们也并没有找到那个被代理类的原方法在哪。因为 PageHelper 中它的实现方式和我们日常中使用到的有点不同。已经走到了 PageHelper 的具体增强实现,此处不再过多分析。注意一下这个:invocation.getTarget();就行了我们继续向下走【第六步】

/**
 * 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 {
    //缓存count查询的ms
    protected Cache<CacheKey, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            //注意此处!!!!!!!!
            //此处 invocation.getTarget() 可以来获取被代理对象的一些方法等
            //这个传过来的 invocation 对象,我们继续向下走【第六步】开始分析
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if(args.length == 4){
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //反射获取动态参数
                Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //创建 count 查询的缓存 key
                    CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
                    countKey.update(MSUtils.COUNT);
                    MappedStatement countMs = msCountMap.get(countKey);
                    if (countMs == null) {
                        //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                        countMs = MSUtils.newCountMappedStatement(ms);
                        msCountMap.put(countKey, countMs);
                    }
                    //调用方言获取 count sql
                    String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
                    countKey.update(countSql);
                    BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
                    //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
                    for (String key : additionalParameters.keySet()) {
                        countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //执行 count 查询
                    Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
                    Long count = (Long) ((List) countResultList).get(0);
                    //处理查询总数
                    //返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                //判断是否需要进行分页查询
                if (dialect.beforePage(ms, parameter, rowBounds)) {
                    //生成分页的缓存 key
                    CacheKey pageKey = cacheKey;
                    //处理参数对象
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    //调用方言获取分页 sql
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
                    //设置动态参数
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //执行分页查询
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
                } else {
                    //不执行分页的情况下,也不执行内存分页
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                }
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }
3.6 第六步

        在 【3.4 第四步】中,我们还有预留一个 new Invocation(target, method, args)类未做分析。这个 Invocation()类就是传入到 PageHelper 的 增强方法 intercept () 中的参数。这个参数是啥呢?我们来了解一下

/**
 * @author Clinton Begin
 */
public class Invocation {

  	private Object target;
  	private Method method;
 	private Object[] args;

  	public Invocation(Object target, Method method, Object[] args) {
    	this.target = target;
    	this.method = method;
    	this.args = args;
  	}

  	public Object getTarget() {
    	return target;
  	}

  	public Method getMethod() {
    	return method;
  	}

  	public Object[] getArgs() {
    	return args;
  	}

  	public Object proceed() throws InvocationTargetException, IllegalAccessException {
  		//重点:
  		//此处,通过反射机制来调用被代理对象的原方法
    	return method.invoke(target, args);
  	}
}

        在该类中,我们发现一个 proceed方法。在该方法中,你看到什么重点东西了???method.invoke()方法,这可是被代理类要执行的方法啊。

        你还没看懂?那进入【JDK 动态代理】里面看看这个,你就了解了,如下图。
在这里插入图片描述
       既然已经在自定义插件的 intrcept(Invocation invocation) 方法中传入了 这个参数,我们便能够通过invocation.proceed() 方法,直接调用被代理对象的原操作了。

至此,MyBatis插件源码分析结束

4.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 四大对象,均为接口。

  • Executor:是 SqlSession 会话中的一个实例,用来执行对数据库的增删改查的操作,一个比较顶层的对象。
  • ParameterHandler: SQL 语句组装的过程,在此过程拦截会用到 ParameterHandler 对象;
  • ResultSetHandler:是处理结果集的过程。
  • StatementHandler:是处理SQL语句预编译的过程

5.自定义MyBatis插件

        我们来自定义一个 MyBatis 插件,用来统计每个 SQL 执行的时间。(可以用来统计慢 SQL 的执行,方便 SQL 的优化)

5.1 编写 SqlTimeInterceptor 插件类

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Statement;
import java.util.Properties;

/**
 * 自定义的 MyBatis 组件类
 * 1.用来获取执行的SQL语句
 * 2.用来获取 SQL 执行时长
 */
@Intercepts(
        @Signature(type = StatementHandler.class, method = "query",args = {Statement.class, ResultHandler.class})
)
public class SqlTimeInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //开始时间
        long startTime = System.currentTimeMillis();

        //通过 invocation.getTarget() 可以获取被代理对象的一些方法等
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        System.out.println("获取SQL语句:" + sql);

        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            long time = endTime - startTime;
            System.out.println("SQL执行时间为:" + time + "ms");
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //获取<plugins> 标签下参数配置
        String prop = properties.getProperty("testProp");
        System.out.println("获取到测试属性值:" + prop);
    }
}

5.2 编写 SqlTimeInterceptor 插件类

        将自定义的 MyBatis 组件,注册到 mybatis-config.xml 核心配置文件中,你也可以在该插件中设置插件中需要使用到的一些参数。

<plugins>
    <plugin interceptor="com.springboot.plugins.SqlTimeInterceptor">
		<property name="testProp" value="我是属性值"/>
	</plugin>
</plugins>

5.3 代码中,不需要任何配置

        PageHelper 中,需要配置PageHelper.startPage(2, 2);,我们编写的这个,因为没有用到参数,可以不用在代码中作任何操作,便能够1.记录SQL 2.统计SQL执行时长。我们使用本文 2.3 中代码 进行测试。将 PageHelper 插件 和我们编写的插件一起使用,测试结果如下所示。
在这里插入图片描述

18:38:12.172 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl' adapter.
获取到测试属性值:我是属性值
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Created connection 1210005556.
Returned connection 1210005556 to pool.
18:38:13.365 [main] DEBUG SQL_CACHE - Cache Hit Ratio [SQL_CACHE]: 0.0
Cache Hit Ratio [com.springboot.dao.UserMapper]: 0.0
Opening JDBC Connection
Checked out connection 1210005556 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@481f3834]
==>  Preparing: SELECT count(0) FROM db_user 
获取SQL语句:SELECT count(0) FROM db_user
==> Parameters: 
<==    Columns: count(0)
<==        Row: 4
<==      Total: 1
SQL执行时间为:18ms
Cache Hit Ratio [com.springboot.dao.UserMapper]: 0.0
==>  Preparing: select * from db_user LIMIT 2,2 
获取SQL语句:select * from db_user LIMIT 2,2
==> Parameters: 
<==    Columns: id, username, age, address
<==        Row: 3, James, 31, 天津市南开区
<==        Row: 4, Clark, 27, 重庆市渝中区
<==      Total: 2
SQL执行时间为:3ms
User{id=3, username='James', age=31, address='天津市南开区'}
User{id=4, username='Clark', age=27, address='重庆市渝中区'}

博主写作不易,来个关注呗

求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙

博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

扛麻袋的少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值