2023-06-06  阅读(5)
原文作者:惑边 原文地址:https://blog.csdn.net/my_momo_csdn

核心流程-初始化阶段

一、核心流程-初始化阶段

  • Mybatis的核心流程三大阶段是:初始化–>动态代理–>数据读写阶段,本文主要分析初始化阶段。初始化阶段主要是完成XML配置文件和注解配置信息的读取,创建全局单例的Configuration配置对象,完成各部分的初始化工作,具体的创建过程需要三个核心类来完成解析。

二、核心类

2.1 创建Configuration的三个核心类

  • 创建Configuration的核心类和作用如下
类名 作用
XmlConfigBuilder 加载解析主配置文件
XmlMapperBuilder 加载解析mapper映射文件中非SQL节点部分
XmlStatementBuilder 加载解析mapper映射文件中SQL节点部分
  • 这三个类都比较复杂,使用到了建造者模式,关于建造者模式可以参考:01-创建型模式(上)
  • 上面三个类共同完成全局配置文件的加载解析,来构建Configuration配置对象。这三个类看起来并没有使用到建造者模式的流式风格,但是借鉴了建造者模式的思想,在CacheBuilder里面的build方法是典型的建造者模式加载核心配置文件来创建全局的配置对象。

2.2 其他核心类

类名 作用
Configuration 单例对象,存在于程序的整个生命周期,包含所有的配置信息(初始化核心就是创建该对象)
MapperRegistry mapper接口动态代理的注册中心,注册的就是Java接口
MapperProxy 动态代理类,实现了InvocationHandler接口,用于为接口生成动态代理实例,实例就是MapperProxy类型的。
MapperProxyFactory 用于生成MapperProxy实例
ResultMap 解析映射配置文件中的resultMap节点,内部包含一个ResultMapping类型的List,里面就封装了id,result等子元素
MappedStatement 存储映射文件中的insert
SqlSource 映射文件中的SQL语句会解析成SqlSource对象,解析sqlSource后得到的sql语句只包含占位符,可以直接交给DB执行

三、Configuration源码解析

3.1 Configuration属性

    /**
     * @author Clinton Begin
     * Mybatis的核心配置文件
     * 1.包含Mybatis全部的配置信息
     * 2.全局单例的
     * 3.生命周期贯穿整个Mybatis的生命周期,应用级生命周期
     */
    public class Configuration {
    
        //环境信息
        protected Environment environment;
        //是否启用行内嵌套语句
        protected boolean safeRowBoundsEnabled = false;
        protected boolean safeResultHandlerEnabled = true;
        //是否启用下划线转驼峰命名的属性
        protected boolean mapUnderscoreToCamelCase = false;
        //延迟加载
        protected boolean aggressiveLazyLoading = true;
        //是否允许单条sql 返回多个数据集  (取决于驱动的兼容性) default:true 
        protected boolean multipleResultSetsEnabled = true;
        //允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。  default:false
        protected boolean useGeneratedKeys = false;
        protected boolean useColumnLabel = true;
        //是否开启2级缓存
        protected boolean cacheEnabled = true;
        protected boolean callSettersOnNulls = false;
        protected boolean useActualParamName = true;
    
        //日志打印所有的前缀 
        protected String logPrefix;
        protected Class<? extends Log> logImpl;
        protected Class<? extends VFS> vfsImpl;
        protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
        protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
        //设置触发延迟加载的方法
        protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[]{"equals", "clone", "hashCode", "toString"}));
        protected Integer defaultStatementTimeout;
        protected Integer defaultFetchSize;
        //执行类型,有simple、resue及batch
        protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
        //指定 MyBatis 应如何自动映射列到字段或属性
        protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
        protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
    
        protected Properties variables = new Properties();
        protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
        //MyBatis每次创建结果对象的新实例时,它都会使用对象工厂(ObjectFactory)去构建POJO
        protected ObjectFactory objectFactory = new DefaultObjectFactory();
        protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
    
        //延迟加载的全局开关
        protected boolean lazyLoadingEnabled = false;
        //指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具
        protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
    
        protected String databaseId;
        /**
         * Configuration factory class.
         * Used to create Configuration for loading deserialized unread properties.
         *
         * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
         */
        //插件集合
        protected Class<?> configurationFactory;
    
        //mapper接口的动态代理注册中心
        protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
        protected final InterceptorChain interceptorChain = new InterceptorChain();
        //TypeHandler注册中心
        protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
        //TypeAlias别名注册中心
        protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
        protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
    
        //mapper文件中增删改查操作的注册中心
        protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
        //mapper文件中配置cache节点的 二级缓存
        protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
        //mapper文件中配置的所有resultMap对象  key为命名空间+ID
        protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
        protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
        //mapper文件中配置KeyGenerator的insert和update节点,key为命名空间+ID
        protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
    
        //加载到的所有*mapper.xml文件
        protected final Set<String> loadedResources = new HashSet<String>();
        //mapper文件中配置的sql元素,key为命名空间+ID
        protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
    
        protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
        protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
        protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
        protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();
    
    
        public Configuration(Environment environment) {
            this();
            this.environment = environment;
        }
    
        //注册默认的别名
        public Configuration() {
            typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
            typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    
            typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
            typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
            typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    
            typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
            typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
            typeAliasRegistry.registerAlias("LRU", LruCache.class);
            typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
            typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    
            typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
    
            typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
            typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    
            typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
            typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
            typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
            typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
            typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
            typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
            typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
    
            typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
            typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
    
            languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
            languageRegistry.register(RawLanguageDriver.class);
        }
    
        //相关属性的get/set方法,和其他方法,省略
    }

四、解析对应图

202306062341067921.png

  • 上图展示了配置文件和Configuration对象之间的对应关系,每一个配置都会映射到Configuration对象内部的属性。

五、初始化分析

5.1 XMLConfigBuilder

  • 示例如下,初始化过程在代码的角度看,只有SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) 这一句代码,我们从这一句代码分析其背后初始化所走过的流程。
        public void test() throws IOException {
            String resource = "mybatis/mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 1.读取mybatis配置文件创SqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 2.从SqlSessionFactory获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession();
            // 3.获取对应mapper
            PlayerDao mapper = sqlSession.getMapper(PlayerDao.class);
            // 4.执行查询语句并返回结果
            Player player = mapper.findPlayerById(1);
            System.out.println(player);
            inputStream.close();
        }

5.1.1 入口方法build

  • SqlSessionFactoryBuilder#build(java.io.InputStream) // new SqlSessionFactoryBuilder().build(inputStream) ,最后调用的方法是:SqlSessionFactoryBuilder#buildbuild(InputStream inputStream, String environment, Properties properties),
    //XMLConfigBuilder#parse方法是配置解析的主要方法
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          //1.构建XMLConfigBuilder对象
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          //2.建造者模式,XMLConfigBuilder对象的parse方法就可以构造Configuration对象,屏蔽了所有实现细节,并且将返回的Configuration对象作为参数构
          //造SqlSessionFactory对象(SqlSessionFactory的默认实现类DefaultSqlSessionFactory),DefaultSqlSessionFactory拿到了配置对象之后,就具备生产SqlSession的能力了
          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.
          }
        }
      }
    
      //DefaultSqlSessionFactory是SqlSessionFactory的默认实现
      public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
      }

5.1.2 XMLConfigBuilder#parse

  • XMLConfigBuilder#parse方法是配置解析的核心方法
    public Configuration parse() {
        //1.判断是否已经解析过,不重复解析
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        //2.读取主配置文件的configuration节点下面的配置信息,parseConfiguration方法完成解析的流程
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }

5.1.3 XMLConfigBuilder#parseConfiguration

  • XMLConfigBuilder#parseConfiguration方法完成全部的配置解析主流程
    /**
       * 解析核心配置文件的关键方法,
       * 读取节点的信息,并通过对应的方法去解析配置,解析到的配置全部会放在configuration里面
       * */
    private void parseConfiguration(XNode root) {
        try {
          Properties settings = settingsAsPropertiess(root.evalNode("settings"));
          //issue #117 read properties first
          //解析<properties>节点
          propertiesElement(root.evalNode("properties"));
          //解析<settings>节点
          loadCustomVfs(settings);
          //解析<typeAliases>节点
          typeAliasesElement(root.evalNode("typeAliases"));
          //解析<plugins>节点
          pluginElement(root.evalNode("plugins"));
          //解析<objectFactory>节点
          objectFactoryElement(root.evalNode("objectFactory"));
          //解析<objectWrapperFactory>节点
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          //解析<reflectorFactory>节点
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          //将settings填充到configuration
          settingsElement(settings);
          // read it after objectFactory and objectWrapperFactory issue #631
          //解析<environments>节点
          environmentsElement(root.evalNode("environments"));
          //解析<databaseIdProvider>节点
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          //解析<typeHandlers>节点
          typeHandlerElement(root.evalNode("typeHandlers"));
          //解析<mappers>节点,里面会使用XMLMapperBuilder
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
  • 我们看到这个方法的解析流程都很类似,最后面引入了XMLMapperBuilder解析节点,我们在前面挑一个解析typeAliases的看看细节,其他的就不一一分析了,然后进入5.2小节XMLMapperBuilder解析阶段的分析

5.1.4 XMLConfigBuilder#typeAliasesElement

  • typeAliasesElement解析别名节点,typeAliases有两种配置方式,如下所示:
        //配置示例,整个包配置或者配置单个类
        <typeAliases>
            <package name="com.intellif.mozping.entity"/>
            <typeAlias alias="Product" type="com.intellif.mozping.entity.Product"/>
        </typeAliases>
  • XMLConfigBuilder#typeAliasesElement
    private void typeAliasesElement(XNode parent) {
        if (parent != null) {
          //1.非空才会处理,依次遍历所有节点
          for (XNode child : parent.getChildren()) {
            //2.处理package类型配置
            if ("package".equals(child.getName())) {
              //2.1获取包名
              String typeAliasPackage = child.getStringAttribute("name");
              //2.2注册包名,将包名放到typeAliasRegistry里面,里面拿到包名之后还会进一步处理
              //最后会放到TypeAliasRegistry.TYPE_ALIASES这个Map里面去
              configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
              //3.处理typeAlias类型配置
              //3.1获取别名
              String alias = child.getStringAttribute("alias");
              //3.2获取类名
              String type = child.getStringAttribute("type");
              try {
                Class<?> clazz = Resources.classForName(type);
                //3.3下面的注册逻辑其实比前面注册包名要简单,注册包名要依次处理包下的类,也会调用registerAlias方法,
                //这里直接处理类,别名没有配置也没关系,里面会生成一个getSimpleName或者根据Alias注解去取别名
                if (alias == null) {
                  typeAliasRegistry.registerAlias(clazz);
                } else {
                  typeAliasRegistry.registerAlias(alias, clazz);
                }
              } catch (ClassNotFoundException e) {
                //4.其他类型直接报错
                throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
              }
            }
          }
        }
      }

5.2 XMLMapperBuilder

  • 最前面我们说过XMLMapperBuilder是用于解析Mapper.xml映射文件的,因此在前面的XMLConfigBuilder#parseConfiguration方法的解析流程最后一个步骤是mapperElement,该方法内部XMLMapperBuilder就会登场进行mapper.xml映射文件的解析工作,我们先看看这个方法:

5.2.1 XMLConfigBuilder#mapperElement

  • XMLConfigBuilder#mapperElement方法解析节点,这个方法包含解析mapper节点的主流程,但是解析的细节还看不到,解析的细节在后面的XMLMapperBuilder#parse里面,我们先通过这个方法看一下解析的主流程,mappers有多种配置方式,如下所示:
        <mappers>
            <!--直接映射到相应的mapper文件 -->
            <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
            <mapper url="xx"/>
            <mapper class="yy"/>
            <package name="com.intellif.mozping"/>
        </mappers>
  • XMLConfigBuilder#mapperElement
    /**
       * 解析配置文件的mappers子节点,方法主要是实现一个大体框架,按照resource->url->class的优先级读取配置,具体的解
       * 析细节是依赖于XMLMapperBuilder来实现的,XMLMapperBuilder通过parse方法屏蔽了细节,内部完成解析过程
       * */
      private void mapperElement(XNode parent) throws Exception {
        //
        if (parent != null) {
            //1.节点非空,遍历子节点逐个处理,因为mapperElement(root.evalNode("mappers"))解析的mappers里面可能有多个标签
          for (XNode child : parent.getChildren()) {
              //1.1 处理package类型的配置
            if ("package".equals(child.getName())) {
              //1.2 按照包来添加,扫包之后默认会在包下找与java接口名称相同的mapper映射文件,name就是包名,
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
              //1.3 一个一个Mapper.xml文件的添加 , resource、url和class三者是互斥的,resource优先级最高
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
              if (resource != null && url == null && mapperClass == null) {
                //1.4 按照resource属性实例化XMLMapperBuilder来解析xml配置文件
                ErrorContext.instance().resource(resource);
                InputStream inputStream = Resources.getResourceAsStream(resource);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                //1.5解析配置,因为XMLMapperBuilder继承了BaseBuilder,BaseBuilder内部持有Configuration对象,因此
                //XMLMapperBuilder解析之后直接把配置设置到Configuration对象
                mapperParser.parse();
              } else if (resource == null && url != null && mapperClass == null) {
                //1.6 按照url属性实例化XMLMapperBuilder来解析xml配置文件
                ErrorContext.instance().resource(url);
                InputStream inputStream = Resources.getUrlAsStream(url);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                mapperParser.parse();
              } else if (resource == null && url == null && mapperClass != null) {
                //1.7 按照class属性实例化XMLMapperBuilder来解析xml配置文件
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
              } else {
                //resource、url和class三者是互斥的,配置了多个或者不配置都抛出异常
                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
              }
            }
          }
        }
      }
  • 结合代码注释和配置文件的结构,流程还是比较清晰的,这里面会将mappers里面的节点信息全部注册到Configuration所持有的MapperRegistry对象里面去,2.2说过MapperRegistry是mapper接口动态代理的注册中心,总而言之Configuration里包含所有的配置信息,最后一种情况使用的是configuration.addMapper(mapperInterface);方法,其实前面几种情况最后也是走的这个方法,这里我们看到了主体流程,5.2.1我们开始看解析的具体过程分析。

5.2.2 XMLMapperBuilder#parse

  • XMLMapperBuilder#parse进入了XMLMapperBuilder解析mapper节点的详细流程,最后把mapper节点信息保存到Configuration的MapperRegistry里面去,并且会对Mapper.xml映射文件的内存进行解析,映射文件的内容是非常复杂的,我们慢慢跟进看
    public void parse() {
            //1.首先判断是否已经加载过了,没有加载才继续加载(loadedResources是一个set集合,保存了已经加载的映射文件,如果一个配置在mappers里面写了2次,那么第二次就不加载了)
            if (!configuration.isResourceLoaded(resource)) {
                //2.处理mapper子节点
                //这里使用XPathParser来解析xml文件,XPathParser在XMLMapperBuilder构造方法执行的时候就已经初始化好了,已经将
                //UserMapper.xml读取转换为一个document对象了
                configurationElement(parser.evalNode("/mapper"));
                //3.将解析过的文件添加到已经解析过的set集合里面
                configuration.addLoadedResource(resource);
                //4.注册mapper接口
                bindMapperForNamespace();
            }
    
            //5.处理解析失败的节点
            //把加载失败的节点重新加载一遍,因为这些节点可能在之前解析失败了,比如他们继承的节点还未加载导致,
            //因此这里把失败的部分再加载一次,之前加载失败的节点会放在一个map里面
            //6.处理解析失败的ResultMap节点
            parsePendingResultMaps();
            //7.处理解析失败的CacheRef节点
            parsePendingChacheRefs();
            //8.处理解析失败的Sql语句节点
            parsePendingStatements();
        }

5.2.3 XMLMapperBuilder#configurationElement

  • configurationElement方法是解析mapper映射文件的主流程,我们可以看到每种类型节点的解析过程,注意第八步是解析sql信息,我们前面说过,XMLMapperBuilder解析mapper文件但是不解析sql节点,sql节点是由XmlStatementBuilder来负责的,后面我们会看到XmlStatementBuilder的作用
        /**
         * 解析mapper.xml映射文件的主流程
         */
        private void configurationElement(XNode context) {
            try {
                //1.获取namespace属性(对应java接口的全路径名称);不能为空
                String namespace = context.getStringAttribute("namespace");
                if (namespace == null || namespace.equals("")) {
                    throw new BuilderException("Mapper's namespace cannot be empty");
                }
                //2.把namespace属性交给建造助手builderAssistant
                builderAssistant.setCurrentNamespace(namespace);
                //3.解析cache-ref节点
                cacheRefElement(context.evalNode("cache-ref"));
                //4.重点:解析cache节点,和缓存相关
                cacheElement(context.evalNode("cache"));
                //5.解析parameterMap节点(已废弃)
                parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                //6.重点:解析resultMap节点
                resultMapElements(context.evalNodes("/mapper/resultMap"));
                //7.重点:解析sql节点
                sqlElement(context.evalNodes("/mapper/sql"));
                //8.重点:解析sql语句,解析select、insert、update、delete节点
                buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            } catch (Exception e) {
                //异常处理
                throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
            }
        }

5.2.4 XMLMapperBuilder#bindMapperForNamespace

  • bindMapperForNamespace负责将mapper映射文件对应的java接口类(这个接口类的名称就是mapper文件里面的namespace)注册到Configuration的MapperRegistry里面,这代表该接口已经注册,这也是5.2.1主流程的目的
        /**
         *注册mapper接口 
         */
        private void bindMapperForNamespace() {
            //1.获取命名空间,在bindMapperForNamespace前面的configurationElement方法将namespace已经set到builderAssistant了,这里直接取出
            String namespace = builderAssistant.getCurrentNamespace();
            if (namespace != null) {
                Class<?> boundType = null;
                try {
                    //1.1 通过命名空间获取mapper接口的class对象
                    boundType = Resources.classForName(namespace);
                } catch (ClassNotFoundException e) {
                    //ignore, bound type is not required
                }
    
                if (boundType != null) {
                    //1.2 是否已经注册过该mapper接口?
                    if (!configuration.hasMapper(boundType)) {
                        // Spring may not know the real resource name so we set a flag
                        // to prevent loading again this resource from the mapper interface
                        // look at MapperAnnotationBuilder#loadXmlResource
                        //1.3如果没有注册,将命名空间添加至configuration.loadedResource集合中
                        configuration.addLoadedResource("namespace:" + namespace);
                        //1.4将mapper接口添加到mapper注册中心
                        configuration.addMapper(boundType);
                    }
                }
            }
        }

5.2.5 XMLMapperBuilder#parsePendingResultMaps

        //处理之前加载失败的resultMap
        private void parsePendingResultMaps() {
            //1之前加载失败的resultMap会保存在这个集合中,这里需要做的就是遍历这个集合并处理
            Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
            synchronized (incompleteResultMaps) {
                Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
                while (iter.hasNext()) {
                    try {
                        //2.遍历并且调用resolve处理即可,如果异常直接忽略,因为最后还会重试的
                        iter.next().resolve();
                        //3.处理完就从集合移除
                        iter.remove();
                    } catch (IncompleteElementException e) {
                        // ResultMap is still missing a resource...
                    }
                }
            }
        }
  • parsePendingChacheRefs(),parsePendingStatements()和parsePendingResultMaps方法是类似的,就不多解析了,这一步我们主要是梳理XMLMapperBuilder#parse方法,并对里面几个关键的方法做了大概的跟踪,但是底层的实现逻辑我们暂时不跟进,否则内容太多。

5.3 XmlStatementBuilder

  • 前面的XMLMapperBuilder我们跟了一部分源码,看到了XMLMapperBuilder解析mapper映射文件的主体流程,现在我们看XmlStatementBuilder是如何解析mapper中的SQL语句的,解析sql语句属于解析mapper文件的一部分,我们XMLMapperBuilder#configurationElement的
    最后一个流程方法buildStatementFromContext里面跟进去,就可以看到他的身影,我们跟进去看看主流程:

5.3.1 XMLStatementBuilder#parseStatementNode

  • parseStatementNode方法是解析sql语句的主流程方法
    
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        
        //解析sql语句节点并注册到condifuration对象
        private void buildStatementFromContext(List<XNode> list) {
            if (configuration.getDatabaseId() != null) {
                buildStatementFromContext(list, configuration.getDatabaseId());
            }
            buildStatementFromContext(list, null);
        }
        
    
       private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
            for (XNode context : list) {
                //1.由第三个火枪手XmlStatementBuilder来解析sql语句,并注册到configuration对象
                final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
                try {
                    //2.XMLStatementBuilder和XMLMapperBuilder一样都是继承自BaseBuilder,BaseBuilder持有Configuration对象,因此
                    //直接解析,让后将配置信息set进去即可
                    statementParser.parseStatementNode();
                } catch (IncompleteElementException e) {
                    //3.如果解析异常,就把对象添加到未完成的Map集合里面
                    configuration.addIncompleteStatement(statementParser);
                }
            }
        }

5.3.2 XMLStatementBuilder#parseStatementNode

  • XMLStatementBuilder#parseStatementNode是解析sql节点的核心方法,该方法是解析sql节点的主流程,包括四种sql节点,解析完毕之后会将sql语句信息封装之后,注册到Configuration对象里面的mappedStatements集合里面去,2.2说过MappedStatement存储了映射文件中的insert|delete|update|select节点的重要信息
        /**
         *解析sql节点的核心方法
         */
        public void parseStatementNode() {
            //1.获取sql节点的id
            String id = context.getStringAttribute("id");
            //2.获取databaseId
            String databaseId = context.getStringAttribute("databaseId");
    
            //不符合就返回
            if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
                return;
            }
            //3.获取sql节点的各种属性
            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");
            LanguageDriver langDriver = getLanguageDriver(lang);
    
            Class<?> resultTypeClass = resolveClass(resultType);
            String resultSetType = context.getStringAttribute("resultSetType");
            StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
            ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    
            //4.根据sql节点的名称获取操作的类型SqlCommandType(INSERT, UPDATE, DELETE, SELECT)
            String nodeName = context.getNode().getNodeName();
            SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
            //5.获取操作的各种配置,比如缓存,resultOrdered之类的
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
            //6.flushCache默认值是!isSelect,如果是查询语句,没有配置flushCache就是false,不是查询语句,没有配置flushCache就是true
            boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
            boolean useCache = context.getBooleanAttribute("useCache", isSelect);
            boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
            // Include Fragments before parsing
            //7.在解析sql语句之前先解析<include>节点
            XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
            includeParser.applyIncludes(context.getNode());
    
            // Parse selectKey after includes and remove them.
            //8.在解析sql语句之前,处理<selectKey>子节点,并在xml节点中删除
            processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
            // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
            //9.解析sql语句是解析mapper.xml的核心,实例化sqlSource,使用sqlSource封装sql语句
            SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
            //10.获取resultSets属性
            String resultSets = context.getStringAttribute("resultSets");
            //11.获取主键信息keyProperty
            String keyProperty = context.getStringAttribute("keyProperty");
            //12.获取主键信息keyColumn
            String keyColumn = context.getStringAttribute("keyColumn");
    
            //13.根据<selectKey>获取对应的SelectKeyGenerator的id
            KeyGenerator keyGenerator;
            String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
            keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    
            //14.获取keyGenerator对象,如果是insert类型的sql语句,会使用KeyGenerator接口获取数据库生产的id;
            if (configuration.hasKeyGenerator(keyStatementId)) {
                keyGenerator = configuration.getKeyGenerator(keyStatementId);
            } else {
                keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                        ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
            }
    
            //15.通过MapperBuilderAssistant类型的builderAssistant实例化MappedStatement,并注册至configuration对象
            //相当于自身主要复杂属性的解析和读取,构造对象的过程交给助手来做
            //注意前面解析了mapper节点之后是注册到mapperRegistry,那是注册mapper节点信息,注册的实际上是对应的java接口
            //这里是注册到MappedStatement,注册的是sql语句信息,但是二者都是保存在Configuration对象里面的Map集合
            builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                    resultSetTypeEnum, flushCache, useCache, resultOrdered,
                    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
        }

5.3.3 MapperBuilderAssistant#addMappedStatement()

  • 在XMLStatementBuilder#parseStatementNode的最后一步,通过调用builderAssistant.addMappedStatement方法来完成MappedStatement对象实例化和注册到Configuration对象的mappedStatements集合,sql语句解析之后,对应到的java的数据结构类是MappedStatement,它是保存sql语句的数据结构。sql语句的节点的全部配置,都能够在MappedStatement中找到对应的属性。到此XmlConfigBuilder、XmlMapperBuilder和XmlStatementBuilder三者分工协作,依次完成了主配置文件、mapper映射文件(不含sql信息)和mapper文件sql节点信息配置的加载,最终都把这些信息放到了Configuration这个大配置对象里面去了,到此处虽然还有很多细节没有分析,但是基本上初始化的流程已经看得出个大概了。 下面是代码细节:
        /**
         *通过builderAssistant实例化MappedStatement,并注册至configuration对象,参数列表非常复杂
         */
        public MappedStatement addMappedStatement(
                String id,
                SqlSource sqlSource,
                StatementType statementType,
                SqlCommandType sqlCommandType,
                Integer fetchSize,
                Integer timeout,
                String parameterMap,
                Class<?> parameterType,
                String resultMap,
                Class<?> resultType,
                ResultSetType resultSetType,
                boolean flushCache,
                boolean useCache,
                boolean resultOrdered,
                KeyGenerator keyGenerator,
                String keyProperty,
                String keyColumn,
                String databaseId,
                LanguageDriver lang,
                String resultSets) {
    
            //1.确保Cache-ref节点已经被解析过了
            if (unresolvedCacheRef) {
                throw new IncompleteElementException("Cache-ref not yet resolved");
            }
    
            id = applyCurrentNamespace(id, false);
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
            //2.典型的建造者模式,创建MappedStatement对象
            MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
                    .resource(resource)
                    .fetchSize(fetchSize)
                    .timeout(timeout)
                    .statementType(statementType)
                    .keyGenerator(keyGenerator)
                    .keyProperty(keyProperty)
                    .keyColumn(keyColumn)
                    .databaseId(databaseId)
                    .lang(lang)
                    .resultOrdered(resultOrdered)
                    .resultSets(resultSets)
                    .resultMaps(getStatementResultMaps(resultMap, resultType, id))
                    .resultSetType(resultSetType)
                    .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
                    .useCache(valueOrDefault(useCache, isSelect))
                    .cache(currentCache);
    
            ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
            if (statementParameterMap != null) {
                statementBuilder.parameterMap(statementParameterMap);
            }
            //3.build方法创建MappedStatement对象
            MappedStatement statement = statementBuilder.build();
            //4.添加MappedStatement对象到全局Configuration配置对象的对应Map中
            configuration.addMappedStatement(statement);
            return statement;
        }

六、补充

6.1 resultMap加载

  • 在5.2.3 XMLMapperBuilder#configurationElement解析mapper.xml映射文件的主流程的第6步是解析resultMap配置节点。resultMap的数据结构相对比较复杂,设计的映射关系,属性比较多,比如id,type,里面又可能有构造方法节点,id,result等。因此加载解析也比较复杂在,Configuration对象里面有一个resultMap类型的Map对象来保存全部的resultMap,key是namespace+id,保证全局唯一。解析后对应的数据结构,可以参考org.apache.ibatis.mapping.ResultMap这个类。属性展示如下,这个类代码不多,可以参考源码理解。
      private Class<?> type;
      private List<ResultMapping> resultMappings;
      private List<ResultMapping> idResultMappings;
      private List<ResultMapping> constructorResultMappings;
      private List<ResultMapping> propertyResultMappings;
      private Set<String> mappedColumns;
      private Discriminator discriminator;
      private boolean hasNestedResultMaps;
      private boolean hasNestedQueries;
      //是否开启自定映射
      private Boolean autoMapping;
  • 典型的resultMap配置
     <resultMap id="BaseResultMap" type="Player">
            <!-- <constructor>
                可以指定构造方法
                <idArg column="id" javaType="int"/>
                <arg column="user_name" javaType="String"/>
            </constructor>  -->
            <id column="id" property="id" jdbcType="INTEGER"/>
            <result column="playName" property="playName" jdbcType="VARCHAR"/>
            <result column="playNo" property="playNo" jdbcType="INTEGER"/>
            <result column="team" property="team" jdbcType="VARCHAR"/>
            <result column="height" property="height" jdbcType="VARCHAR"/>
        </resultMap>
  • 加载resuleMap的流程在XMLMapperBuilder#resultMapElement()方法内,代码如下,只有部分注释,有兴趣可以继续研究
    //解析resultMap节点
        private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
            ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
            //1.获取id
            String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
            //2.获取type
            String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",
                    resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
            //3.获取extends
            String extend = resultMapNode.getStringAttribute("extends");
            //4.获取autoMapping配置
            Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
            //5.根据type解析到类型
            Class<?> typeClass = resolveClass(type);
            //6.discriminator配置默认为null
            Discriminator discriminator = null;
            List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
            resultMappings.addAll(additionalResultMappings);
            List<XNode> resultChildren = resultMapNode.getChildren();
            for (XNode resultChild : resultChildren) {
                //判断是否有构造方法节点
                if ("constructor".equals(resultChild.getName())) {
                    processConstructorElement(resultChild, typeClass, resultMappings);
                } else if ("discriminator".equals(resultChild.getName())) {
                    discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
                } else {
                    List<ResultFlag> flags = new ArrayList<ResultFlag>();
                    if ("id".equals(resultChild.getName())) {
                        flags.add(ResultFlag.ID);
                    }
                    resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
                }
            }
            ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
            try {
                return resultMapResolver.resolve();
            } catch (IncompleteElementException e) {
                configuration.addIncompleteResultMap(resultMapResolver);
                throw e;
            }
        }

6.2 Mybais的初始化流程图

202306062341081162.png

6.3 小结

  • 更多的节点配置信息源码,有兴趣可以自行解读,代码量比较大,但是熟悉了Mybatis框架和配置之后,总体结构还是比较清晰的,阅读这些源码本身难度不是很大,有助于帮助我们了解Mybatis框架,关键还是理解里面不同模块运用的设计思想和设计模式。下一篇文章我们分析核心流程的第二阶段—动态代理阶段

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文