• Mybatis解析配置文件流程


    Mybatis解析配置文件

    mybatis-config.xml 解析全流程

    MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。

    这里创建的 XMLConfigBuilder 对象的核心功能就是解析 mybatis-config.xml 配置文件。

    BaseBuilder 抽象类扮演了构造者模式中 Builder 接口的角色,下面我们先来看 BaseBuilder 中各个字段的定义。

    • configuration(Configuration 类型):MyBatis 的初始化过程就是围绕 Configuration 对象展开的,我们可以认为 Configuration 是一个单例对象,MyBatis 初始化解析到的全部配置信息都会记录到 Configuration 对象中。

    • typeAliasRegistry(TypeAliasRegistry 类型):别名注册中心。我们在 mybatis-config.xml 配置文件中,使用 标签为很多类定义了别名。

    • typeHandlerRegistry(TypeHandlerRegistry 类型):TypeHandler 注册中心。除了定义别名之外,我们在 mybatis-config.xml 配置文件中,还可以使用 标签添加自定义 TypeHandler 实现,实现数据库类型与 Java 类型的自定义转换,这些自定义的 TypeHandler 都会记录在这个 TypeHandlerRegistry 对象中。

    除了关联 Configuration 对象之外,BaseBuilder 还提供了另外两个基本能力:

    • 解析别名,核心逻辑是在 resolveAlias() 方法中实现的,主要依赖于 TypeAliasRegistry 对象;

    • 解析 TypeHandler,核心逻辑是在 resolveTypeHandler() 方法中实现的,主要依赖于 TypeHandlerRegistry 对象。

    首先我们来了解一下 XMLConfigBuilder 的核心字段。

    • parsed(boolean 类型):状态标识字段,记录当前 XMLConfigBuilder 对象是否已经成功解析完 mybatis-config.xml 配置文件。

    • parser(XPathParser 类型):XPathParser 对象是一个 XML 解析器,这里的 parser 对象就是用来解析 mybatis-config.xml 配置文件的。

    • environment(String 类型): 标签定义的环境名称。

    • localReflectorFactory(ReflectorFactory 类型):ReflectorFactory 接口的核心功能是实现对 Reflector 对象的创建和缓存。

    在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析,其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程,核心步骤如下:

    • 解析 标签;

    • 解析 标签;

    处理日志相关组件;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签;

    • 解析 标签。

    从 parseConfiguration()方法中,我们可以清晰地看到 XMLConfigBuilder 对 mybatis-config.xml 配置文件中各类标签的解析方法,下面我们就逐一介绍这些方法的核心实现。

    1. 处理标签
      我们可以通过 标签定义 KV 信息供 MyBatis 使用,propertiesElement() 方法的核心逻辑就是解析 mybatis-config.xml 配置文件中的 标签。

    标签中解析出来的 KV 信息会被记录到一个 Properties 对象(也就是 Configuration 全局配置对象的 variables 字段),在后续解析其他标签的时候,MyBatis 会使用这个 Properties 对象中记录的 KV 信息替换匹配的占位符。

    1. 处理标签
      MyBatis 中有很多全局性的配置,例如,是否使用二级缓存、是否开启懒加载功能等,这些都是通过 mybatis-config.xml 配置文件中的 标签进行配置的。

    XMLConfigBuilder.settingsAsProperties() 方法的核心逻辑就是解析 标签,并将解析得到的配置信息记录到 Configuration 这个全局配置对象的同名属性中,具体实现如下:

    private Properties settingsAsProperties(XNode context) {
        if (context == null) {
            return new Properties();
        }
        // 处理<settings>标签的所有子标签,也就是<setting>标签,将其name属性和value属性
        // 整理到Properties对象中保存
        Properties props = context.getChildrenAsProperties();
        // 创建Configuration对应的MetaClass对象
        MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
        // 检测Configuration对象中是否包含每个配置项的setter方法
        for (Object key : props.keySet()) {
            if (!metaConfig.hasSetter(String.valueOf(key))) {
                throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
            }
        }
        return props;
    }
    
    1. 处理标签
      XMLConfigBuilder 中提供了 typeAliasesElement() 方法和 typeHandlerElement() 方法,分别用来负责处理 标签和 标签,解析得到的别名信息和 TypeHandler 信息就会分别记录到 TypeAliasRegistry 和 TypeHandlerRegistry(前面介绍 BaseHandler 的时候,我们已经简单介绍过这两者了)。

    下面我们以 typeHandlerElement() 方法为例来分析一下这个过程:

    private void typeHandlerElement(XNode parent) {
        if (parent != null) {
            for (XNode child : parent.getChildren()) { // 处理全部<typeHandler>子标签
                if ("package".equals(child.getName())) { 
                    // 如果指定了package属性,则扫描指定包中所有的类,
                    // 并解析@MappedTypes注解,完成TypeHandler的注册
                    String typeHandlerPackage = child.getStringAttribute("name");
                    typeHandlerRegistry.register(typeHandlerPackage);
                } else {
                    // 如果没有指定package属性,则尝试获取javaType、jdbcType、handler三个属性
                    String javaTypeName = child.getStringAttribute("javaType");
                    String jdbcTypeName = child.getStringAttribute("jdbcType");
                    String handlerTypeName = child.getStringAttribute("handler");
                    // 根据属性确定TypeHandler类型以及它能够处理的数据库类型和Java类型
                    Class<?> javaTypeClass = resolveClass(javaTypeName);
                    JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                    Class<?> typeHandlerClass = resolveClass(handlerTypeName);
                    // 调用TypeHandlerRegistry.register()方法注册TypeHandler
                    if (javaTypeClass != null) {
                        if (jdbcType == null) {
                            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                        } else {
                            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                        }
                    } else {
                        typeHandlerRegistry.register(typeHandlerClass);
                    }
                }
            }
        }
    }
    
    1. 处理标签
      我们知道 MyBatis 是一个非常易于扩展的持久层框架,而插件就是 MyBatis 提供的一种重要扩展机制。

    我们可以自定义一个实现了 Interceptor 接口的插件来扩展 MyBatis 的行为,或是拦截 MyBatis 的一些默认行为。插件的工作机制我们会在后面的课时中详细分析,这里我们重点来看 MyBatis 初始化过程中插件配置的加载,也就是 XMLConfigBuilder 中的 pluginElement()方法,该方法的核心就是解析 标签中配置的自定义插件,具体实现如下:

    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            // 遍历全部的<plugin>子标签
            for (XNode child : parent.getChildren()) {
                // 获取每个<plugin>标签中的interceptor属性
                String interceptor = child.getStringAttribute("interceptor");
                // 获取<plugin>标签下的其他配置信息
                Properties properties = child.getChildrenAsProperties();
                // 初始化interceptor属性指定的自定义插件
                Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
                // 初始化插件的配置
                interceptorInstance.setProperties(properties);
                // 将Interceptor对象添加到Configuration的插件链中保存,等待后续使用
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }
    
    1. 处理标签
      MyBatis 支持自定义 ObjectFactory 实现类和 ObjectWrapperFactory。XMLConfigBuilder 中的 objectFactoryElement() 方法就实现了加载自定义 ObjectFactory 实现类的功能,其核心逻辑就是解析 标签中配置的自定义 ObjectFactory 实现类,并完成相关的实例化操作,相关的代码实现如下:
    private void objectFactoryElement(XNode context) throws Exception {
    if (context != null) {
        // 获取<objectFactory>标签的type属性
        String type = context.getStringAttribute("type");
        // 根据type属性值,初始化自定义的ObjectFactory实现
        ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
        // 初始化ObjectFactory对象的配置
        Properties properties = context.getChildrenAsProperties();
        factory.setProperties(properties);
        // 将ObjectFactory对象记录到Configuration这个全局配置对象中
        configuration.setObjectFactory(factory);
    }
    

    ​ 除了 标签之外,我们还可以通过 标签和 标签配置自定义的ObjectWrapperFactory 实现类和 ReflectorFactory 实现类,这两个标签的解析分别对应 objectWrapperFactoryElement() 方法和 reflectorFactoryElement() 方法。

    1. 处理标签
      在 MyBatis 中,我们可以通过 标签为不同的环境添加不同的配置,例如,线上环境、预上线环境、测试环境等,每个 标签只会对应一种特定的环境配置。

    environmentsElement() 方法中实现了 XMLConfigBuilder 处理 标签的核心逻辑,它会根据 XMLConfigBuilder.environment 字段值,拿到正确的 标签,然后解析这个环境中使用的 TransactionFactory、DataSource 等核心对象,也就知道了 MyBatis 要请求哪个数据库、如何管理事务等信息。

    ​ 下面是 environmentsElement() 方法的核心逻辑:

    private void environmentsElement(XNode context) throws Exception {
        if (context != null) {
            if (environment == null) { // 未指定使用的环境id,默认获取default值 
                environment = context.getStringAttribute("default");
            }
            // 获取<environment>标签下的所有配置
            for (XNode child : context.getChildren()) {
                // 获取环境id
                String id = child.getStringAttribute("id");
                if (isSpecifiedEnvironment(id)) { 
                    // 获取<transactionManager>、<dataSource>等标签,并进行解析,其中会根据配置信息初始化相应的TransactionFactory对象和DataSource对象
                    TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                    DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                    DataSource dataSource = dsFactory.getDataSource();
                    // 创建Environment对象,并关联创建好的TransactionFactory和DataSource
                    Environment.Builder environmentBuilder = new Environment.Builder(id)
                            .transactionFactory(txFactory)
                            .dataSource(dataSource);
                    // 将Environment对象记录到Configuration中,等待后续使用
                    configuration.setEnvironment(environmentBuilder.build());
                }
            }
        }
    }
    
    1. 处理标签

    在 mybatis-config.xml 配置文件中,我们可以通过 标签定义需要支持的全部数据库的 DatabaseId,在后续编写 Mapper 映射配置文件的时候,就可以为同一个业务场景定义不同的 SQL 语句(带有不同的 DataSourceId),来支持不同的数据库,这里就是靠 DatabaseId 来确定哪个 SQL 语句支持哪个数据库的。

    ​ databaseIdProviderElement() 方法是 XMLConfigBuilder 处理 标签的地方,其中的核心就是获取 DatabaseId 值,具体实现如下:

    private void databaseIdProviderElement(XNode context) throws Exception {
        DatabaseIdProvider databaseIdProvider = null;
        if (context != null) {
            // 获取type属性值
            String type = context.getStringAttribute("type");
            if ("VENDOR".equals(type)) { // 兼容操作
                type = "DB_VENDOR";
            }
            // 初始化DatabaseIdProvider
            Properties properties = context.getChildrenAsProperties();
            databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
            databaseIdProvider.setProperties(properties);
        }
        Environment environment = configuration.getEnvironment();
        if (environment != null && databaseIdProvider != null) {
            // 通过DataSource获取DatabaseId,并保存到Configuration中,等待后续使用
            String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
            configuration.setDatabaseId(databaseId);
        }
    }
    

    ​ 可以看到,解析 标签之后会得到一个 DatabaseIdProvider 对象,其核心方法是 getDatabaseId() 方法,主要是根据前面解析得到的 DataSource 对象来生成 DatabaseId。DatabaseIdProvider 的继承关系如下图所示:

    image-20210308105755350

    从继承关系图中可以看出,DefaultDatabaseIdProvider 是个空实现,而且已被标记为过时了,所以这里我们就重点来看 VendorDatabaseIdProvider 实现。

    在 getDatabaseId() 方法中,VendorDatabaseIdProvider 首先会从 DataSource 中拿到数据库的名称,然后根据 标签配置和 DataSource 返回的数据库名称,确定最终的 DatabaseId 标识,具体实现如下:

    public String getDatabaseId(DataSource dataSource) {
        // 省略边界检查和异常处理
        return getDatabaseName(dataSource);
    }
    private String getDatabaseName(DataSource dataSource) throws SQLException {
        // 从数据库连接中,获取数据库名称
        String productName = getDatabaseProductName(dataSource);
        if (this.properties != null) {
            // 根据<databaseIdProvider>标签配置,查找自定义数据库名称
            for (Map.Entry<Object, Object> property : properties.entrySet()) {
                if (productName.contains((String) property.getKey())) {
                    return (String) property.getValue(); // 返回配置的value
                }
            }
            return null;
        }
        return productName;
    }
    
    1. 处理标签
      除了 mybatis-config.xml 这个全局配置文件之外,MyBatis 初始化的时候还会加载 标签下定义的 Mapper 映射文件。 标签中会指定 Mapper.xml 映射文件的位置,通过解析 标签,MyBatis 就能够知道去哪里加载这些 Mapper.xml 文件了。

    mapperElement() 方法就是 XMLConfigBuilder 处理 标签的具体实现,其中会初始化 XMLMapperBuilder 对象来加载各个 Mapper.xml 映射文件。同时,还会扫描 Mapper 映射文件相应的 Mapper 接口,处理其中的注解并将 Mapper 接口注册到 MapperRegistry 中。

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

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) { // 遍历每个子标签
                if ("package".equals(child.getName())) {
                    // 如果指定了<package>子标签,则会扫描指定包内全部Java类型
                    String mapperPackage = child.getStringAttribute("name");
                    configuration.addMappers(mapperPackage);
                } else {
                    // 解析<mapper>子标签,这里会获取resource、url、class三个属性,这三个属性互斥
                    String resource = child.getStringAttribute("resource");
                    String url = child.getStringAttribute("url");
                    String mapperClass = child.getStringAttribute("class");
                    // 如果<mapper>子标签指定了resource或是url属性,都会创建XMLMapperBuilder对象,
                    // 然后使用这个XMLMapperBuilder实例解析指定的Mapper.xml配置文件
                    if (resource != null && url == null && mapperClass == null) {
                        ErrorContext.instance().resource(resource);
                        InputStream inputStream = Resources.getResourceAsStream(resource);
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if (resource == null && url != null && mapperClass == null) {
                        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) {
                        // 如果<mapper>子标签指定了class属性,则向MapperRegistry注册class属性指定的Mapper接口
                        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.");
                    }
                }
            }
        }
    }
    
  • 相关阅读:
    VS2010如何以管理员权限启动? 转载
    ArcGIS属性编辑字符型字段值出现乱码问题
    点批量移动到线上(ArcGis版) 转载
    C#判断数据库中取出的字段值是否为空(NULL) .
    方框内打勾(钩)的符号(word和excel)
    C# 获取屏幕的大小
    神通数据库的备份和还原
    推荐系列文章:《DotText源码阅读》
    Lucene.Net 2.3.1开发介绍 —— 阅读索引(转载)
    Lucene.Net:使用eaglet的盘古分词进行分词和搜索(转载)
  • 原文地址:https://www.cnblogs.com/dalianpai/p/14500185.html
Copyright © 2020-2023  润新知