【MyBatis】MyBatis解析全局配置文件源码详解

目录

一、前言

思维导图概括

二、配置文件解析过程分析

2.1 配置文件解析入口

2.2 初始化XMLConfigBuilder

2.3 XMLConfigBuilder#parse()方法:解析全局配置文件

2.3.1 解析properties配置

2.3.2 解析settings配置

2.3.2.1 元信息对象(MetaClass)创建过程源码解析

2.3.2.1.1 DefaultReflectorFactory 源码分析

 

2.3.2.1.2 Reflector 源码分析

● Reflector 构造方法及成员变量分析

● getter 方法解析过程

● setter 方法解析过程

2.3.2.1.3 PropertyTokenizer 源码分析

2.3.2.2 小结

2.3.3 设置 settings 配置到 Configuration 中

2.3.4 解析 typeAliases 配置

2.3.4.1 从 typeAlias 节点中解析并注册别名

2.3.4.2 从指定的包中解析并注册别名

2.3.4.3 注册 MyBatis 内部类及常见类型的别名

2.3.5 解析 plugins 配置

2.3.6 解析 environments 配置

2.3.7 解析 typeHandlers 配置

2.3.7.1 register(Class, JdbcType, Class) 方法分析

2.3.7.2 register(Class, Class) 方法分析

2.3.7.3 register(Class) 方法分析

2.3.7.4 register(String) 方法分析

2.3.7.5 小结

2.3.8 解析 mappers 配置

2.4 创建SqlSessionFactory对象

三、总结

解析全局配置文件的时序图:


一、前言

前面我们介绍了MyBatis的一些基本特性和使用方法,对MyBatis有了个初步了解。接下来,我们将着手来分析一下MyBatis的源码,从源码层面复盘MyBatis的执行流程。首先我们先来看MyBatis是如何解析全局配置文件(mybatis-config.xml)的。

思维导图概括

二、配置文件解析过程分析

有了上述思维导图,我们对配置文件文件的解析过程就有了一个大概的认识,下面我们就来具体分析下解析过程。

2.1 配置文件解析入口

// 全局配置文件的路径
String resource = "chapter1/mybatis-cfg.xml";

// 使用Resources将全局配置文件转化为文件流
InputStream inputStream = Resources.getResourceAsStream(resource);

// 通过加载配置文件流,将XML配置文件构建为Configuration配置类,构建一个SqlSessionFactory,默认是DefaultSqlSessionFactory
SqlSessionFactory  sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

从上述示例代码中我们可以很清晰的看出,初始化过程是:

  1. 首先通过MyBatis 提供的工具类Resources 解析配置文件得到文件流;
  2. 然后将文件流传给SqlSessionFactoryBuilder的build方法,并最终得到sqlSessionFactory。

那么我们MyBatis的初始化入口就是SqlSessionFactoryBuilder类的build()方法。build()方法有很多个重载方法,区别就是传入的参数不同,这里我们就以传入InputStream类型为例来分析源码:

//* SqlSessionFactoryBuilder类
public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 根据全局配置文件的文件流实例化出一个XMLConfigBuilder对象
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 使用XMLConfigBuilder的parse()方法构造出Configuration对象
        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.
        }
    }
}

// 调用链中最后一个build方法使用了一个Configuration对象作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

从上述源码,我们可以知道build 构建SqlSessionFactory 分三步:

  1. 首先实例化一个XMLConfigBuilder;
  2. 然后调用XMLConfigBuilder的parse方法得到Configuration对象;
  3. 最后将Configuration对象作为参数实例化一个DefaultSqlSessionFactory 即SqlSessionFactory对象。

从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过XMLConfigBuilder进行解析的。不过目前这里还没有非常明确的解析逻辑,所以我们继续往下看。

2.2 初始化XMLConfigBuilder

接着往下看,下面我们来看看XMLConfigBuilder类。首先是实例化XMLConfigBuilder的过程:

//* XMLConfigBuilder类
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    // 这里调用XPathParser构造方法来实例化XPathParser对象
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

// XMLConfigBuilder类有6个构造函数,最终其实都是调用的这个函数,传入XPathParser
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    // 首先调用父类初始化Configuration,这样XMLConfigBuilder就持有了Configuration对象
    super(new Configuration());
    // 错误上下文设置成SQL Mapper Configuration(XML文件配置),以便后面出错了报错用吧
    ErrorContext.instance().resource("SQL Mapper Configuration");
    // 将Properties全部设置到Configuration里面去
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    // 将XPathParser实例对象设置到XMLConfigBuilder中
    this.parser = parser;
}

//* XPathParser类
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
    commonConstructor(validation, variables, entityResolver);
    this.document = createDocument(new InputSource(reader));
}

private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
        // 这个是DOM解析方式
        // 得到DocumentBuilderFactory对象,工厂设计模式,用来创建DocumentBuilder对象
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(validation);
        // 命名空间
        factory.setNamespaceAware(false);
        // 忽略注释
        factory.setIgnoringComments(true);
        // 忽略空白
        factory.setIgnoringElementContentWhitespace(false);
        // 把 CDATA 节点转换为 Text 节点
        factory.setCoalescing(false);
        // 扩展实体引用
        factory.setExpandEntityReferences(true);
        // 通过DocumentBuilderFactory创建DocumentBuilder对象
        DocumentBuilder builder = factory.newDocumentBuilder();
        // 需要注意的就是定义了EntityResolver(XMLMapperEntityResolver),这样不用联网去获取DTD,
        // 将DTD放在org\apache\ibatis\builder\xml\mybatis-3-config.dtd,来达到验证xml合法性的目的
        builder.setEntityResolver(entityResolver);
        builder.setErrorHandler(new ErrorHandler() {
            @Override
            public void error(SAXParseException exception) throws SAXException {
                throw exception;
            }
            @Override
            public void fatalError(SAXParseException exception) throws SAXException {
                throw exception;
            }
            @Override
            public void warning(SAXParseException exception) throws SAXException {
            }
        });
        // DocumentBuilder的parse方法用于解析输入流,将xml解析到Document对象中
        return builder.parse(inputSource);
    } catch (Exception e) {
        throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
}

从上述源码中,我们可以看出在XMLConfigBuilder的实例化过程包括两个过程:

  1. 创建XPathParser的实例并初始化;
  2. 创建Configuration的实例对象,然后将XPathParser的实例设置到XMLConfigBuilder中。

XPathParser 初始化主要做了两件事:

  1. 初始化DocumentBuilder对象;
  2. 并通过调用DocumentBuilder对象的parse方法得到Document对象,我们配置文件的配置就全部都转移到了Document对象中。

最终,XMLConfigBuilder对象中就持有了XPathParser对象和Configuration对象,XPathParser对象中就持有了解析XML得到的Document对象。

我们下面通过调试看看Document 对象中的内容,测试用例是MyBatis 自身的单元测试XPathParserTest

测试的xml:

<!--
  nodelet_test.xml
-->
<employee id="${id_var}">
    <blah something="that"/>
    <first_name>Jim</first_name>
    <last_name>Smith</last_name>
    <birth_date>
        <year>1970</year>
        <month>6</month>
        <day>15</day>
    </birth_date>
    <height units="ft">5.8</height>
    <weight units="lbs">200</weight>
    <active>true</active>
</employee>

测试用例:

//* XPathParserTest类
@Test
public void shouldTestXPathParserMethods() throws Exception {
    String resource = "resources/nodelet_test.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    XPathParser parser = new XPathParser(inputStream, false, null, null);
    assertEquals((Long)1970l, parser.evalLong("/employee/birth_date/year"));
    assertEquals((short) 6, (short) parser.evalShort("/employee/birth_date/month"));
    assertEquals((Integer) 15, parser.evalInteger("/employee/birth_date/day"));
    assertEquals((Float) 5.8f, parser.evalFloat("/employee/height"));
    assertEquals((Double) 5.8d, parser.evalDouble("/employee/height"));
    assertEquals("${id_var}", parser.evalString("/employee/@id"));
    assertEquals(Boolean.TRUE, parser.evalBoolean("/employee/active"));
    assertEquals("<id>${id_var}</id>", parser.evalNode("/employee/@id").toString().trim());
    assertEquals(7, parser.evalNodes("/employee/*").size());
    XNode node = parser.evalNode("/employee/height");
    assertEquals("employee/height", node.getPath());
    assertEquals("employee[${id_var}]_height", node.getValueBasedIdentifier());
}

调试结果:

2.3 XMLConfigBuilder#parse()方法:解析全局配置文件

介绍完XMLConfigBuilder的初始化过程之后,接着我们来看看XMLConfigBuilder中的parse()方法,由前面其初始化过程我们可以得知我们的MyBatis全局配置信息(mybatis-config.xml)已经保存到了XMLConfigBuilder的XPathParser对象的Document中了。XMLConfigBuilder中的parse()方法就是去解析MyBatis的全局配置文件,其实就是将XPathParser中已经解析到Document对象的全局配置信息转移到XMLConfigBuilder对象持有的Configuration对象中,不多说了,看源码。

//* XMLConfigBuilder类
// 解析全局配置信息
public Configuration parse() {
    // 如果已经解析过了,报错
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 将是否解析过设置为true,表示我们要解析全局配置文件了
    parsed = true;    
    // parser就是XMLConfigBuilder持有的XPathParser对象
    // 开始解析全局配置文件,将XPathParser对象持有的Document对象中的全局配置信息,解析转移到configuration对象中
    // 全局配置文件的根节点是configuration(就是xml中的<configuration>标签),parseConfiguration方法要传入根节点对象
    parseConfiguration(parser.evalNode("/configuration"));
    // 返回解析完成的configuration对象,此时全局配置信息已经解析到了configuration对象中了
    return configuration;
}

到这里大家可以看到一些端倪了,注意一个 xpath 表达式 - /configuration。这个表达式代表的是 MyBatis 的<configuration/>标签,这里选中这个标签,并传递给parseConfiguration方法。我们继续跟下去。

//* XMLConfigBuilder类
// 解析全局配置信息到configuration对象中
private void parseConfiguration(XNode root) {
    try {
        // 分步骤解析
        /**
        * 1.解析 properties节点
        *     对应mybatis-config.xml标签:<properties resource="mybatis/db.properties" />
        *     解析到org.apache.ibatis.parsing.XPathParser#variables成员属性 和
        *           org.apache.ibatis.session.Configuration#variables成员属性
        */
        propertiesElement(root.evalNode("properties"));

        /**
         * 2.解析settings节点
         * 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
         * 
         *      对应mybatis-config.xml中的<settings>标签
         *      <settings>
                    <setting name="cacheEnabled" value="true"/>
                    <setting name="lazyLoadingEnabled" value="true"/>
                <setting name="mapUnderscoreToCamelCase" value="false"/>
                <setting name="localCacheScope" value="SESSION"/>
                <setting name="jdbcTypeForNull" value="OTHER"/>
                    ..............
                </settings>
            解析到XMLConfigBuilder的settings成员属性中
        *
        */
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        /**
         * 基本没有用过该属性
         * VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
             Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
            解析到:org.apache.ibatis.session.Configuration#vfsImpl属性
        */
        loadCustomVfs(settings);
        /**
         * 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
         * SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
         * 
         *   解析到org.apache.ibatis.session.Configuration#logImpl属性
         */
        loadCustomLogImpl(settings);

        /**
         * 3.解析类型别名
         *      对应mybatis-config.xml中的<typeAliases>标签
                <typeAliases>
                    <typeAlias alias="Author" type="cn.tulingxueyuan.pojo.Author"/>
                </typeAliases>
                <typeAliases>
                    <package name="cn.tulingxueyuan.pojo"/>
                </typeAliases>
            解析到org.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases成员属性
        */
        typeAliasesElement(root.evalNode("typeAliases"));

        /**
         * 4.解析插件(比如分页插件)
         *      mybatis自带的
         *      Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
                ParameterHandler (getParameterObject, setParameters)
                ResultSetHandler (handleResultSets, handleOutputParameters)
                StatementHandler (prepare, parameterize, batch, update, query)
            解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors成员属性
        */
        pluginElement(root.evalNode("plugins"));
        // 5.对象工厂
        objectFactoryElement(root.evalNode("objectFactory"));
        // 6.对象包装工厂
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 7.将前面解析出来的settings配置设置到Configuration对象中
        settingsElement(root.evalNode("settings"));

        /**
         * 8.解析mybatis环境
         *      对应mybatis-config.xml中的<environments>标签
                <environments default="dev">
                    <environment id="dev">
                        <transactionManager type="JDBC"/>
                        <dataSource type="POOLED">
                            <property name="driver" value="${jdbc.driver}"/>
                            <property name="url" value="${jdbc.url}"/>
                            <property name="username" value="root"/>
                            <property name="password" value="Zw726515"/>
                        </dataSource>
                    </environment>
                    <environment id="test">
                        <transactionManager type="JDBC"/>
                        <dataSource type="POOLED">
                            <property name="driver" value="${jdbc.driver}"/>
                            <property name="url" value="${jdbc.url}"/>
                            <property name="username" value="root"/>
                            <property name="password" value="123456"/>
                        </dataSource>
                    </environment>
                </environments>
        *   解析到:org.apache.ibatis.session.Configuration#environment成员属性
        *   在集成spring情况下由 spring-mybatis提供数据源和事务工厂
        */
        environmentsElement(root.evalNode("environments"));
        /**
         * 9.解析数据库厂商databaseIdProvider
         * 
         *     对应mybatis-config.xml中的<databaseIdProvider>标签
         *     <databaseIdProvider type="DB_VENDOR">
                    <property name="SQL Server" value="sqlserver"/>
                    <property name="DB2" value="db2"/>
                    <property name="Oracle" value="oracle" />
                    <property name="MySql" value="mysql" />
                </databaseIdProvider>
        *  解析到:org.apache.ibatis.session.Configuration#databaseId成员属性
        */
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        /**
         * 10.解析类型处理器节点
         *      对应mybatis-config.xml中的<typeHandlers>标签
         *      <typeHandlers>
                    <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
                </typeHandlers>
            解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap成员属性
        */
        typeHandlerElement(root.evalNode("typeHandlers"));

        /**
         * 11.解析mapper映射器(最最最最最重要的就是解析我们的mapper)
         *
         *      对应mybatis-config.xml中的<mappers>标签
         *          <mappers>
                        <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>   // 指定xml
                        <mapper class="com.tuling.mapper.DeptMapper"></mapper>   // 指定Mapper类
                        <package name="com.tuling.mapper"></package>  // 也可以批量指定Mapper类所在的包名
                   
                    </mappers>
                    resource:来注册我们的class类路径下的
                    url:来指定我们磁盘下的或者网络资源的
                    class:
                        1.若注册Mapper不带xml文件的,这里可以直接注册
                        2.若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名,同路径
            解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers成员属性
                            
        */
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

至此,一个MyBatis的解析过程就出来了,每个配置的解析逻辑封装在相应的方法中,接下来将重点介绍一些常用的配置,例如properties、settings、environments、typeAliases、typeHandler、mappers。闲话少叙,接下来我们首先来分析下properties的解析过程。

2.3.1 解析properties配置

解析properties节点是由propertiesElement这个方法完成的,该方法的逻辑比较简单。在分析方法源码前,首先我们来看看一个普通的properties配置。

<properties resource="jdbc.properties">
    <property name="jdbc.username" value="coolblog"/>
    <property name="hello" value="world"/>
</properties>

在上面的配置中,我为 properties 节点配置了一个 resource 属性,以及两个子节点。下面我们参照上面的配置,来分析一下 propertiesElement 的逻辑。相关分析如下。

//* XMLConfigBuilder类
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 如果在这些地方,属性多于一个的话,MyBatis按照如下的顺序加载它们:
        // 1.在 properties 元素体内指定的属性首先被读取。
        // 2.从类路径下资源或 properties 元素的 url 属性中加载的属性第二被读取,它会覆盖已经存在的完全一样的属性。
        // 3.作为方法参数传递的属性最后被读取, 它也会覆盖任一已经存在的完全一样的属性,这些属性可能是从 properties 元素体内和资源/url 属性中加载的。通过方法参数传递的传入方式是调用构造函数时传入,public XMLConfigBuilder(Reader reader, String environment, Properties props)

        // 1.XNode.getChildrenAsProperties函数方便得到孩子所有Properties
        Properties defaults = context.getChildrenAsProperties();
        // 2.然后查找resource或者url,加入前面的Properties
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        if (resource != null && url != null) {
            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        if (resource != null) {
          // 从文件系统中加载并解析属性文件
          defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
          // 通过url加载并解析属性文件
          defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // 3.Variables也全部加入Properties
        Properties vars = configuration.getVariables();
        if (vars != null) {
          defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        // 4. 将属性值设置到configuration中
        configuration.setVariables(defaults);
    }
}

/**
 * 得到孩子,返回Properties,孩子的格式肯定都有name,value属性
 * @return
 */
public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    // 获取并遍历子节点
    for (XNode child : getChildren()) {
        // 获取 property 节点的 name 和 value 属性
        String name = child.getStringAttribute("name");
        String value = child.getStringAttribute("value");
        if (name != null && value != null) {
            // 设置属性到属性对象中
            properties.setProperty(name, value);
        }
    }
    return properties;
}

// -☆- XNode
public List<XNode> getChildren() {
    List<XNode> children = new ArrayList<XNode>();
    // 获取子节点列表
    NodeList nodeList = node.getChildNodes();
    if (nodeList != null) {
        for (int i = 0, n = nodeList.getLength(); i < n; i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                // 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中
                children.add(new XNode(xpathParser, node, variables));
            }
        }
    }
    return children;
}

上面是 properties 节点解析的主要过程,不是很复杂。主要包含三个步骤:

  1. 一是解析 properties 节点的子节点,并将解析结果设置到 Properties 对象中。
  2. 二是从文件系统或通过网络读取属性配置,这取决于 properties 节点的 resource 和 url 是否为空。第二步对应的代码比较简单,这里就不分析了。有兴趣的话,大家可以自己去看看。
  3. 最后一步则是将解析出的属性对象设置到 XPathParser 和 Configuration 对象中。

需要注意的是,propertiesElement 方法是先解析 properties 节点的子节点内容,后再从文件系统或者网络读取属性配置,并将所有的属性及属性值都放入到 defaults 属性对象中。这就会存在同名属性覆盖的问题,也就是从文件系统,或者网络上读取到的属性及属性值会覆盖掉 properties 子节点中同名的属性和及值。比如上面配置中的jdbc.properties内容如下:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/myblog?...
jdbc.username=root
jdbc.password=1234

与 properties 子节点内容合并后,结果如下:

如上,原来在<property>子标签配置的jdbc.username值为coolblog,现在被引入的jdbc.properties中的同名属性覆盖为了root。同名属性覆盖的问题需要大家注意一下,总结properties元素的解析顺序是:

  1. 在Properties 元素体内指定的属性首先被读取。
  2. 在类路径下资源或properties元素的url 属性中加载的属性第二个被读取,它会覆盖完全一样的属性
  3. 作为方法参数传递的属性最后被读取,它也会覆盖任一已存在的完全一样的属性,这些属性可能是从properties 元素体内和资源 /url 属性中加载的。通过方法参数传递的传入方式是调用构造函数时传入,public XMLConfigBuilder(Reader reader, String environment, Properties props)

2.3.2 解析settings配置

settings相关配置是MyBatis中非常重要的配置,这些配置用于用户调整MyBatis运行时的行为。settings配置繁多,在对这些配置不熟悉的情况下,保持默认的配置即可。详细的配置说明可以参考MyBatis官方文档setting

我们先看看一个settings 的简单配置:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
</settings>

接下来我们来看看setting的解析源码。

//* XMLConfigBuilder
private void settingsElement(XNode context) throws Exception {
    if (context != null) {
        // 获取settings子节点中的内容
        Properties props = context.getChildrenAsProperties();
        // 创建Configuration类的"元信息"对象
        MetaClass metaConfig = MetaClass.forClass(Configuration.class);
        for (Object key : props.keySet()) {
            // Check that all settings are known to the configuration class
            // 通过metaConfig来检查settings子节是否在Configuration类里都有相应的setter方法(其实就是检查settings子标签是否有拼写错误)
            if (!metaConfig.hasSetter(String.valueOf(key))) {
                throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
            }
        }
    }
    // 如果通过了上面的检查,就将生成好的settings子节点对象返回,会在外部将其赋值给Configuration的settings成员属性
    return props;
}

从上述源码中我们可以总结出setting 的解析主要分为如下几个步骤:

  1. 获取settings 子节点中的内容,这段代码在之前已经解释过,再次不在赘述。
  2. 然后就是创建Configuration类的“元信息”对象,在这一部分中出现了一个陌生的类MetaClass,我们一会再分析。
  3. 接着检查是否在Configuration类里都有相应的setter方法,不存在则抛出异常。
  4. 若通过MetaClass的检测,则会将Properties对象返回,逻辑结束,会在外部将其中的信息设置到configuration对象中。

上述代码看似简单,不过这是一个假象。在上面的代码中出现了一个陌生的类MetaClass,这个类是用来做什么的呢?它是用来解析目标类的一些元信息,比如类的成员变量,getter/setter 方法等。创建元信息对象的过程还是蛮复杂的。接下来我们就来看看MetaClass类。

2.3.2.1 元信息对象(MetaClass)创建过程源码解析

元信息类MetaClass的构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass方法进行创建。它的创建逻辑如下:

public class MetaClass {
	// 反射器工厂类
    private final ReflectorFactory reflectorFactory;

    // 反射器
    // 可以看到方法基本都是再次委派给这个Reflector
    private final Reflector reflector;

    private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
        this.reflectorFactory = reflectorFactory;
        // 根据类型创建 Reflector
        this.reflector = reflectorFactory.findForClass(type);
    }

    public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
        // 调用构造方法
        return new MetaClass(type, reflectorFactory);
    }
    // 省略其他方法
}

上面的代码看起来很简单,不过这只是冰山一角。上面代码出现了两个新的类ReflectorFactoryReflector,MetaClass 通过引入这些新类帮助它完成功能。下面我们看一下MetaClass类的hasSetter方法的源码就知道是怎么回事了。

// -☆- MetaClass
public boolean hasSetter(String name) {
    // 属性分词器,用于解析属性名
    PropertyTokenizer prop = new PropertyTokenizer(name);
    // hasNext 返回 true,则表明 name 是一个复合属性,后面会进行分析
    if (prop.hasNext()) {
        // 调用 reflector 的 hasSetter 方法
        if (reflector.hasSetter(prop.getName())) {
            // 为属性创建创建 MetaClass
            MetaClass metaProp = metaClassForProperty(prop.getName());
            // 再次调用 hasSetter
            return metaProp.hasSetter(prop.getChildren());
        } else {
            return false;
        }
    } else {
        // 调用 reflector 的 hasSetter 方法
        return reflector.hasSetter(prop.getName());
    }
}

从上面的代码中,我们可以看出 MetaClass 中的 hasSetter 方法最终调用了 Reflector 的 hasSetter 方法。关于 Reflector 的 hasSetter 方法,这里先不分析,Reflector 这个类的逻辑较为复杂,本节会在随后进行详细说明。下面来简单介绍一下上面代码中出现的几个类:

  1. ReflectorFactory -> 顾名思义,Reflector 的工厂类,兼有缓存 Reflector 对象的功能
  2. Reflector -> 反射器,用于解析和存储目标类中的元信息
  3. PropertyTokenizer -> 属性名分词器,用于处理较为复杂的属性名

上面的描述比较简单,仅从上面的描述中,还不能让大家有更深入的理解。所以下面单独分析一下这几个类的逻辑,首先是ReflectorFactory。ReflectorFactory 是一个接口,MyBatis 中目前只有一个实现类DefaultReflectorFactory,它的分析如下:

2.3.2.1.1 DefaultReflectorFactory 源码分析

DefaultReflectorFactory 用于创建 Reflector,同时兼有缓存的功能,它的源码如下。

public class DefaultReflectorFactory implements ReflectorFactory {
    private boolean classCacheEnabled = true;
    /** 
     * 目标类和反射器映射缓存 
     * 用来快速通过类对象去找到对应的反射器
    */
    private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<Class<?>, Reflector>();
    // 省略部分代码
    public Reflector findForClass(Class<?> type) {
        // classCacheEnabled 默认为 true
        if (classCacheEnabled) {
            // 从缓存中获取 Reflector 对象
            Reflector cached = reflectorMap.get(type);
            // 缓存为空,则创建一个新的 Reflector 实例,并放入缓存中
            if (cached == null) {
                cached = new Reflector(type);
                // 将 <type, cached> 映射缓存到 map 中,方便下次取用
                reflectorMap.put(type, cached);
            }
            return cached;
        } else {
            // 创建一个新的 Reflector 实例
            return new Reflector(type);
        }
    }
}

如上,DefaultReflectorFactory 的findForClass方法逻辑不是很复杂,包含两个访问操作,和一个对象创建操作。代码注释的比较清楚了,就不多说了。接下来,来分析一下反射器 Reflector。

 
2.3.2.1.2 Reflector 源码分析

本小节,我们来看一下 Reflector 的源码。Reflector 这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用。Reflector 本身代码比较多,这里不能一一分析。本小节,我将会分析三部分逻辑,分别如下:

  1. Reflector 构造方法及成员变量分析
  2. getter 方法解析过程
  3. setter 方法解析过程

下面我们按照这个步骤进行分析,先来分析 Reflector 构造方法。

Reflector 构造方法及成员变量分析

Reflector 构造方法中包含了很多初始化逻辑,目标类的元信息解析过程也是在构造方法中完成的,这些元信息最终会被保存到 Reflector 的成员变量中。下面我们先来看看 Reflector 的构造方法和相关的成员变量定义,代码如下:

public class Reflector {
    private final Class<?> type;
    private final String[] readablePropertyNames;
    private final String[] writeablePropertyNames;
    private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
    private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
    private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
    private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
    private Constructor<?> defaultConstructor;
    private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();

    public Reflector(Class<?> clazz) {
        type = clazz;
        // 解析目标类的默认构造方法,并赋值给 defaultConstructor 变量
        addDefaultConstructor(clazz);
        // 解析 getter 方法,并将解析结果放入 getMethods 中
        addGetMethods(clazz);
        // 解析 setter 方法,并将解析结果放入 setMethods 中
        addSetMethods(clazz);
        // 解析属性字段,并将解析结果添加到 setMethods 或 getMethods 中
        addFields(clazz);
        // 从 getMethods 映射中获取可读属性名数组
        readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
        // 从 setMethods 映射中获取可写属性名数组
        writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);

        // 将所有属性名的大写形式作为键,属性名作为值,存入到 caseInsensitivePropertyMap 中
        for (String propName : readablePropertyNames) {
            caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
        }
        for (String propName : writeablePropertyNames) {
            caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
        }
    }
    // 省略其他方法
}

如上,Reflector 的构造方法看起来略为复杂,不过好在一些比较复杂的逻辑都封装在了相应的方法中,这样整体的逻辑就比较清晰了。Reflector 构造方法所做的事情均已进行了注释,大家对照着注释先看一下。相关方法的细节待会会进行分析。看完构造方法,下面我来通过表格的形式,列举一下 Reflector 部分成员变量的用途。如下:

变量名

类型

用途

readablePropertyNames

String[]

可读属性名称数组,用于保存 getter 方法对应的属性名称

writeablePropertyNames

String[]

可写属性名称数组,用于保存 setter 方法对应的属性名称

setMethods

Map<String, Invoker>

用于保存属性名称到 Invoke 的映射。setter 方法会被封装到 MethodInvoker 对象中,Invoke 实现类比较简单,大家自行分析

getMethods

Map<String, Invoker>

用于保存属性名称到 Invoke 的映射。同上,getter 方法也会被封装到 MethodInvoker 对象中

setTypes

Map<String, Class<?>>

用于保存 setter 对应的属性名与参数类型的映射

getTypes

Map<String, Class<?>>

用于保存 getter 对应的属性名与返回值类型的映射

caseInsensitivePropertyMap

Map<String, String>

用于保存大写属性名与属性名之间的映射,比如 <NAME, name>

上面列举了一些集合变量,这些变量用于缓存各种元信息。关于这些变量,这里描述的不太好懂,主要是不太好解释。要想了解这些变量更多的细节,还是要深入到源码中。所以我们成热打铁,继续往下分析。

getter 方法解析过程

getter 方法解析的逻辑被封装在了addGetMethods方法中,这个方法除了会解析形如getXXX的方法,同时也会解析isXXX方法。该方法的源码分析如下:

private void addGetMethods(Class<?> cls) {
    Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
    // 获取当前类、接口以及父类中的所有方法。该方法逻辑不是很复杂,这里就不展开了
    Method[] methods = getClassMethods(cls);
    for (Method method : methods) {
        // getter 方法不应该有参数,若存在参数,则忽略当前方法
        if (method.getParameterTypes().length > 0) {
            continue;
        }
        String name = method.getName();
        // 过滤出方法名以 get 或 is 开头的方法
        if ((name.startsWith("get") && name.length() > 3)
            || (name.startsWith("is") && name.length() > 2)) {
            // 将 getXXX 或 isXXX 等方法名转成相应的属性,比如 getName -> name
            name = PropertyNamer.methodToProperty(name);
            /*
             * 将冲突的方法添加到 conflictingGetters 中。考虑这样一种情况:
             * 
             * getTitle 和 isTitle 两个方法经过 methodToProperty 处理,
             * 均得到 name = title,这会导致冲突,我们不知道这两个方法到底要解析哪个方法的信息存储到getMethods和getTypes中。
             *
             * 对于冲突的方法,这里先统一起存起来,后续再解决冲突,解决冲突就是在两个冲突的方法中,选一个方法去解析
             */
            addMethodConflict(conflictingGetters, name, method);
        }
    }
    // 解决 getter 冲突
    resolveGetterConflicts(conflictingGetters);
}

如上,addGetMethods 方法的执行流程如下:

  1. 获取当前类,接口,以及父类中的方法
  2. 遍历上一步获取的方法数组,并过滤出以get和is开头的方法
  3. 将方法名转换成相应的属性名
  4. 将属性名和方法对象添加到冲突集合中
  5. 解决冲突

在上面的执行流程中,前三步比较简单,大家自行分析吧。第4步也不复杂,下面我会把源码贴出来,大家看一下就能懂。在这几步中,第5步逻辑比较复杂,我们知道getter截取属性冲突主要是由于 getXXX() 和isXXX() 两种类型的方法,截取属性后会冲突,这一步逻辑我们重点关注一下。下面继续看源码吧。

/** 添加属性名和方法对象到冲突集合中 */
private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
    List<Method> list = conflictingMethods.get(name);
    if (list == null) {
        list = new ArrayList<Method>();
        conflictingMethods.put(name, list);
    }
    list.add(method);
}
    
/** 解决冲突 */
private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
    for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
        Method winner = null;
        String propName = entry.getKey();
        for (Method candidate : entry.getValue()) {
            if (winner == null) {
                winner = candidate;
                continue;
            }
            // 获取返回值类型
            Class<?> winnerType = winner.getReturnType();
            Class<?> candidateType = candidate.getReturnType();
            /* 
             * 两个方法的返回值类型一致,若两个方法返回值类型均为 boolean,则选取 isXXX 方法
             * 为 winner。否则无法决定哪个方法更为合适,只能抛出异常
             */
            if (candidateType.equals(winnerType)) {
                if (!boolean.class.equals(candidateType)) {
                    throw new ReflectionException(
                        "Illegal overloaded getter method with ambiguous type for property "
                            + propName + " in class " + winner.getDeclaringClass()
                            + ". This breaks the JavaBeans specification and can cause unpredictable results.");
                /*
                 * 如果方法返回值类型为 boolean,且方法名以 "is" 开头,
                 * 则认为候选方法 candidate 更为合适
                 */
                } else if (candidate.getName().startsWith("is")) {
                    winner = candidate;
                }
            /*
             * winnerType 是 candidateType 的子类,类型上更为具体,
             * 则认为当前的 winner 仍是合适的,无需做什么事情
             */
            } else if (candidateType.isAssignableFrom(winnerType)) {
            /*
             * candidateType 是 winnerType 的子类,此时认为 candidate 方法更为合适,
             * 故将 winner 更新为 candidate
             */
            } else if (winnerType.isAssignableFrom(candidateType)) {
                winner = candidate;
            } else {
                throw new ReflectionException(
                    "Illegal overloaded getter method with ambiguous type for property "
                        + propName + " in class " + winner.getDeclaringClass()
                        + ". This breaks the JavaBeans specification and can cause unpredictable results.");
            }
        }
        // 将筛选出的方法添加到 getMethods 中,并将方法返回值添加到 getTypes 中
        addGetMethod(propName, winner);
    }
}

private void addGetMethod(String name, Method method) {
    if (isValidPropertyName(name)) {
        getMethods.put(name, new MethodInvoker(method));
        // 解析返回值类型
        Type returnType = TypeParameterResolver.resolveReturnType(method, type);
        // 将返回值类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes 中
        getTypes.put(name, typeToClass(returnType));
    }
}

以上就是解除冲突的过程,代码有点长,不太容易看懂。这里大家只要记住解决冲突的规则即可理解上面代码的逻辑。相关规则如下:

  1. 冲突方法的返回值类型具有继承关系,子类返回值对应的方法被认为是更合适的选择
  2. 冲突方法的返回值类型相同,如果返回值类型为boolean,那么以is开头的方法则是更合适的方法
  3. 冲突方法的返回值类型相同,但返回值类型非boolean,此时出现歧义,抛出异常
  4. 冲突方法的返回值类型不相关,无法确定哪个是更好的选择,此时直接抛异常

分析完 getter 方法的解析过程,下面继续分析 setter 方法的解析过程。

setter 方法解析过程

与 getter 方法解析过程相比,setter 方法的解析过程与此有一定的区别。主要体现在冲突出现的原因,以及冲突的解决方法上。那下面,我们深入源码来找出两者之间的区别。

private void addSetMethods(Class<?> cls) {
    Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
    // 获取当前类、接口以及父类中的所有方法。该方法逻辑不是很复杂,这里就不展开了
    Method[] methods = getClassMethods(cls);
    for (Method method : methods) {
        String name = method.getName();
        // 过滤出 setter 方法(方法名以set开头),且方法仅有一个参数
        if (name.startsWith("set") && name.length() > 3) {
            if (method.getParameterTypes().length == 1) {
                name = PropertyNamer.methodToProperty(name);
                /*
                 * setter 方法发生冲突原因是:可能存在重载情况,比如:
                 *     void setSex(int sex);
                 *     void setSex(SexEnum sex);
                 */
                addMethodConflict(conflictingSetters, name, method);
            }
        }
    }
    // 解决 setter 冲突
    resolveSetterConflicts(conflictingSetters);
}

如上,与addGetMethods 方法的执行流程类似,addSetMethods方法的执行流程也分为如下几个步骤:

  1. 获取当前类,接口,以及父类中的方法
  2. 过滤出setter方法其方法之后一个参数
  3. 获取方法对应的属性名
  4. 将属性名和其方法对象放入冲突集合中
  5. 解决setter冲突

前四步相对而言比较简单,我在此处就不展开分析了,我们来重点分析下解决setter冲突的逻辑。

从上面的代码和注释中,我们可知道 setter 方法之间出现冲突的原因。即方法存在重载,方法重载导致methodToProperty方法解析出的属性名完全一致。而 getter 方法之间出现冲突的原因是getXXXisXXX对应的属性名一致。既然冲突发生了,要进行调停,那接下来继续来看看调停冲突的逻辑。

private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
    for (String propName : conflictingSetters.keySet()) {
        List<Method> setters = conflictingSetters.get(propName);
        /*
         * 获取 getter 方法的返回值类型,由于 getter 方法不存在重载的情况,
         * 所以可以用它的返回值类型反推哪个 setter 的更为合适
         */
        Class<?> getterType = getTypes.get(propName);
        Method match = null;
        ReflectionException exception = null;
        for (Method setter : setters) {
            // 获取参数类型
            Class<?> paramType = setter.getParameterTypes()[0];
            if (paramType.equals(getterType)) {
                // 参数类型和返回类型一致,则认为是最好的选择,并结束循环
                match = setter;
                break;
            }
            if (exception == null) {
                try {
                    // 选择一个更为合适的方法
                    match = pickBetterSetter(match, setter, propName);
                } catch (ReflectionException e) {
                    match = null;
                    exception = e;
                }
            }
        }
        // 若 match 为空,表示没找到更为合适的方法,此时抛出异常
        if (match == null) {
            throw exception;
        } else {
            // 将筛选出的方法放入 setMethods 中,并将方法参数值添加到 setTypes 中
            addSetMethod(propName, match);
        }
    }
}

/** 从两个 setter 方法中选择一个更为合适方法 */
private Method pickBetterSetter(Method setter1, Method setter2, String property) {
    if (setter1 == null) {
        return setter2;
    }
    Class<?> paramType1 = setter1.getParameterTypes()[0];
    Class<?> paramType2 = setter2.getParameterTypes()[0];
    // 如果参数2可赋值给参数1,即参数2是参数1的子类,则认为参数2对应的 setter 方法更为合适
    if (paramType1.isAssignableFrom(paramType2)) {
        return setter2;
        
    // 这里和上面情况相反
    } else if (paramType2.isAssignableFrom(paramType1)) {
        return setter1;
    }
    
    // 两种参数类型不相关,这里抛出异常
    throw new ReflectionException("Ambiguous setters defined for property '" + property + "' in class '"
        + setter2.getDeclaringClass() + "' with types '" + paramType1.getName() + "' and '"
        + paramType2.getName() + "'.");
}

private void addSetMethod(String name, Method method) {
    if (isValidPropertyName(name)) {
        setMethods.put(name, new MethodInvoker(method));
        // 解析参数类型列表
        Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
        // 将参数类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes
        setTypes.put(name, typeToClass(paramTypes[0]));
    }
}

关于 setter 方法冲突的解析规则,这里也总结一下吧。如下:

  1. 冲突方法的参数类型与 getter 的返回类型一致,则认为是最好的选择
  2. 冲突方法的参数类型具有继承关系,子类参数对应的方法被认为是更合适的选择
  3. 冲突方法的参数类型不相关,无法确定哪个是更好的选择,此时直接抛异常

到此关于 setter 方法的解析过程就说完了。我在前面说过 MetaClass 的hasSetter最终调用了 Refactor 的hasSetter方法,那么现在是时候分析 Refactor 的hasSetter方法了。代码如下如下:

public boolean hasSetter(String propertyName) {
    return setMethods.keySet().contains(propertyName);
}

代码如上,就两行,很简单,就是判断是否存在propertyName这个成员属性的setter方法。

2.3.2.1.3 PropertyTokenizer 源码分析

PropertyTokenizer类的主要作用是对复合属性进行分解。

对于较为复杂的属性,需要进行进一步解析才能使用。那什么样的属性是复杂属性呢?来看个测试代码就知道了。

public class MetaClassTest {
    private class Author {
        private Integer id;
        private String name;
        private Integer age;
        /** 一个作者对应多篇文章 */
        private Article[] articles;
        // 省略 getter/setter
    }

    private class Article {
        private Integer id;
        private String title;
        private String content;
        /** 一篇文章对应一个作者 */
        private Author author;
        // 省略 getter/setter
    }
    
    public void testHasSetter() {
        // 为 Author 创建元信息对象
        MetaClass authorMeta = MetaClass.forClass(Author.class, new DefaultReflectorFactory());
        System.out.println("------------☆ Author ☆------------");
        System.out.println("id -> " + authorMeta.hasSetter("id"));
        System.out.println("name -> " + authorMeta.hasSetter("name"));
        System.out.println("age -> " + authorMeta.hasSetter("age"));
        // 检测 Author 中是否包含 Article[] 的 setter
        System.out.println("articles -> " + authorMeta.hasSetter("articles"));
        System.out.println("articles[] -> " + authorMeta.hasSetter("articles[]"));
        System.out.println("title -> " + authorMeta.hasSetter("title"));
        // 为 Article 创建元信息对象
        MetaClass articleMeta = MetaClass.forClass(Article.class, new DefaultReflectorFactory());
        System.out.println("\n------------☆ Article ☆------------");
        System.out.println("id -> " + articleMeta.hasSetter("id"));
        System.out.println("title -> " + articleMeta.hasSetter("title"));
        System.out.println("content -> " + articleMeta.hasSetter("content"));
        // 下面两个均为复杂属性,分别检测 Article 类中的 Author 类是否包含 id 和 name 的 setter 方法
        System.out.println("author.id -> " + articleMeta.hasSetter("author.id"));
        System.out.println("author.name -> " + articleMeta.hasSetter("author.name"));
    }
}

如上,Article类中包含了一个Author引用。然后我们调用 articleMeta 的 hasSetter 检测author.idauthor.name属性是否存在,我们的期望结果为 true。测试结果如下:

如上,标记⑤处的输出均为 true,我们的预期达到了。标记②处检测 Article 数组的是否存在 setter 方法,结果也均为 true。这说明 PropertyTokenizer 对数组和复合属性均进行了处理。那它是如何处理的呢?答案如下:

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
    private String name;
    private final String indexedName;
    private String index;
    private final String children;

    public PropertyTokenizer(String fullname) {
        // 检测传入的参数中是否包含字符 '.'
        int delim = fullname.indexOf('.');
        if (delim > -1) {
            /*
             * 以点位为界,进行分割。比如:
             *    fullname = www.coolblog.xyz
             *
             * 以第一个点为分界符:
             *    name = www
             *    children = coolblog.xyz
             */ 
            name = fullname.substring(0, delim);
            children = fullname.substring(delim + 1);
        } else {
            // fullname 中不存在字符 '.'
            name = fullname;
            children = null;
        }
        indexedName = name;
        // 检测传入的参数中是否包含字符 '['
        delim = name.indexOf('[');
        if (delim > -1) {
            /*
             * 获取中括号里的内容,比如:
             *   1. 对于数组或List集合:[] 中的内容为数组下标,
             *      比如 fullname = articles[1],index = 1
             *   2. 对于Map:[] 中的内容为键,
             *      比如 fullname = xxxMap[keyName],index = keyName
             *
             * 关于 index 属性的用法,可以参考 BaseWrapper 的 getCollectionValue 方法
             */
            index = name.substring(delim + 1, name.length() - 1);
            // 获取分解符前面的内容,比如 fullname = articles[1],name = articles
            name = name.substring(0, delim);
        }
    }
    // 省略 getter
    
    public boolean hasNext() {
        return children != null;
    }
    
    public PropertyTokenizer next() {
        // 对 children 进行再次切分,用于解析多重复合属性
        return new PropertyTokenizer(children);
    }
    // 省略部分方法
}

以上是 PropertyTokenizer 的源码分析,注释的比较多,应该分析清楚了。

PropertyTokenizer类的核心逻辑就在其构造器中,主要包括三部分逻辑:

  1. 根据 ‘.’,如果不能找到则取全部部分
  2. 能找到的话则首先截取 ’ .’ 符号之前的部分,把其余部分作为children。 然后通过MetaClass类的getGetterType的方法来循环提取。

2.3.2.2 小结

本节的篇幅比较大,大家看起来应该蛮辛苦的。本节为了分析 MetaClass 的 hasSetter 方法,把这个方法涉及到的源码均分析了一遍。其实,如果想简单点分析,我可以直接把 MetaClass 当成一个黑盒,然后用一句话告诉大家 hasSetter 方法有什么用即可。但是这样做我觉的文章太虚,没什么深度。关于 MetaClass 及相关源码大家第一次看可能会有点吃力,看不懂可以先放一放。后面多看几遍,动手写点测试代码调试一下,可以帮助理解。

好了,关于 setting 节点的解析过程就先分析到这里,我们继续往下分析。

2.3.3 设置 settings 配置到 Configuration

上一节讲了 settings 配置的解析过程,这些配置解析出来要有一个存放的地方,以使其他代码可以找到这些配置。这个存放地方就是 Configuration 对象,本节就来看一下这将 settings 配置设置到 Configuration 对象中的过程。如下:

private void settingsElement(Properties props) throws Exception {
    // 设置 autoMappingBehavior 属性,默认值为 PARTIAL
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    // 设置 cacheEnabled 属性,默认值为 true
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    // 省略部分代码
    // 解析默认的枚举处理器
    Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
    // 设置默认枚举处理器
    configuration.setDefaultEnumTypeHandler(typeHandler);
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    
    // 省略部分代码
}

上面代码处理调用了很多 Configuration 的 setter 方法,就没太多逻辑了。这里来看一下上面出现的一个调用resolveClass,它的源码如下:

// -☆- BaseBuilder类
protected Class<?> resolveClass(String alias) {
    if (alias == null) {
        return null;
    }
    try {
        // 通过别名解析
        return resolveAlias(alias);
    } catch (Exception e) {
        throw new BuilderException("Error resolving class. Cause: " + e, e);
    }
}
protected final TypeAliasRegistry typeAliasRegistry;
protected Class<?> resolveAlias(String alias) {
    // 通过别名注册器解析别名对于的类型 Class
    return typeAliasRegistry.resolveAlias(alias);
}

这里出现了一个新的类TypeAliasRegistry,大家对于它可能会觉得陌生,但是对于typeAlias应该不会陌生。TypeAliasRegistry 的用途就是将别名和类型进行映射,这样就可以用别名表示某个类了,方便使用。既然聊到了别名,那下面我们不妨看看别名的配置的解析过程。

2.3.4 解析 typeAliases 配置

在 MyBatis 中,可以为我们自己写的有些类定义一个别名。这样在使用的时候,我们只需要输入别名即可,无需再把全限定的类名写出来。

该配置主要是减少在映射文件中填写全限定名的冗余。

在 MyBatis 中,我们有两种方式进行别名配置。第一种是仅配置包名,让 MyBatis 去扫描包中的类型,并根据类型得到相应的别名。这种方式可配合 @Alias 注解使用,即通过注解为某个类配置别名,而不是让 MyBatis 按照默认规则生成别名。这种方式的配置如下:

<typeAliases>
    <package name="xyz.coolblog.model1"/>
    <package name="xyz.coolblog.model2"/>
</typeAliases>

第二种方式是通过手动的方式,明确为某个类型配置别名。这种方式的配置如下:

<typeAliases>
    <typeAlias alias="article" type="xyz.coolblog.model.Article" />
    <typeAlias type="xyz.coolblog.model.Author" />
</typeAliases>

对比这两种方式,第一种自动扫描的方式配置起来比较简单,缺点也不明显。唯一能想到缺点可能就是 MyBatis 会将某个包下所有符合要求的类的别名都解析出来,并形成映射关系。如果你不想让某些类被扫描,这个好像做不到,没发现 MyBatis 提供了相关的排除机制。不过我觉得这并不是什么大问题,最多是多解析并缓存了一些别名到类型的映射,在时间和空间上产生了一些额外的消耗而已。当然,如果无法忍受这些消耗,可以使用第二种配置方式,通过手工的方式精确配置某些类型的别名。不过这种方式比较繁琐,特别是配置项比较多时。至于两种方式怎么选择,这个看具体的情况了。配置项非常少时,两种皆可。比较多的话,还是让 MyBatis 自行扫描吧。

以上介绍了两种不同的别名配置方式,下面我们来看一下两种不同的别名配置是怎样解析的。代码如下:

// -☆- XMLConfigBuilder类
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // ⭐️ 从指定的包中解析别名和类型的映射
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
                
            // ⭐️ 从 typeAlias 节点中解析别名和类型的映射
            } else {
                // 获取 alias 和 type 属性值,alias 不是必填项,可为空
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    // 加载 type 对应的类型
                    Class<?> clazz = Resources.classForName(type);
                    // 注册别名到类型的映射
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) {
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

如上,上面的代码通过一个if-else条件分支来处理两种不同的配置,这里我用⭐️标注了出来。该入口程序方法执行流程如下:

  1. 根据节点名称判断是否是package,如果是的话则调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)
  2. 如果不是的话,则进入另外一个分支,则根据Class名字来注册类型别名。

下面我们来分别看一下这两种配置方式的解析过程,首先来看一下手动配置方式的解析过程。

2.3.4.1 typeAlias 节点中解析并注册别名

在别名的配置中,type属性是必须要配置的,而alias属性则不是必须的。这个在配置文件的 DTD 中有规定。如果使用者未配置 alias 属性,则需要 MyBatis 自行为目标类型生成别名。对于别名为空的情况,注册别名的任务交由void registerAlias(Class<?>)方法处理。若不为空,则由void registerAlias(String, Class<?>)进行别名注册。这两个方法的分析如下:

// 别名映射    别名 -> 类对象
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();

// 没有自己设置别名
public void registerAlias(Class<?> type) {
    // 获取全路径类名的简称
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    // 如果这个类使用了@Alias注解,就用注解上设定的值作为别名
    if (aliasAnnotation != null) {
        // 从注解中取出别名
        alias = aliasAnnotation.value();
    }
    // 调用重载方法注册别名和类型映射
    registerAlias(alias, type);
}

// 自己设置了别名
public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // 将别名转成小写
    String key = alias.toLowerCase(Locale.ENGLISH);
    /*
     * 如果 TYPE_ALIASES 中存在了某个类型映射,这里判断当前类型与映射中的类型是否一致,
     * 不一致则抛出异常,不允许一个别名对应两种类型
     */
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
        throw new TypeException(
            "The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    // 缓存别名到类型映射
    TYPE_ALIASES.put(key, value);
}

如上,若用户为明确配置 alias 属性,MyBatis 会使用类名的小写形式作为别名。比如,全限定类名xyz.coolblog.model.Author的别名为author。若类中有@Alias注解,则从注解中取值作为别名。

上面的代码不是很复杂,注释的也比较清楚了,就不多说了。继续往下看。

2.3.4.2 从指定的包中解析并注册别名

从指定的包中解析并注册别名过程主要由别名的解析和注册两步组成。下面来看一下相关代码:

public void registerAliases(String packageName) {
    // 调用重载方法注册别名
    registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    /*
     * 查找某个包下的父类为 superType 的类。从调用栈来看,这里的 
     * superType = Object.class,所以 ResolverUtil 将查找所有的类。
     * 查找完成后,查找结果将会被缓存到内部集合中。
     */ 
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 获取查找结果
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for (Class<?> type : typeSet) {
        // 忽略匿名类,接口,内部类
        if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
            // 为类型注册别名 
            registerAlias(type);
        }
    }
}

上面的代码不多,相关流程也不复杂,可简单总结为下面两个步骤:

  1. 通过ResolverUtil的find方法找到该包下所有的类,传入的父类是Object
  1. 循环注册别名,只有非匿名类及非接口及内部类及非成员类才能注册。注册别名最终还是调用registerAlias(alias, type)完成的。

在这两步流程中,第2步流程对应的代码上一节已经分析过了,这里不再赘述。第1步的功能理解起来不难,但是背后对应的代码有点多。限于篇幅原因,这里简单说一下ResolverUtil查找包下的所有类的源码:

// 主要的方法,找一个package下满足条件的所有类,被TypeHanderRegistry,MapperRegistry,TypeAliasRegistry调用
public ResolverUtil<T> find(Test test, String packageName) {
    String path = getPackagePath(packageName);
    try {
        // 通过VFS来深入jar包里面去找一个class
        List<String> children = VFS.getInstance().list(path);
        for (String child : children) {
        if (child.endsWith(".class")) {
            // 将.class的class对象放入Set集合中,供后面调用
            addIfMatching(test, child);
        }
        }
    } catch (IOException ioe) {
        log.error("Could not read package: " + packageName, ioe);
    }
    return this;
}

简单的流程总结。如下:

  1. 通过 VFS(虚拟文件系统)获取指定包下的所有文件的路径名,
    比如xyz/coolblog/model/Article.class
  2. 筛选以.class结尾的文件名
  3. 将路径名转成全限定的类名,通过类加载器加载类名
  4. 对类型进行匹配,若符合匹配规则,则将其放入内部集合中

以上就是类型资源查找的过程,并不是很复杂,大家有兴趣自己看看吧。

2.3.4.3 注册 MyBatis 内部类及常见类型的别名

最后,我们来看一下一些 MyBatis 内部类及一些常见类型的别名注册过程。如下:

// -☆- Configuration构造方法
public Configuration() {
    // 注册事务工厂的别名
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    // 省略部分代码,下同
    // 注册数据源的别名
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    // 注册缓存策略的别名
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    // 注册日志类的别名
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    // 注册动态代理工厂的别名
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
}

// -☆- TypeAliasRegistry构造方法
public TypeAliasRegistry() {
    // 注册 String 的别名
    registerAlias("string", String.class);
    // 注册基本类型包装类的别名
    registerAlias("byte", Byte.class);
    // 省略部分代码,下同
    // 注册基本类型包装类数组的别名
    registerAlias("byte[]", Byte[].class);
    
    // 注册基本类型的别名
    registerAlias("_byte", byte.class);
    // 注册基本类型包装类的别名
    registerAlias("_byte[]", byte[].class);
    // 注册 Date, BigDecimal, Object 等类型的别名
    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("object", Object.class);
    // 注册 Date, BigDecimal, Object 等数组类型的别名
    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("object[]", Object[].class);
    // 注册集合类型的别名
    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);
    // 注册 ResultSet 的别名
    registerAlias("ResultSet", ResultSet.class);
}

以上就是别名解析的全部流程。

2.3.5 解析 plugins 配置

插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现Interceptor接口。然后在插件类上添加@Intercepts和@Signature注解,用于指定想要拦截的目标方法。MyBatis 允许拦截下面接口中的一些方法:

  • Executor: update 方法,query 方法,flushStatements 方法,commit 方法,rollback 方法, getTransaction 方法,close 方法,isClosed 方法
  • ParameterHandler: getParameterObject 方法,setParameters 方法
  • ResultSetHandler: handleResultSets 方法,handleOutputParameters 方法
  • StatementHandler: prepare 方法,parameterize 方法,batch 方法,update 方法,query 方法

比较常见的插件有分页插件、分表插件等,有兴趣的朋友可以去了解下。本节我们来分析一下插件的配置的解析过程,先来了解插件的配置。如下:

<plugins>
    <plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
        <property name="key" value="value"/>
    </plugin>
</plugins>

 

解析过程分析如下:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            // 获取配置信息
            Properties properties = child.getChildrenAsProperties();
            // 解析拦截器的类型,并创建拦截器
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // 设置属性
            interceptorInstance.setProperties(properties);
            // 添加拦截器到 Configuration 中
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

 如上,插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到 Configuration 中。好了,关于插件配置的分析就先到这,继续往下分析。

2.3.6 解析 environments 配置

在 MyBatis 中,事务管理器和数据源是配置在 environments 中的。它们的配置大致如下:

<!-- 设置一个默认的连接环境信息 -->
<environments default="development">
    <!--连接环境信息,取一个任意唯一的名字 -->
    <environment id="development">
        <!-- mybatis使用jdbc事务管理方式 -->
        <transactionManager type="JDBC">
            <property name="..." value="..."/>
        </transactionManager>

        <!-- mybatis使用连接池方式来获取数据源连接 -->
        <dataSource type="POOLED">
            <!-- 配置数据源的4个必要属性 -->
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>

如上,配置了连接环境信息,我们心中肯定会有个疑问,${} 这种参数是如何解析的?我一会再分析。

下面我们就来看看这个配置的解析过程。

对照上面的配置进行分析,如下:

private String environment;
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 获取 default 属性
            environment = context.getStringAttribute("default");
        }
        // 循环比较id是否就是指定的environment
        for (XNode child : context.getChildren()) {
            // 获取 id 属性
            String id = child.getStringAttribute("id");
            /*
             * 检测当前 environment 节点的 id 与其父节点 environments 的属性 default 
             * 内容是否一致,一致则返回 true,否则返回 false
             */
            if (isSpecifiedEnvironment(id)) {
                // 1、解析 transactionManager 节点,创建事务工厂TransactionFactory。逻辑和插件的解析逻辑很相似,不在赘述
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 2、解析 dataSource 节点,创建数据源。逻辑和插件的解析逻辑很相似,不在赘述
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                // 3、创建 DataSource 对象
                DataSource dataSource = dsFactory.getDataSource();
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                // 构建 Environment 对象,并设置到 configuration 中
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

如上,解析environments 的流程有三个:

  1. 创建事务工厂TransactionFactory
  2. 创建数据源
  3. 创建Environment对象

我们看看第一步和第二步的代码。

//* XMLConfigBuilder
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 根据type="JDBC"解析返回适当的TransactionFactory
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
}

protected Class<?> resolveClass(String alias) {
    if (alias == null) {
        return null;
    }
    try {
        return resolveAlias(alias);
    } catch (Exception e) {
        throw new BuilderException("Error resolving class. Cause: " + e, e);
    }
}
//*Configuration
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);

JDBC 通过别名解析器解析之后会得到JdbcTransactionFactory工厂实例。

数据源的解析与此类似最终得到的是PooledDataSourceFactory工厂实例,这里就不再赘述解析数据源的源码了。

下面我们来看看之前说过的类似${driver}的解析。其实是通过PropertyParser的parse来处理的。下面我们来看个时序图。

这里最核心的就是第五步,我们来看看源码

public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
}
// 就是一个map,用相应的value替换key
private static class VariableTokenHandler implements TokenHandler {
    private Properties variables;
    public VariableTokenHandler(Properties variables) {
        this.variables = variables;
    }
    @Override
    public String handleToken(String content) {
        if (variables != null && variables.containsKey(content)) {
        return variables.getProperty(content);
        }
        return "${" + content + "}";
    }
}

如上,在VariableTokenHandler 会将${driver} 作为key,其需要被替换的值作为value。传入GenericTokenParser中。然后通过GenericTokenParser 类的parse进行替换。

至此,我们environments配置就解析完了。

2.3.7 解析 typeHandlers 配置

在向数据库存储或读取数据时,我们需要将数据库字段类型和 Java 类型进行一个转换。比如数据库中有CHAR和VARCHAR等类型,但 Java 中没有这些类型,不过 Java 有String类型。所以我们在从数据库中读取 CHAR 和 VARCHAR 类型的数据时,就可以把它们转成 String 。在 MyBatis 中,数据库类型和 Java 类型之间的转换任务是委托给类型处理器TypeHandler去处理的。MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。这里我就不演示自定义类型处理器的编写方法了,没用过或者不熟悉的同学可以 MyBatis 官方文档,或者看这一篇MyBatis自定义通用类型处理器的实现与自动注册文章中写的示例。

下面,我们来看一下类型处理器的配置方法:

<!-- 自动扫描 -->
<typeHandlers>
    <package name="xyz.coolblog.handlers"/>
</typeHandlers>

<!-- 手动配置 -->
<typeHandlers>
    <typeHandler jdbcType="TINYINT"
            javaType="xyz.coolblog.constant.ArticleTypeEnum"
            handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>

使用自动扫描的方式注册类型处理器时,应使用@MappedTypes@MappedJdbcTypes注解配置javaTypejdbcType。关于注解,这里就不演示了,比较简单,大家自行尝试。下面开始分析代码。

private void typeHandlerElement(XNode parent) throws Exception {
  if (parent != null) {
      for (XNode child : parent.getChildren()) {
          // 从指定的包中注册 TypeHandler
          if ("package".equals(child.getName())) {
              String typeHandlerPackage = child.getStringAttribute("name");
              // 注册方法 ①
              typeHandlerRegistry.register(typeHandlerPackage);
          // 从 typeHandler 节点中解析别名到类型的映射
          } else {
              // 获取 javaType,jdbcType 和 handler 等属性值
              String javaTypeName = child.getStringAttribute("javaType");
              String jdbcTypeName = child.getStringAttribute("jdbcType");
              String handlerTypeName = child.getStringAttribute("handler");
              // 解析上面获取到的属性值
              Class<?> javaTypeClass = resolveClass(javaTypeName);
              JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
              Class<?> typeHandlerClass = resolveClass(handlerTypeName);
              // 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
              if (javaTypeClass != null) {
                  if (jdbcType == null) {
                      // 注册方法 ②
                      typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                  } else {
                      // 注册方法 ③
                      typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                  }
              } else {
                  // 注册方法 ④
                  typeHandlerRegistry.register(typeHandlerClass);
              }
          }
      }
  }
}

上面代码中用于解析 XML 部分的代码比较简单,没什么需要特别说明的。除此之外,上面的代码中调用了4个不同的类型处理器注册方法。这些注册方法的逻辑不难理解,但是重载方法很多,上面调用的注册方法只是重载方法的一部分。由于重载太多且重载方法之间互相调用,导致这一块的代码有点凌乱。我一开始在整理这部分代码时,也很抓狂。后来没辙了,把重载方法的调用图画了出来,才理清了代码。一图胜千言,看图吧。

在上面的调用图中,每个蓝色背景框下都有一个标签。每个标签上面都已一个编号,这些编号与上面代码中的标签是一致的。这里我把蓝色背景框内的方法称为开始方法红色背景框内的方法称为终点方法白色背景框内的方法称为中间方法,这些方法都是TypeHandlerRegistry类的方法。下面我会分析从每个开始方法向下分析,为了避免冗余分析,我会按照③ → ② → ④ → ①的顺序进行分析。大家在阅读代码分析时,可以参照上面的图片,辅助理解。好了,下面开始进行分析。

2.3.7.1 register(Class, JdbcType, Class) 方法分析

当代码执行到此方法时,表示javaTypeClass != null && jdbcType != null条件成立,即使用者明确配置了javaTypejdbcType属性的值。那下面我们来看一下该方法的分析。

public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
    // 调用终点方法
    register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
}

/** 类型处理器注册过程的终点 */
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        // 建立JdbcType 到 TypeHandler 的映射
        Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<JdbcType, TypeHandler<?>>();
            // 存储 javaType 到 Map<JdbcType, TypeHandler> 的映射
            TYPE_HANDLER_MAP.put(javaType, map);
        }
        map.put(jdbcType, handler);
    }
    // 存储所有的 TypeHandler
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

上面的代码只有两层调用,比较简单。同时,所谓的注册过程也就是把类型和处理器进行映射而已,没什么特别之处。关于这个方法就先分析到这里,继续往下分析。下面的方法对应注册方法②。

2.3.7.2 register(Class, Class) 方法分析

当代码执行到此方法时,表示javaTypeClass != null && jdbcType == null条件成立,即使用者仅设置了javaType属性的值。下面我们来看一下该方法的分析。

public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    // 调用中间方法 register(Type, TypeHandler)
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}

private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
    // 获取 @MappedJdbcTypes 注解
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
        // 遍历 @MappedJdbcTypes 注解中配置的值
        for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
            // 调用终点方法,参考上一小节的分析
            register(javaType, handledJdbcType, typeHandler);
        }
        if (mappedJdbcTypes.includeNullJdbcType()) {
            // 调用终点方法,jdbcType = null
            register(javaType, null, typeHandler);
        }
    } else {
        // 调用终点方法,jdbcType = null
        register(javaType, null, typeHandler);
    }
}

上面的代码包含三层调用,其中终点方法的逻辑上一节已经分析过,这里不再赘述。上面的逻辑也比较简单,主要做的事情是尝试从注解中获取JdbcType的值。这个方法就分析这么多,下面分析注册方法④。

2.3.7.3 register(Class) 方法分析

当代码执行到此方法时,表示javaTypeClass == null && jdbcType == null条件成立,即使用者未配置javaTypejdbcType属性的值。该方法的分析如下。

public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    // 获取 @MappedTypes 注解
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
        // 遍历 @MappedTypes 注解中配置的值
        for (Class<?> javaTypeClass : mappedTypes.value()) {
            // 调用注册方法 ②
            register(javaTypeClass, typeHandlerClass);
            mappedTypeFound = true;
        }
    }
    if (!mappedTypeFound) {
        // 调用中间方法 register(TypeHandler)
        register(getInstance(null, typeHandlerClass));
    }
}

public <T> void register(TypeHandler<T> typeHandler) {
    boolean mappedTypeFound = false;
    // 获取 @MappedTypes 注解
    MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
        for (Class<?> handledType : mappedTypes.value()) {
            // 调用中间方法 register(Type, TypeHandler)
            register(handledType, typeHandler);
            mappedTypeFound = true;
        }
    }
    // 自动发现映射类型
    if (!mappedTypeFound && typeHandler instanceof TypeReference) {
        try {
            TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
            // 获取参数模板中的参数类型,并调用中间方法 register(Type, TypeHandler)
            register(typeReference.getRawType(), typeHandler);
            mappedTypeFound = true;
        } catch (Throwable t) {
        }
    }
    if (!mappedTypeFound) {
        // 调用中间方法 register(Class, TypeHandler)
        register((Class<T>) null, typeHandler);
    }
}

public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
    // 调用中间方法 register(Type, TypeHandler)
    register((Type) javaType, typeHandler);
}

上面的代码比较多,不过不用太担心。不管是通过注解的方式,还是通过反射的方式,它们最终目的是为了解析出javaType的值。解析完成后,这些方法会调用中间方法register(Type, TypeHandler),这个方法负责解析jdbcType,该方法上一节已经分析过。一个负责解析 javaType,另一个负责解析 jdbcType,逻辑比较清晰了。那我们趁热打铁,继续分析下一个注册方法,编号为①。

2.3.7.4 register(String) 方法分析

本节代码的主要是用于自动扫描类型处理器,并调用其他方法注册扫描结果。该方法的分析如下:

public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    // 从指定包中查找 TypeHandler
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
        // 忽略内部类,接口,抽象类等
        if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
            // 调用注册方法 ④
            register(type);
        }
    }
}

上面代码的逻辑比较简单,其中注册方法④已经在上一节分析过了,这里就不多说了。

2.3.7.5 小结

类型处理器的解析过程不复杂,但是注册过程由于重载方法间相互调用,导致调用路线比较复杂。这个时候需要想办法理清方法的调用路线,理清后,整个逻辑就清晰明了了。好了,关于类型处理器的解析过程就先分析到这。

2.3.8 解析 mappers 配置

前面分析的都是 MyBatis 的一些配置,本节的内容原本是打算分析 mappers 节点的解析过程。但由于本文的篇幅已经很大了,加之 mappers 节点的过程也比较复杂,而且非常重要(可以说是这些解析步骤中最重要的一个)。所以,关于本节的内容,会在后面的文章中单独讲解。

2.4 创建SqlSessionFactory对象

到这里,配置文件mybatis-config.xml和我们定义映射文件XxxMapper.xml就全部解析完成。

再回到SqlSessionFactoryBuilder类,前面讲到了XMLConfigBuilder中的parse方法,并返回了一个Configuration对象。

//* SqlSessionFactoryBuilder类
public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 根据全局配置文件的文件流实例化出一个XMLConfigBuilder对象
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 使用XMLConfigBuilder的parse()方法构造出Configuration对象
        return build(parser.parse());
    } 
	....

}

返回的Configuration对象此时已经存储了解析出来的全部全局配置信息,然后再将该对象传入build()方法,利用Configuration对象创建一个SqlSessionFactory并将其返回。

// 调用链中最后一个build方法使用了一个Configuration对象作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

继续回到最开始的配置文件解析入口的demo代码中这一行代码里:

// 通过加载配置文件流,将XML配置文件构建为Configuration配置类,构建一个SqlSessionFactory,默认是DefaultSqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

这一行代码就相当于:

SqlSessionFactory sqlSessionFactory = new new DefaultSqlSessionFactory();

至此,SqlSessionFactory 对象就创建好了,后面就就可以通过这个工厂类来创建sqlSession连接,进而进行MyBatis的数据库操作。

三、总结

MyBatis解析全局配置文件的流程总结来看就是,SqlSessionFactoryBuilder利用XMLConfigBuilder去解析全局配置文件,包括属性配置、别名配置、拦截器配置、环境(数据源和事务管理器)、Mapper配置等;解析完这些配置后会生成一个Configuration对象,这个对象就包含了所有的全局配置信息。然后利用这个Configuration对象创建一个SqlSessionFactory对象,这个对象包含了Configration对象。我们就可以使用这个工厂对象SqlSessionFactory来生成数据库连接对象sqlSession,进而进行相应的数据库操作。

解析全局配置文件的时序图:


相关文章: 【MyBatis】MyBatis的介绍和基本使用
                   【MyBatis】MyBatis的日志实现_mybatis数据库执行日志-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/643724.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

结构型模式之桥接模式

文章目录 概述原理结构图代码示例 小结 概述 桥接模式(bridge pattern) 的定义是&#xff1a;将抽象部分与它的实现部分分离&#xff0c;使它们都可以独立地变化。 桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联来取代传统的多层继承,将类之间的静态继承关系转…

oracle数据库无法连接问题排查

查看数据库告警日志如下图。发现问题时间段&#xff0c;没有数据库服务故障报错&#xff0c;但是存在较多TNS-12535、12560、12170、00505错误&#xff1a; 通过检查问题时间段应用日志&#xff0c;也记录了Caused by:java.sql.SQLRecoverableException: IO 错误: Connection r…

从反向传播(BP)到BPTT:详细数学推导【原理理解】

从反向传播到BPTT&#xff1a;详细推导与问题解析 在本文中&#xff0c;我们将从反向传播算法开始&#xff0c;详细推导出反向传播通过时间&#xff08;Backpropagation Through Time, BPTT&#xff09;算法。重点讨论BPTT中的梯度消失和梯度爆炸问题&#xff0c;并解释如何解…

如何彻底搞懂装饰器(Decorator)设计模式?

对于任何一个软件系统而言&#xff0c;往现有对象中添加新功能是一种不可避免的实现场景&#xff0c;但这一实现过程对现有系统的影响可大可小。从架构设计上讲&#xff0c;我们也知道存在一个开闭原则&#xff08;Open-Closed Principle&#xff0c;OCP&#xff09;&#xff0…

asp.net core接入prometheus2-自定义指标

前提 了解一下asp.net core接入prometheus快速入门 https://blog.csdn.net/qq_36437991/article/details/139064138 新建.net 8空web项目 安装下面三个包 <PackageReference Include"OpenTelemetry.Exporter.Prometheus.AspNetCore" Version"1.8.0-rc.1&…

真拿AI赚到钱的人,不在朋友圈里

1 最近有张两大AI巨头对比的梗图给我看乐了&#xff0c;玩儿AI的还在做产品&#xff0c;玩儿焦虑的已经在数钱了。 这也是在做AI&#xff0c;只不过是唉声叹气的ai。 要我说&#xff0c;现在缺的根本不是AI&#xff0c;而是【有用的AI】。 恩格斯老师说过一句话&#xff1a…

Java 对接百度网盘

文章目录 前言一、创建百度网盘账号二、代码实现1. 常量类2. 工具类3. 授权码模式授权4. 文件分片上传&#xff08;可获取进度&#xff09;--方法一5. 文件下载(可获取进度)--方法一6. 获取文件列表7. 文件分片上传&#xff08;不可获取进度&#xff09;--方法二7. 文件下载&am…

算法之堆排序

堆排序是一种基于比较的排序算法&#xff0c;通过构建二叉堆&#xff08;Binary Heap&#xff09;&#xff0c;可以利用堆的性质进行高效的排序。二叉堆是一个完全二叉树&#xff0c;可以有最大堆和最小堆两种形式。在最大堆中&#xff0c;父节点的值总是大于或等于其子节点的值…

C++与Android处理16进制大端/小端数据实例(二百七十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

“大数据建模、分析、挖掘技术应用研修班”的通知!

随着2015年9月国务院发布了《关于印发促进大数据发展行动纲要的通知》&#xff0c;各类型数据呈现出了指数级增长&#xff0c;数据成了每个组织的命脉。今天所产生的数据比过去几年所产生的数据大好几个数量级&#xff0c;企业有了能够轻松访问和分析数据以提高性能的新机会&am…

夏日采摘季,视频智能监控管理方案助力智慧果园管理新体验

5月正值我国各地西瓜、杨梅、大樱桃、油桃等水果丰收的季节&#xff0c;许多地方都举办了采摘旅游活动&#xff0c;吸引了众多游客前来体验采摘乐趣。随着采摘的人流量增多&#xff0c;果园的管理工作也面临压力。 为了提升水果园采摘活动的管理效果&#xff0c;减少人工巡查成…

harbor 认证

Harbor 认证过程 Harbor以 Docker Registry v2认证为基础&#xff0c;添加上一层权限保护。1.v2 集成了一个安全认证的功能&#xff0c;将安全认证暴露给外部服务&#xff0c;让外部服务去实现2.强制用户每次Docker pull/push请求都要带一个合法的Token&#xff0c;Registry会…

基于jeecgboot-vue3的Flowable新建流程定义(一)

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、vue3版本因为流程分类是动态的&#xff0c;不再固定了&#xff0c;所以新建的时候需要选择建立哪种流程类型的流程 代码如下&#xff1a; <!-- 选择模型的流程类型对话框 -->&…

JDBCTemplate介绍

Spring JDBC Spring框架对Spring的简单封装。提供一个JDBCTemplate对象简化JDBC开发 *步骤&#xff1a; 1、导入jar包 2、创建JDBCTemplate对象。依赖于数据源DataSource *JdbcTemplate templatenew JdbcTemplate(ds); 3、调用JdbcTemplate的方法来完成CRUD的操作 *update()&…

【实战教程】使用Spring AOP和自定义注解监控接口调用

一、背景 随着项目的长期运行和迭代&#xff0c;积累的功能日益繁多&#xff0c;但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。 为了提高系统性能和代码质量&#xff0c;我们往往需要对那些不常用的功能进行下线处理。 那么&#xff0c;该下线哪些功能呢&…

代码随想录-Day18

513. 找树左下角的值 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 方法一&#xff1a;深度优先搜索 class Solution {int curVal 0;int curHeight 0;public int findBottomLeftValue(TreeNode roo…

GitLens或者Git Graph在vscode中对比文件历史变化,并将历史变化同步到当前文件中

有时候我们上周改的代码&#xff0c;现在想反悔把它恢复过来&#xff0c;怎么办&#xff1f;&#xff1f;&#xff1f;很好&#xff0c;你有这个需求&#xff0c;说明你找对人了&#xff0c;那就是我们需要在vscode中安装这个插件&#xff1a;GitLens或者Git Graph&#xff0c;…

做抖店四年来的经验分享,想做抖店的多看看,给你揭露真正的抖店

大家好&#xff0c;我是电商花花。 我做抖音小店从21年就已经开始了&#xff0c;中间一直都没断过&#xff0c;一直都抖店无货源&#xff0c;从刚开始的一家店铺&#xff0c;到现在的80多家店铺&#xff0c;不断完善和总结我们做店的方法。 在我看来做抖音小店现在很简单&…

Linux服务升级:Twemproxy 升级 Redis代理

目录 一、实验 1.环境 2.多实例Redis部署 3.Twemproxy 升级Redis代理 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 系统版本软件IP备注CentOS7.9Twemproxy192.168.204.200 Redis代理 Redis127.0.0.1:6379第一个Redis实例 Redis127.0.0.1:6380第二个…

别被“涨价“带跑,性价比才是消费真理

文章来源&#xff1a;全食在线 “再不好好赚钱&#xff0c;连方便面也吃不起了。”这是昨天在热搜下&#xff0c;一位网友的留言。而热搜的内容&#xff0c;正是康师傅方便面即将涨价的消息。 01 传闻初现 昨天上午&#xff0c;朋友圈就有人放出康师傅方便面要涨价的消息&am…