mybatis源码学习

一、背景

之前在项目中一直使用得是hibernate,对于mybatis一直没有使用过,最近项目负荷不高,终于抽出时间来学习下mybatis。
mybatis的背景知识就不介绍,基础用法在本文也不做过多赘述,不清楚的可以去阅读:https://mybatis.org/mybatis-3/zh_CN/configuration.html

二、SqlSessionFactory的加载过程

SqlSessionFactory是mybatis的一个核心类,它是创建SqlSession的工厂类。而SqlSession是操作数据库的核心类。本文将以一个简单的例子说明SqlSessionFactory的加载过程。
在这里插入图片描述
本例中的mybatis配置文件是org/mybatis/example/mybatis-config.xml,SqlSessionFactory初始化的过程实际上就是读取并组织mybatis-config.xml的配置信息的过程。
在这里插入图片描述
解析mybatis-config.xml的核心类是XMLConfigBuilder

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      //读取外挂的配置文件
      propertiesElement(root.evalNode("properties"));
      
      //读取settings标签,并将值注入到Properties中
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);

     //设置日志
      loadCustomLogImpl(settings);
     
      //设置 type别名,注入到configuration.typeAliasRegistry中
      typeAliasesElement(root.evalNode("typeAliases"));
     
      // 加载插件  注入到configuration.interceptorChain
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // 解析environments 生成TransactionFactory、dataSource  ,注入到configuration.environment
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      
      //解析typehandlers,并且将类型处理器注入到TypeHandlerRegistry中
      typeHandlerElement(root.evalNode("typeHandlers"));
     
      //处理mappers
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

通过读取这段源码,可以发现mybatis读取mybatis-config.xml最终时间所有的配置信息解析后存放到Configuration类中。Configuration类就像一个容器。

三、mapper信息的加载

mybatis中的mapper有两种,一种是Mapper.class,一种是配置文件Mapper.xml。两种方式各有优劣。本章节将分别讲述这两种方式的加载。

3.1 Mapper.xml的解析

一个典型的Mapper.xml如下图所示。MyBlogMapper.xml中记录中 数据库字段和java类型的映射关系,以及相关的sql语句。
在这里插入图片描述
在这里插入图片描述
当需要操作数据库时,通过 namespace+sqlId定位到语句,然后执行。
在这里插入图片描述
Mapper.xml的解析过程是:
在这里插入图片描述

  • 新建一个XMLMapperBuilder对象,然后去解析
    在这里插入图片描述
  • 逐个去解析Mapper.xml中的各个元素。最后将sql语句解析注入到Configuration.mappedStatements中。
    在这里插入图片描述

3.2 Mapper.class的解析

在这里插入图片描述

  • mapperClass的解析过程是直接调用Configuration.addMapper()方法
    在这里插入图片描述
  • 调用MapperRegistry的addMapper方法
    在这里插入图片描述
  • MapperRegistry中维护了一个knownMappers的HashMap存放MapperClass和其代理对象工厂(MapperProxyFactory)的映射。MapperProxyFactory实际上是为每个MapperClass生成MapperProxy代理对象。
  • 生成一个MapperAnnotationBuilder对象,来解析MapperClass
    在这里插入图片描述
  • 循环解析MapperClass的方法,并将sql操作封装为Statement,注册到Configuration.mappedStatements中。
    在这里插入图片描述

四、操作数据

4.1 MapperClass接口方法调用在这里插入图片描述

MapperClass接口方法的调用实际上是Mybatis为每个mapperClass生成了一个动态代理对象MapperProxy。对数据库的的操作实际上实在MapperProxy中完成,时序图如上图所示。
MapperProxy.invoke() 方法是代理对象执行的入口,其中会拦截所有非 Object 方法,针对每个被拦截的方法,都会调用 cachedInvoker() 方法获取对应的 MapperMethod 对象,并调用其 invoke() 方法执行代理逻辑以及目标方法。

在 cachedInvoker() 方法中,首先会查询 methodCache 缓存,如果查询的方法为 default 方法,则会根据当前使用的 JDK 版本,获取对应的 MethodHandle 并封装成 DefaultMethodInvoker 对象写入缓存;如果查询的方法是非 default 方法,则创建 PlainMethodInvoker 对象写入缓存。

cachedInvoker() 方法的具体实现如下:

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    // 尝试从methodCache缓存中查询方法对应的MapperMethodInvoker
    MapperMethodInvoker invoker = methodCache.get(method);
    if (invoker != null) {
        return invoker;
    }
    // 如果方法在缓存中没有对应的MapperMethodInvoker,则进行创建
    return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) { // 针对default方法的处理
            // 这里根据JDK版本的不同,获取方法对应的MethodHandle的方式也有所不同
            // 在JDK 8中使用的是lookupConstructor字段,而在JDK 9中使用的是
            // privateLookupInMethod字段。获取到MethodHandle之后,会使用
            // DefaultMethodInvoker进行封装
            if (privateLookupInMethod == null) {
                return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
                return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
        } else {
            // 对于其他方法,会创建MapperMethod并使用PlainMethodInvoker封装
            return new PlainMethodInvoker(
                    new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
    });
}

其中使用到的 DefaultMethodInvoker 和 PlainMethodInvoker 都是 MapperMethodInvoker 接口的实现,如下图所示:

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    // 首先将MethodHandle绑定到一个实例对象上,然后调用invokeWithArguments()方法执行目标方法
    return methodHandle.bindTo(proxy).invokeWithArguments(args);
}

在 PlainMethodInvoker.invoke() 方法中,会通过底层维护的 MapperMethod 完成方法调用,其核心实现如下:

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    // 直接执行MapperMethod.execute()方法完成方法调用
    return mapperMethod.execute(sqlSession, args);
}
  • MapperMethod
    通过对 MapperProxy 的分析我们知道,MapperMethod 是最终执行 SQL 语句的地方,同时也记录了 Mapper 接口中的对应方法,其核心字段也围绕这两方面的内容展开。

  • SqlCommand
    MapperMethod 的第一个核心字段是 command(SqlCommand 类型),其中维护了关联 SQL 语句的相关信息。在 MapperMethod$SqlCommand 这个内部类中,通过 name 字段记录了关联 SQL 语句的唯一标识,通过 type 字段(SqlCommandType 类型)维护了 SQL 语句的操作类型,这里 SQL 语句的操作类型分为 INSERT、UPDATE、DELETE、SELECT 和 FLUSH 五种。

下面我们就来看看 SqlCommand 如何查找 Mapper 接口中一个方法对应的 SQL 语句的信息,该逻辑在 SqlCommand 的构造方法中实现,如下:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 获取Mapper接口中对应的方法名称
    final String methodName = method.getName();
    // 获取Mapper接口的类型
    final Class<?> declaringClass = method.getDeclaringClass();
    // 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识,
    // 到Configuration这个全局配置对象中查找SQL语句
    // MappedStatement对象就是Mapper.xml配置文件中一条SQL语句解析之后得到的对象
    MappedStatement ms = resolveMappedStatement(mapperInterface, 
            methodName, declaringClass, configuration);
    if (ms == null) { 
        // 针对@Flush注解的处理
        if (method.getAnnotation(Flush.class) != null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else { // 没有@Flush注解,会抛出异常
            throw new BindingException("...");
        }
    } else {
        // 记录SQL语句唯一标识
        name = ms.getId();
        // 记录SQL语句的操作类型
        type = ms.getSqlCommandType();
    }
}

这里调用的 resolveMappedStatement() 方法不仅会尝试根据 SQL 语句的唯一标识从 Configuration 全局配置对象中查找关联的 MappedStatement 对象,还会尝试顺着 Mapper 接口的继承树进行查找,直至查找成功为止。具体实现如下

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                                   Class<?> declaringClass, Configuration configuration) {
        // 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识
        String statementId = mapperInterface.getName() + "." + methodName;
        // 检测Configuration中是否包含相应的MappedStatement对象
        if (configuration.hasStatement(statementId)) {
            return configuration.getMappedStatement(statementId);
        } else if (mapperInterface.equals(declaringClass)) {
            // 如果方法就定义在当前接口中,则证明没有对应的SQL语句,返回null
            return null;
        }
        // 如果当前检查的Mapper接口(mapperInterface)中不是定义该方法的接口(declaringClass),
        // 则会从mapperInterface开始,沿着继承关系向上查找递归每个接口,
        // 查找该方法对应的MappedStatement对象
        for (Class<?> superInterface : mapperInterface.getInterfaces()) {
            if (declaringClass.isAssignableFrom(superInterface)) {
                MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                        declaringClass, configuration);
                if (ms != null) {
                    return ms;
                }
            }
        }
        return null;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值