【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 constructortry {// 这个是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() {@Overridepublic void error(SAXParseException exception) throws SAXException {throw exception;}@Overridepublic void fatalError(SAXParseException exception) throws SAXException {throw exception;}@Overridepublic 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函数方便得到孩子所有PropertiesProperties defaults = context.getChildrenAsProperties();// 2.然后查找resource或者url,加入前面的PropertiesString 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也全部加入PropertiesProperties 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;// 反射器// 可以看到方法基本都是再次委派给这个Reflectorprivate final Reflector reflector;private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {this.reflectorFactory = reflectorFactory;// 根据类型创建 Reflectorthis.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())) {// 为属性创建创建 MetaClassMetaClass metaProp = metaClassForProperty(prop.getName());// 再次调用 hasSetterreturn 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 默认为 trueif (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 -> namename = 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,并将转换后的结果缓存到 setTypessetTypes.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[] 的 setterSystem.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 = articlesname = name.substring(0, delim);}}// 省略 getterpublic 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 属性,默认值为 PARTIALconfiguration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));// 设置 cacheEnabled 属性,默认值为 trueconfiguration.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) {// 通过别名注册器解析别名对于的类型 Classreturn 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包里面去找一个classList<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是否就是指定的environmentfor (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"解析返回适当的TransactionFactoryTransactionFactory 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;}@Overridepublic 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()) {// 从指定的包中注册 TypeHandlerif ("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);}// 存储所有的 TypeHandlerALL_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 = nullregister(javaType, null, typeHandler);}} else {// 调用终点方法,jdbcType = nullregister(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<?>>();// 从指定包中查找 TypeHandlerresolverUtil.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博客

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决移植Metasploitable3到VM虚拟机无网络的问题

第一步 导入后不要开机&#xff0c;先在虚拟机设置里面将原有的两个网络适配器移除。 第二步 接着在选项里面&#xff0c;在客户机操作系统里面&#xff0c;选择Microsoft Windwos(W)&#xff0c; 版本选择Windows Server 2008 R2 x64 第三步 先打开虚拟机&#xff0c;然后…

Python_文件操作_学习

目录 一、关于文件的打开和关闭 1. 文件的打开 2.文件的关闭 二、文件的读取 1. 文件的读_r 2. 使用readline 3.使用readlines 三、文件的写入 1. 文本的新建写入 2.文本的追加写入 四、文件的删除和重命名 1.文件的重命名 2.文件的删除 五、文件的定位读写 1.t…

Unity Miscellaneous入门

概述 在Unity中有非常多好用的组件&#xff0c;也是Unity为我们提供的方便的开发工具&#xff0c;它的功能可能不是主流的内容&#xff0c;比如渲染&#xff0c;音乐&#xff0c;视频等等&#xff0c;所有Unity把这些内容统一归到了一个杂项文件组中。 Unity组件入门篇总目录-…

Python线程

Python线程 1. 进程和线程 先来了解下进程和线程。 类比&#xff1a; 一个工厂&#xff0c;至少有一个车间&#xff0c;一个车间中至少有一个工人&#xff0c;最终是工人在工作。 一个程序&#xff0c;至少有一个进程&#xff0c;一个进程中至少有一个线程&#xff0c;最终…

langchain实战-从0到1搭建ai聊天机器人

介绍 当前&#xff0c;人工智能大模型公司如雨后春笋般迅速涌现&#xff0c;例如 OpenAI、文心一言、通义千问等&#xff0c;它们提供了成熟的 API 调用服务。然而&#xff0c;随之而来的是不同公司的繁琐协议接入过程&#xff0c;这让许多开发者感到头疼不已。有没有一种统一…

SpringBoot + Redis实现对接口的限流

目录 前言 什么是限流&#xff1f; 实现限流 创建一个注解类 接着创建一个切面类 前言 在项目中&#xff0c;对于接口的限流&#xff0c;是任何项目都必不可少的一部分&#xff0c;主要是为了防止用户频繁的发送请求&#xff0c;对服务器造成压力。 另外一点就是防止外来攻…

C++之第八课

课程列表 今天我们来学一学C里的一些实用的东西。 1.域宽 说到域宽setw&#xff0c;就叒要加头文件了。 #include<iomanip> 使用格式是&#xff1a; cout<<setw(5)<<"123"; setw括号里面可以改数字&#xff0c;后面就是输出内容了&#xff…

COD论文笔记 Boundary-Guided Camouflaged Object Detection

动机 挑战性任务&#xff1a;伪装物体检测&#xff08;COD&#xff09;是一个重要且具有挑战性的任务&#xff0c;因为伪装物体往往与背景高度相似&#xff0c;使得准确识别和分割非常困难。现有方法的不足&#xff1a;现有的深度学习方法难以有效识别伪装物体的结构和细节&am…

MySQL索引、视图练习

素材 1.学生表&#xff1a;Student (Sno, Sname, Ssex , Sage, Sdept) 学号&#xff0c;姓名&#xff0c;性别&#xff0c;年龄&#xff0c;所在系 Sno为主键 2.课程表&#xff1a;Course (Cno, Cname,) 课程号&#xff0c;课程名 Cno为主键 3.学生选课表&#xff1a;SC (Sno…

Home Credit - Credit Risk Model Stability

本篇是对Kaggle上Home Credit - Credit Risk Model Stability竞赛中的开源代码VotingClassifier Home Credit的解读。原链接在VotingClassifier Home Credit (kaggle.com)。 %%writefile script.py import sys from pathlib import Path import subprocess import os import g…

浅谈JMeter运行原理

浅谈JMeter运行原理 JMeter架构基础 JMeter基于Java平台开发&#xff0c;运行于Java虚拟机&#xff08;JVM&#xff09;之上。这意味着它可以在任何支持JVM的操作系统上运行&#xff0c;包括Windows、Linux、macOS等。其核心架构设计围绕着多线程执行机制&#xff0c;这使得它…

AI大模型探索之路-实战篇6: Function Calling技术调研之详细流程剖析

系列篇章&#x1f4a5; AI大模型探索之路-实战篇4&#xff1a;DB-GPT数据应用开发框架调研实践 AI大模型探索之路-实战篇5&#xff1a; Open Interpreter开放代码解释器调研实践 目录 系列篇章&#x1f4a5;一、前言二、Function Calling详细流程剖析1、创建OpenAI客户端2、定…

PCL 法向量加权的RANSAC拟合分割平面

目录 一、算法原理1、原理概述2、主要函数二、代码实现三、结果展示四、相关链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、原理概述

鸿蒙布局List简介

鸿蒙布局List简介 List--常见的布局容器List 创建方式创建方式一&#xff0c;通过Listitem创建方式二&#xff0c;通过ForEach和Listitem创建方式三&#xff0c;通过ListItemGroup List–常见的布局容器 List是在app开发中最常见的一种布局方式&#xff0c;例如通讯录、新闻列…

Wpf 使用 Prism 实战开发Day24

自定义询问窗口 当需要关闭系统或进行删除数据或进行其他操作的时候&#xff0c;需要询问用户是否要执行对应的操作。那么就需要一个弹窗来给用户进行提示。 一.添加自定义询问窗口视图 (MsgView.xaml) 1.首先&#xff0c;添加一个自定义询问窗口视图 (MsgView.xaml) <Use…

域内攻击 ----->约束非约束委派攻击

在域中&#xff0c;除了我们常见的横向移动以外&#xff0c;还有很多攻击&#xff0c;像什么kerberoasting&#xff0c;委派攻击&#xff0c;NTLMrelay啊...... 还有很多&#xff08;暂时只知道这些&#xff09; 以前在一篇公众号看到的一个笑话也荟萃了网安的一些攻击手法&am…

《拯救大学生课设不挂科第二期之Windows11下安装VC6.0(VC++6.0)与跑通Hello World C语言程序教程》【官方笔记】

背景与目标人群&#xff1a; 大学第一次学C语言的时候&#xff0c;大部分老师会选择VC6这个编辑器。 但由于很多人是新手&#xff0c;第一次上大学学C语言。 老师要求VC6.0&#xff08;VC6.0&#xff09;写C语言跑程序可能很多人还是第一次接触电脑。 需要安装VC6这个编辑器…

初识Java--开启我的Java学习之旅

目录 一、JAVA语言概述二、JAVA语言的重要性2.1语言使用广泛程度2.2工作领域2.3在校招岗位的需求2.4 java语言发展简史2.5Java语言特性 三、初识java的main方法四、运行java程序五、【面试题】JDK、JRE、JVM之间的关系&#xff1f; 一、JAVA语言概述 Java是一种优秀的程序设计…