面试:你知道MyBatis执行过程之初始化是如何执行的吗?

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 在了解MyBatis架构以及核心内容分析后,我们可以研究MyBatis执行过程,

前言


在了解MyBatis架构以及核心内容分析后,我们可以研究MyBatis执行过程,包括


  • MyBatis初始化
  • SQL执行过程


而且在面试会问到一下关于MyBatis初始化的问题,比如:


  • Mybatis需要初始化哪些?
  • MyBatis初始化的过程?


MyBatis初始化


在 MyBatis 初始化过程中,会加载 mybatis-config.xml 配置文件、Mapper.xml映射配置文件以及 Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到 Configuration 对象中。初始化过程可以分成三部分:


  • 解析mybatis-config.xml 配置文件


  • SqlSessionFactoryBuilder
  • XMLConfigBuilder
  • Configuration


  • 解析Mapper.xml映射配置文件


  • XMLMapperBuilder::parse()
  • XMLStatementBuilder::parseStatementNode()
  • XMLLanguageDriver
  • SqlSource
  • MappedStatement


  • 解析Mapper接口中的注解


  • MapperRegistry
  • MapperAnnotationBuilder::parse()


解析mybatis-config.xml 配置文件


MyBatis 的初始化流程的入口SqlSessionFactoryBuilder::build(Reader reader, String environment, Properties properties) 方法,看看具体流程图:


25.png


public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
复制代码


首先会使用XMLConfigBuilder::parser()解析mybatis-config.xml 配置文件,


  • 先解析标签configuration内的数据封装成XNodeconfiguration 也是 MyBatis 中最重要的一个标签
  • 根据XNode解析mybatis-config.xml 配置文件的各个标签转变为各个对象


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);
    }
  }
复制代码


再基于Configuration使用SqlSessionFactoryBuilder::build()生成DefaultSqlSessionFactory供给后续执行使用。


解析Mapper.xml映射配置文件


首先使用XMLMapperBuilder::parse()解析Mapper.xml,看看加载流程图来分析分析


26.png


通过XPathParser::evalNodemapper标签中内容解析到XNode


public void parse() {
    if (!this.configuration.isResourceLoaded(this.resource)) {
        this.configurationElement(this.parser.evalNode("/mapper"));
        this.configuration.addLoadedResource(this.resource);
        this.bindMapperForNamespace();
    }
    this.parsePendingResultMaps();
    this.parsePendingCacheRefs();
    this.parsePendingStatements();
}
复制代码


再由configurationElement()方法去解析XNode中的各个标签:


  • namespace
  • parameterMap
  • resultMap
  • select|insert|update|delete


private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析MapperState
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
复制代码


其中,基于XMLMapperBuilder::buildStatementFromContext(),遍历 <select /><insert /><update /><delete /> 节点们,逐个创建 XMLStatementBuilder对象,执行解析,通过XMLStatementBuilder::parseStatementNode()解析,


  • parameterType
  • resultType
  • selectKey


并会通过LanguageDriver::createSqlSource()(默认XmlLanguageDriver)解析动态sql生成SqlSource(详细内容请看下个小节),


  • 使用GenericTokenParser::parser()负责将 SQL 语句中的 #{} 替换成相应的 ? 占位符,并获取该 ? 占位符对应的


而且通过MapperBuilderAssistant::addMappedStatement()生成MappedStatement


public void parseStatementNode() {
  //获得 id 属性,编号
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");
  // 判断 databaseId 是否匹配
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }
  //解析获得各种属性
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);
  String resultMap = context.getStringAttribute("resultMap");
  String resultType = context.getStringAttribute("resultType");
  String lang = context.getStringAttribute("lang");
  //获得 lang 对应的 LanguageDriver 对象
  LanguageDriver langDriver = getLanguageDriver(lang);
  //获得 resultType 对应的类
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  //获得 statementType 对应的枚举值
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  //获得 resultSet 对应的枚举值
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  String nodeName = context.getNode().getNodeName();
  //获得 SQL 对应的 SqlCommandType 枚举值
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  //解析获得各种属性
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
  //创建 XMLIncludeTransformer 对象,并替换 <include /> 标签相关的内容
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());
  //解析 <selectKey /> 标签
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  //创建 SqlSource生成动态sql
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }
  //创建 MappedStatement 对象
  this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,          fetchSize, timeout, parameterMap, parameterTypeClass, 
                    resultMap, resultTypeClass, resultSetTypeEnum, flushCache, 
                    useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, 
                    keyColumn, databaseId, langDriver, resultSets);
}
复制代码


解析Mapper接口中的注解


当执行完XMLMapperBuilder::configurationElement()方法后,会调用XMLMapperBuilder::bindMapperForNamespace()会转换成对接口上注解进行扫描,具体通过MapperRegistry::addMapper()调用MapperAnnotationBuilder实现的


27.png


MapperAnnotationBuilder::parse()是注解构造器,负责解析 Mapper 接口上的注解,解析时需要注意避免和 XMLMapperBuilder::parse() 方法冲突,重复解析,最终使用parseStatement解析,那怎么操作?


public void parse() {
  String resource = type.toString();
  //判断当前 Mapper 接口是否应加载过。
  if (!configuration.isResourceLoaded(resource)) {
    //加载对应的 XML Mapper,注意避免和 `XMLMapperBuilder::parse()` 方法冲突
    loadXmlResource();
    //标记该 Mapper 接口已经加载过
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    //解析 @CacheNamespace 注解
    parseCache();
    parseCacheRef();
     //遍历每个方法,解析其上的注解
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        if (!method.isBridge()) {
          //执行解析
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  //解析待定的方法
  parsePendingMethods();
}
复制代码


那其中最重要的parseStatement()是怎么操作?其实跟解析Mapper.xml类型主要处理流程类似:


  • 通过加载LanguageDriver,GenericTokenParser等为生成SqlSource动态sql作准备
  • 使用MapperBuilderAssistant::addMappedStatement()生成注解@mapper,@CacheNamespace等的MappedStatement信息


void parseStatement(Method method) {
    //获取接口参数类型  
    Class<?> parameterTypeClass = getParameterType(method);
    //加载语言处理器,默认XmlLanguageDriver
    LanguageDriver languageDriver = getLanguageDriver(method);
    //根据LanguageDriver,GenericTokenParser生成动态SQL
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      //获取其他属性
        Options options = method.getAnnotation(Options.class);
        final String mappedStatementId = type.getName() + "." + method.getName();
        Integer fetchSize = null;
        Integer timeout = null;
        StatementType statementType = StatementType.PREPARED;
        ResultSetType resultSetType = null;
        SqlCommandType sqlCommandType = getSqlCommandType(method);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = !isSelect;
        boolean useCache = isSelect;
        //获得 KeyGenerator 对象
        KeyGenerator keyGenerator;
        String keyProperty = null;
        String keyColumn = null;
        if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { // 有
            // first check for SelectKey annotation - that overrides everything else
            //如果有 @SelectKey 注解,则进行处理
            SelectKey selectKey = method.getAnnotation(SelectKey.class);
            if (selectKey != null) {
                keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
                keyProperty = selectKey.keyProperty();
            //如果无 @Options 注解,则根据全局配置处理
            } else if (options == null) {
                keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
            // 如果有 @Options 注解,则使用该注解的配置处理
            } else {
                keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
                keyProperty = options.keyProperty();
                keyColumn = options.keyColumn();
            }
        // 无
        } else {
            keyGenerator = NoKeyGenerator.INSTANCE;
        }
        //初始化各种属性
        if (options != null) {
            if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
                flushCache = true;
            } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
                flushCache = false;
            }
            useCache = options.useCache();
            fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
            timeout = options.timeout() > -1 ? options.timeout() : null;
            statementType = options.statementType();
            resultSetType = options.resultSetType();
        }
        // 获得 resultMapId 编号字符串
        String resultMapId = null;
        //如果有 @ResultMap 注解,使用该注解为 resultMapId 属性
        ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
        if (resultMapAnnotation != null) {
            String[] resultMaps = resultMapAnnotation.value();
            StringBuilder sb = new StringBuilder();
            for (String resultMap : resultMaps) {
                if (sb.length() > 0) {
                    sb.append(",");
                }
                sb.append(resultMap);
            }
            resultMapId = sb.toString();
        // 如果无 @ResultMap 注解,解析其它注解,作为 resultMapId 属性
        } else if (isSelect) {
            resultMapId = parseResultMap(method);
        }
      //构建 MappedStatement 对象
      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }
复制代码


生成动态SqlSource


当在执行langDriver::createSqlSource(configuration, context, parameterTypeClass)中的时候, 是怎样从 Mapper XML 或方法注解上读取SQL内容生成动态SqlSource的呢?现在来一探究竟,


28.png


首先需要获取langDriver实现XMLLanguageDriver/RawLanguageDriver,现在使用默认的XMLLanguageDriver::createSqlSource(configuration, context, parameterTypeClass)开启创建,再使用XMLScriptBuilder::parseScriptNode()解析生成SqlSource


  • DynamicSqlSource: 动态的 SqlSource 实现类 , 适用于使用了 OGNL 表达式,或者使用了 ${} 表达式的 SQL
  • RawSqlSource原始SqlSource 实现类 , 适用于仅使用 #{} 表达式,或者不使用任何表达式的情况


public SqlSource parseScriptNode() {
        MixedSqlNode rootSqlNode = this.parseDynamicTags(this.context);
        Object sqlSource;
        if (this.isDynamic) {
            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
        }
        return (SqlSource)sqlSource;
    }
复制代码


那就选择其中一种来分析一下RawSqlSource,怎么完成构造的呢?看看RawSqlSource构造函数:


public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
        this.sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
    }
复制代码


使用SqlSourceBuilder::parse()去解析SQl,里面又什么神奇的地方呢?


public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        SqlSourceBuilder.ParameterMappingTokenHandler handler = new SqlSourceBuilder.ParameterMappingTokenHandler(this.configuration, parameterType, additionalParameters);
      //创建基于#{}的GenericTokenParser
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(this.configuration, sql, handler.getParameterMappings());
    }
复制代码


ParameterMappingTokenHandlerSqlSourceBuilder 的内部私有静态类, ParameterMappingTokenHandler ,负责将匹配到的 #{} 对,替换成相应的 ? 占位符,并获取该 ? 占位符对应的 org.apache.ibatis.mapping.ParameterMapping 对象。


并基于ParameterMappingTokenHandler使用GenericTokenParser::parse()将SQL中的#{}转化占位符? 占位符后创建一个StaticSqlSource返回。


总结


在 MyBatis 初始化过程中,会加载 mybatis-config.xml 配置文件、Mapper.xml映射配置文件以及 Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并全部保存到 Configuration 对象中,并创建DefaultSqlSessionFactory供SQl执行过程创建出顶层接口SqlSession供给用户进行操作。


各位看官还可以吗?喜欢的话,动动手指点个赞💗呗!!谢谢支持!



目录
相关文章
|
1月前
|
SQL 数据库
SQL面试50题------(初始化工作、建立表格)
这篇文章提供了SQL面试中可能会遇到的50道题目的建表和初始化数据的SQL脚本,包括学生、教师、课程和成绩表的创建及数据插入示例。
SQL面试50题------(初始化工作、建立表格)
|
4月前
|
缓存 Java 数据库连接
Mybatis缓存相关面试题有多卷
使用 MyBatis 缓存机制需要注意以下几点: 对于频繁更新和变动的数据,不适合使用缓存。 对于数据的一致性要求比较高的场景,不适合使用缓存。 如果配置了二级缓存,需要确保缓存的数据不会影响到其他业务模块的数据。 在使用缓存时,需要注意缓存的命中率和缓存的过期策略,避免缓存过期导致查询性能下降。
89 0
|
6天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
3月前
|
SQL 缓存 Java
MyBatis最经典的20道面试题,你都会了吗?
MyBatis最经典的20道面试题,你都会了吗?
43 0
|
1月前
|
前端开发 Java 数据库连接
一天十道Java面试题----第五天(spring的事务传播机制------>mybatis的优缺点)
这篇文章总结了Java面试中的十个问题,包括Spring事务传播机制、Spring事务失效条件、Bean自动装配方式、Spring、Spring MVC和Spring Boot的区别、Spring MVC的工作流程和主要组件、Spring Boot的自动配置原理和Starter概念、嵌入式服务器的使用原因,以及MyBatis的优缺点。
|
2月前
|
SQL 缓存 Java
【面试官】Mybatis缓存有什么问题吗?
面试官:你说下对MyBatis的理解?面试官:那SqlSession知道吧?面试官:Mybatis的缓存有哪几种?面试官:那Mybatis缓存有什么问题吗?面试官:Mybatis分页插件是怎么
【面试官】Mybatis缓存有什么问题吗?
|
2月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
98 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
SQL 缓存 Java
Mybatis面试题
Mybatis面试题
|
2月前
|
前端开发 Java 编译器
Java面试题:描述Java类的加载过程,包括加载、链接、初始化等阶段。
Java面试题:描述Java类的加载过程,包括加载、链接、初始化等阶段。
25 0
|
2月前
|
SQL Java 数据库连接
Java面试题:简述ORM框架(如Hibernate、MyBatis)的工作原理及其优缺点。
Java面试题:简述ORM框架(如Hibernate、MyBatis)的工作原理及其优缺点。
47 0