挖源码~学习 MyBatis 插件运行原理

1.介绍

MyBatis 会话的运行需要 ParameterHandlerResultSetHandlerStatementHandlerExecutor 这四大对象的配合。MyBatis 插件相当于拦截器,可以编写针对以上四种对象的插件,在对象调度的时候,插入一些我们自己的代码。

  • ParameterHandler:对 SQL 参数进行处理
  • ResultSetHandler:对结果对象进行处理
  • StatementHandler:对 SQL 语句进行处理
  • Executor :执行器

看到这里,是不是觉得有 AOP 那味了。

实际上,插件就是基于面向切面编程的思想,通过动态代理来实现。

  • 为需要拦截的接口生成代理对象以实现接口方法拦截功能
  • 每当执行这四大对象的方法时,先判断执行的方法是否需要代理的方法;如果是,则执行插件类的增强方法,进行方法的拦截处理

2.源码解析

1.SpringBoot 通过自动配置,加载 MybatisAutoConfiguration,在该类实例化时,会将 Interceptor 注入到属性 interceptors

2.在 MybatisAutoConfiguration 中通过 @Bean 定义了 SqlSessionFactory;调用该方法实例化 SqlSessionFactory

  • 实例化 SqlSessionFactoryBean,并将各类配置信息注入该对象

    • configLocation:定义了配置文件的位置
    • Configuration 对象:就像是 MyBatis 的总管,MyBatis 的所有配置信息都存放在这里,此外,它还提供了设置这些配置信息的方法
    • configurationProperties
    • 数组 interceptors :插件 Interceptor
    • databaseIdProvider 对象
    • typeAliasesPackage
    • typeAliasesSuperType
    • typeHandlersPackage
    • 数组 typeHandlers:类型处理器集合
    • mapperLocations**Mapper.xml 映射文件位置
  • 调用 getObject() 方法进行实例化

    • 在该方法中,判断 sqlSessionFactory 是否为空,是则调用 afterPropertiesSet() 方法

    • 调用 afterPropertiesSet() 方法时,会调用 buildSqlSessionFactory() 方法

      • buildSqlSessionFactory() 方法中,进行一系列实例化过程
      • 重要的是在这里将 Interceptor 插件注入 Configuration 配置类中
      • 同时设置环境信息 Environment;对 **Mapper.xml 文件进行解析
    • 返回 SqlSessionFactory 对象

2.通过 SqlSessionFactory 创建 SqlSession

java 复制代码
// 创建 SqlSession;核心代码:DefaultSqlSessionFactory$openSessionFromDataSource
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建执行器
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
java 复制代码
// 创建 Executor;核心代码:Configuration$newExecutor
// 这个是实现针对 Executor 的插件的核心代码,稍后详解
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;

3.通过执行器 Executor 执行相应的方法(查询调用 query(),增删改调用 update()

  • 通过 Configuration$newStatementHandler() 创建 StatementHandler

    java 复制代码
    // SimpleExecutor$doQuery()
    StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
  • Configuration$newStatementHandler() 会实例化 RoutingStatementHandler

    java 复制代码
    // SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler 都继承了 BaseStatementHandler;调用 BaseStatementHandler 的构造函数实例化
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        switch(ms.getStatementType()) {
            case STATEMENT:
                this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case PREPARED:
                this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case CALLABLE:
                this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            default:
                throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
        }
    }
    ​
    // 在 BaseStatementHandler 中实例化了 ParameterHandler 和 ResultSetHandler
    this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql);
  • 通过 ParameterHandler 对参数进行处理
  • 通过 StatementHandler 执行 SQL 语句
  • 通过 ResultSetHandlder 对执行结果进行处理

执行流程:

DefaultSqlSession$selectOne() ---> DefaultSqlSession$selectList() ---> BaseExecutor$query() ---> BaseExecutor$queryFromDatabase() ---> SimpleExecutor$doQuery()

实现插件重点源码:

以上的四大对象都是通过 Configuration 对象的四个方法来创建的,源码如下所示:

从源码中可以观察到,每个创建的对象都需要被 this.interceptorChain.pluginAll() 进行处理。

interceptorChain 实际上是 InterceptorChain 对象,在该对象中有一个 List 集合,里面保存了定义的插件(Interceptor)。pluginAll() 方法中,会遍历每一个插件,并调用其 plugin(target) 方法。

Interceptor 实际上调用了 Pluginwrap() 方法。

Plugin.wrap() 方法判断是否需要生成代理。

java 复制代码
public static Object wrap(Object target, Interceptor interceptor) {
    // 解析在 Interceptor 上修饰的 @Intercepts 的注解信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 获取目标对象的 Class
    Class<?> type = target.getClass();
    // 匹配;目标对象所实现的接口中在 signatureMap 中的将其返回
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 判断;如果数组中存在元素,则说明存在接口被 Interceptor 拦截,然后通过 JDK 动态代理实现代理类
    return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}

当这个代理类执行方法时,会被 Plugin(实际是 InvocationHandler) 中的 invoke() 方法拦截。当执行的方法是需要被拦截的方法时,执行对应的 Interceptor 的拦截方法。

java 复制代码
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 根据目标对象的 Class,获取需要被拦截的方法
        Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
        // 当需要被拦截的方法不为空,并且目前调用的方法是需要被拦截的方法时,执行 Interceptor 的 intercept() 方法;在这里就会执行我们自定义的插件的拦截方法
        return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
    } catch (Exception var5) {
        throw ExceptionUtil.unwrapThrowable(var5);
    }
}

参考资料

Tips:

目前还在学习成长中!存在任何问题,欢迎在评论区批评指正。

相关推荐
计算机学姐3 分钟前
基于Asp.net的驾校管理系统
vue.js·后端·mysql·sqlserver·c#·asp.net·.netcore
欢乐少年19042 小时前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
夏天的味道٥3 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
冰糖码奇朵4 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
好教员好4 小时前
【Spring】整合【SpringMVC】
java·spring
浪九天6 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
堕落年代6 小时前
Maven匹配机制和仓库库设置
java·maven
功德+n6 小时前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
uhakadotcom6 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github
uhakadotcom6 小时前
CVE-2025-25012:Kibana 原型污染漏洞解析与防护
后端·面试·github