mybatis源码分析-mybatis配置阶段的执行流程分析

mybatis源码分析-mybatis配置阶段的执行流程分析

hello world

看源码到底怎么去看,我总结的是要有目的的去看源码。例如mybatis,我们在github拉下来源码后发现一堆代码,没头没尾无从下手。但是如果我们先写一个Demo,来分析这个demo的每一步到底干了什么,就好分析了。这也就是为啥学习一门新技术都需要先学习怎么使用。

创建一个maven项目,引入mybatis的依赖
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
项目目录结构

在这里插入图片描述

创建一个实体类和对应的mapper接口
/** 实体类 */
public class File {
    private Integer id;
    private Integer parentId;
    private String name;
    private Integer isFolder;
    .... 省略get/set
}

/** mapper接口 */
public interface FileMapper {
    List<File> selectList();
}
mapper.xml

一段很简单的sql语句,就是查询全部的file文件,我已经提前在数据库中插入了几条数据。数据库就不展示了,很简单就这几个字段。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.internal.example.mapper.FileMapper">
    <select id="selectList" resultType="org.mybatis.internal.example.pojo.File">
        select id,parent_id parentId, name, is_folder isFolder from file;
    </select>
</mapper>
在resource文件夹下创建一个myabtis-config.xml全局配置文件
<?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>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </properties>
    <!--    配置别名 可以在mapper.xml中 使用简称就可以不用使用群全名了-->
    <typeAliases>
<!--        指定类设置别名-->
<!--        <typeAlias alias="user" type="org.mybatis.internal.example.pojo.User"/>-->
<!--        <typeAlias alias="str" type="java.lang.String"/>-->
<!--        包扫面式的添加类别名,默认为类名首字母变为小写 如果个别的类需要指定别名可以使用 @Alias("us") 添加在实体类上-->
        <package name="org.mybatis.internal.example.pojo"/>
    </typeAliases>

<!--    多数据源-->
    <environments default="development2">
        <environment id="development">
            <transactionManager type="JDBC"/>
<!--            数据源模式 POOLED
                dataSource的类型可以配置成其内置类型之一,如UNPOOLED、POOLED、JNDI。
              如果将类型设置成UNPOOLED,mybaties会为每一个数据库操作创建一个新的连接,并关闭它。该方式适用于只有小规模数量并发用户的简单应用程序上。
              如果将属性设置成POOLED,mybaties会创建一个数据库连接池,连接池的一个连接将会被用作数据库操作。一旦数据库操作完成,mybaties会将此连接返回给连接池。在开发或测试环境中经常用到此方式。
              如果将类型设置成JNDI。mybaties会从在应用服务器向配置好的JNDI数据源DataSource获取数据库连接。在生产环境中优先考虑这种方式。
-->
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
        <environment id="development2">
            <transactionManager type="JDBC"/>
<!--            数据源模式 POOLED
                dataSource的类型可以配置成其内置类型之一,如UNPOOLED、POOLED、JNDI。
              如果将类型设置成UNPOOLED,mybaties会为每一个数据库操作创建一个新的连接,并关闭它。该方式适用于只有小规模数量并发用户的简单应用程序上。
              如果将属性设置成POOLED,mybaties会创建一个数据库连接池,连接池的一个连接将会被用作数据库操作。一旦数据库操作完成,mybaties会将此连接返回给连接池。在开发或测试环境中经常用到此方式。
              如果将类型设置成JNDI。mybaties会从在应用服务器向配置好的JNDI数据源DataSource获取数据库连接。在生产环境中优先考虑这种方式。
-->
            <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>
    <databaseIdProvider type="DB_VENDOR">
        <property name="SQL Server" value="sqlserver"/>
        <property name="MySQL" value="mysql"/>
        <property name="Oracle" value="oracle" />
    </databaseIdProvider>
<!--    第一类是使用package自动搜索的模式,这样指定package下所有接口都会被注册为mapper,-->
<!--    <mappers>-->
<!--        <mapper resource="mapper\UserMapper.xml"/>-->
<!--        <mapper resource="mapper\FileMapper.xml"/>-->
<!--    </mappers>-->
    <!-- 将包内的映射器接口实现全部注册为映射器 -->
    <mappers>
        <package name="org.mybatis.internal.example.mapper"/>
    </mappers>
<!--   另外一类是明确指定mapper,这又可以通过resource、url或者class进行细分。例如:-->
<!--    <mappers>-->
<!--        <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>-->
<!--        <mapper class="org.mybatis.builder.AuthorMapper"/>-->
<!--        <mapper url="file:///var/mappers/PostMapper.xml"/>-->
<!--    </mappers>-->
</configuration>
现在创建main方法继续测试
public class MybatisHelloWorld {
    // 配置阶段
        // 得到mybatis 配置类的文件路径
        String resource = "mybatis/Configuration.xml";
        Reader reader = null;
        SqlSession session = null;
        try{
            Yaml yml = new Yaml();
            LinkedHashMap map = yml.loadAs(MybatisHelloWorld.class.getClassLoader().getResourceAsStream("config.yml"), LinkedHashMap.class);
            System.out.println(map);
            Map dataSourceMap = (Map)map.get("dataSource");
            System.out.println(dataSourceMap.get("username"));
            // 通过Resources.getResourceAsReader 得到配置文件的字节流
            reader = Resources.getResourceAsReader(resource);
            // 这里的Properties 和配置文件Configuration.xml 里面的一致,使用Java配置可以实现动态的properties
            Properties properties = new Properties();
//            properties.setProperty("username", dataSourceMap.get("username") + "");

            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties);
            // 执行SQL阶段
            // 得到一个session连接
            session = sqlSessionFactory.openSession();
            FileMapper fileMaper = session.getMapper(FileMapper.class);
            List<File> fileList = fileMaper.selectList();
        	list.forEach(System.out::println);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            session.commit();
            session.close();
        }
       
}

源码分析阶段

这段代码中核心的代码只有四句代码,这四句我把整个mybatis 从配置到执行sql语句得到想要的结果 分为两个阶段:

  • 配置阶段
  • 执行阶段
// 配置阶段
// 创建sqlsession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties);
// 得到一个session连接
session = sqlSessionFactory.openSession();

// 执行阶段
// 获得mapper代理类
FileMapper fileMaper = session.getMapper(FileMapper.class);
// 执行查询方法
List<File> fileList = fileMaper.selectList();

我们一个一个的分析每个阶段的源码,首先是配置阶段,这个阶段我们首先要提出问题。

  • SqlSessionFactory得到build需要将xml配置文件,他拿到这配置文件做了什么?怎么做的?
// 创建sqlsession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties);
// 得到一个session连接
session = sqlSessionFactory.openSession();

我们点击 build(reader,properties) 方法里面看一看他到底做了什么,点击去我们发现他最终调用了这个build方法,在这个方法里面创建了一个XMLConfigBuilder对象

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
//      解析配置文件的关键逻辑都委托给XMLConfigBuilder
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

解析配置文件的关键逻辑都委托给XMLConfigBuilder,我们进入这个类一探究竟。这个类中最重要的三个方法都在这里

  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

  /**
   * 外部调用此方法对mybatis配置文件进行解析
   * 第四步 真正Configuration构建逻辑就在XMLConfigBuilder.parse()里面
   */
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //mybatis配置文件解析的主流程
    //从根节点configuration
    // 返回根节点 parser.evalNode("/configuration")
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  //此方法就是解析configuration节点下的子节点
  //由此也可看出,我们在configuration下面能配置的节点为以下11个节点
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      // 这里是按照官网的配置文件顺序进行解析的 https://mybatis.org/mybatis-3/zh/configuration.html
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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);
    }
  }

我们发现在parseConfiguration()方法中对xml配置文件进行了解析,此方法就是解析configuration节点下的子节点,解析的顺序是按照配置文件的配置顺序依次进行的解析。其中最重要得到的解析,这里就是对mapper的解析处理,我们进去方法mapperElement看一看是怎么实现的。

/**
 *  加载mapper文件mapperElement
 *  mapper文件是mybatis框架的核心之处,所有的用户sql语句都编写在mapper文件中,所以理解mapper文件对于所有的开发人员来说都是必备的要求
 * @param parent
 * @throws Exception
 */
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 如果要同时使用package自动扫描和通过mapper明确指定要加载的mapper,一定要确保package自动扫描的范围不包含明确指定的mapper,
      // 否则在通过package扫描的interface的时候,尝试加载对应xml文件的loadXmlResource()的逻辑中出现判重出错,报org.apache.ibatis.binding.BindingException异常,
      // 即使xml文件中包含的内容和mapper接口中包含的语句不重复也会出错,包括加载mapper接口时自动加载的xml mapper也一样会出错。
      if ("package".equals(child.getName())) {
        //  <package name="org.mybatis.internal.example.mapper"/>
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          //  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // mapperParser.parse()方法就是XMLMapperBuilder对Mapper映射器文件进行解析
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          //  <mapper url="file:///var/mappers/PostMapper.xml"/>
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            // 解析映射配置文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          //  <mapper class="org.mybatis.builder.AuthorMapper"/>
          // 反射加载对象
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

这段代码长一点,我们把它分一下类,从代码上我们可以看出,他按照child.getName()的不同分别进行了处理

一共四种情况,也就是mybatis.xml配置文件四种不同标签加载mapper的方法。虽然分为了四种情况但是他最终执行的代码是相同的,我们只看package包扫描的方式,因为这种方式用的比较多。

/**
 * <mappers>
 * 	<package name="org.mybatis.internal.example.mapper"/>
 * </mappers>
 */
if ("package".equals(child.getName())) {
  // 包名:org.mybatis.internal.example.mapper
  String mapperPackage = child.getStringAttribute("name");
  configuration.addMappers(mapperPackage);
}
/**
 * <mappers>
 * 	<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
 * </mappers>
 */
if (resource != null && url == null && mapperClass == null) {
	...
}

/**
 * <mappers>
 * 	<mapper class="org.mybatis.builder.AuthorMapper"/>
 * </mappers>
 */
if (resource == null && url == null && mapperClass != null) {
	...
}

/**
 * <mappers>
 * 	<mapper url="file:///var/mappers/PostMapper.xml"/>
 * </mappers>
 */
if (resource == null && url != null && mapperClass == null) {
	...
}

我们看一下包扫描这种方法到底干了什么?

点击这个方法 configuration.addMappers(“mapper接口的包名”),我就康康不乱动!(●ˇ∀ˇ●)

点击config的addMappers方法发现又是老样子,他又交给了别人处理了

mapperRegistry.addMappers(packageName);

config是调用了mapperRegistry的addMappers方法,好吧!我进入的深一点,我再跟进去。

  public void addMappers(String packageName, Class<?> superType) {
    // mybatis框架提供的搜索classpath下指定package以及子package中符合条件(注解或者继承于某个类/接口)的类,默认使用Thread.currentThread().getContextClassLoader()返回的加载器,和spring的工具类殊途同归。
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 无条件的加载所有的类,因为调用方传递了Object.class作为父类,这也给以后的指定mapper接口预留了余地
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 所有匹配的calss都被存储在ResolverUtil.matches字段中
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      //调用addMapper方法进行具体的mapper类/接口解析
      addMapper(mapperClass);
    }
  }

当我进入这个方法的时候发现,哎,这里的代码多肯定是这里没错了。我们看看这里做了什么操作。

首先得到了一个包搜索工具 resolverUtil

通过这个工具拿到了这个包下所有的mapper接口的class对象mapperSet,是一个set集合,然后遍历集合,他又调用了 addMappers方法,这次传递的参数是每个mapper接口的class对象。没办法再进去看看

 public <T> void addMapper(Class<T> type) {
    // 对于mybatis mapper接口文件,必须是interface,不能是class,因为mybatis用的是jdk动态代理
    if (type.isInterface()) {
      // 判重,确保只会加载一次不会被覆盖
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 生成一个MapperProxyFactory,用于之后生成动态代理类
        // 为mapper接口创建一个MapperProxyFactory代理工厂,将mapper接口与工厂建立关系,当使用这个mapper的代理类时再去生产一个代理类
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        // MapperAnnotationBuilder进行具体的解析
        //以下代码片段用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件的情况
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          //剔除解析出现异常的接口
          knownMappers.remove(type);
        }
      }
    }
  }

这片代码写了啥,一堆的判断… 嗯就一两句我们想看到的


knownMappers.put(type, new MapperProxyFactory<>(type));

MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        
parser.parse();

第一个 让我们终于知道了,我们那些mapper接口到底去哪里了,原来在这个他为每个mapper接口创建了一一对应的MapperProxyFactory代理工厂,并将这些工厂存放到了 map对象中。

  • key: mapperClass

  • value:MapperProxyFactory<>(mapperClass)

第二个 用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件的情况。这里不在深入,谁想看自己去看。

最后还执行了一句解析 parser.parse(),这个解析到底在解析什么,我们在~~~~点击去看一下。

这里东西有点多,看一下的我的源码片段,这里我都进行了注释。

 /**
   * MapperBuilderAssistant初始化完成之后,就调用build.parse()进行具体的mapper接口文件加载与解析
   */
  public void parse() {
    String resource = type.toString();
    //首先根据mapper接口的字符串表示判断是否已经加载,避免重复加载,正常情况下应该都没有加载
    if (!configuration.isResourceLoaded(resource)) {
      //⭐⭐⭐⭐⭐ 加载Mapper.xml资源,这里面就是解析mapper.xml的方法
      loadXmlResource();
      configuration.addLoadedResource(resource);
      // 命名空间 每个mapper文件自成一个namespace,通常自动匹配就是这么来的,约定俗成代替人工设置最简化常见的开发
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          parseResultMap(method);
        }
        try {
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

loadXmlResource这个方法将mapper.xml进行了解析,具体怎么解析自己看。

 private void loadXmlResource() {
    // 判断资源是否已经加载过
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      // 得到xml的相对路径 例如:com.xx.mapper.UserMapper.xml 注意这是指在resources 问价夹下的资源目录
      // type值得是mapper接口文件的信息
      // type.getName() = com.xx.mapper.UserMapper
      // 他会解析成 com/xx/mapper/UserMapper.xml
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // 加载xml 的文件流
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
      if (inputStream == null) {
        try {
          // ClassLoader.getSystemClassLoader(); 通过类加载器加载
          inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
        } catch (IOException e2) {
        }
      }
      if (inputStream != null) {
        // 调用XMLMapperBuilder 解析mapper.xml文件
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }

到现在我们知道了mybatis配置阶段到底执行了那东西。

    // ClassLoader.getSystemClassLoader(); 通过类加载器加载
      inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
    } catch (IOException e2) {
    }
  }
  if (inputStream != null) {
    // 调用XMLMapperBuilder 解析mapper.xml文件
    XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
    xmlParser.parse();
  }
}

}


到现在我们知道了mybatis配置阶段到底执行了那东西。

接下来就是sql执行阶段了,欲知后事如何,且听下次讲解
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码神附体

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

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

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

打赏作者

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

抵扣说明:

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

余额充值