挖源码~学习 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:

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

相关推荐
hai405874 分钟前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
陈大爷(有低保)23 分钟前
UDP Socket聊天室(Java)
java·网络协议·udp
kinlon.liu36 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
java6666688881 小时前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存1 小时前
源码分析:LinkedList
java·开发语言
执键行天涯1 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
Jarlen1 小时前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽1 小时前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode