一、Demo
1、配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="logImpl" value="SLF4J"/> </settings> <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="xml/TestMapper.xml"/> </mappers> </configuration>
2、Java类
public class MyBatisDemo { private static SqlSessionFactory sqlSessionFactory; public static SqlSession getSqlSession() throws FileNotFoundException { // mybatis配置文件 InputStream configFile = new FileInputStream( "E:\\demo\\mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configFile); // 加载配置文件得到SqlSessionFactory,从而得到SQLSession return sqlSessionFactory.openSession(); } public static void main(String[] args) throws FileNotFoundException { SqlSession sqlSession = getSqlSession(); } // 得到SqlSession后就可以crud了,此处不做demo。后面章节会详细讲解是如何解析mapper文件的,是如何与接口对应上的。 }
二、解析过程分析
1、简介
在上面Demo中,我们大致流程是:加载配置文件–》通过SQLSessionFactoryBuilder对象的build方法构建SQLSessionFactory对象–》通过SQLSessionFactory对象得到SQLSession。
那么问题来了:到底是怎么通过配置文件创建出来SQLSessionFactory对象的呢?
2、源码
首先我们从build方法入手。
public SqlSessionFactory build(InputStream inputStream) { // 重载,不bb return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 创建配置文件的解析器(真正负责解析配置文件的类) XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 调用parse方法解析配置文件(真正负责解析配置文件的核心类),生成Configuration对象(后面篇幅会细说)。且将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. } } } // 根据上行传来的Configuration对象创建SQLSessionFactory对象。 public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
build方法完事了,貌似只明白了build是创建SQLSessionFactory对象的,但是具体如何解析配置文件的我们并不知道,只是核心方法parse负责解析。那么我们接下来就一起分析下parse方法。
public class XMLConfigBuilder extends BaseBuilder { // 负责解析配置文件的核心方法 public Configuration parse() { // 确保配置文件只能被解析一次,避免多个线程或多个重复解析的情况。 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 标记配置文件已经解析,不能再次解析。 parsed = true; /** * parser.evalNode("/configuration") 这里有个xpath表达式,此句话不做多分析,意思就是解析配置文件中的configuration节点。不知道是哪个configuration节点的,再去上面看看Demo的配置文件,根节点就是。 * 解析出根节点下面的内容传递给parseConfiguration:真正负责解析配置文件的方法,私有的。 */ parseConfiguration(parser.evalNode("/configuration")); return configuration; } /** * 真正解析配置文件的方法 */ private void parseConfiguration(XNode root) { try { // 解析配置文件里的properties标签配置 propertiesElement(root.evalNode("properties")); // 解析配置文件里的settings标签配置,并将其转换为Properties对象。 Properties settings = settingsAsProperties(root.evalNode("settings")); // 加载vfs loadCustomVfs(settings); // 解析配置文件里的typeAliases标签配置 typeAliasesElement(root.evalNode("typeAliases")); // 解析配置文件里的plugins标签配置 pluginElement(root.evalNode("plugins")); // 解析配置文件里的objectFactory标签配置 objectFactoryElement(root.evalNode("objectFactory")); // 解析配置文件里的objectWrapperFactory标签配置 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 解析配置文件里的reflectorFactory标签配置 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 将settings中的信息设置到Configuration对象中 settingsElement(settings); // 解析配置文件里的environments标签配置 environmentsElement(root.evalNode("environments")); // 解析配置文件里的databaseIdProvider标签配置,获取并设置databaseId到Configuration对象 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 解析配置文件里的typeHandlers标签配置 typeHandlerElement(root.evalNode("typeHandlers")); // 解析配置文件里的mappers标签配置 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } }
到此,一个完整的配置解析过程就完事了,每个标签的解析逻辑都封装到了对应的方法中,现在大家脑袋里应该有个印象了:XMLConfigBuilder类的parse方法负责解析配置文件的每个标签节点,并将解析出来的结果封装到Configuration对象中。然后SqlSessionFactoryBuilder类的build方法拿着解析出来的Configuration对象去创建SQLSessionFactory。下面我们将分析几个重点的解析方法
2.1、解析properties标签
2.1.1、配置
<properties resource="jdbc.properties"> <property name="jdbc.username" value="hello"/> <property name="jdbc.password" value="world"/> <property name="helloworld" value="helloworld"/> </properties>
jdbc.username=123 jdbc.password=456 jdbc.url=xxx
2.1.2、源码
上面配置包含了一个resource属性和三个子节点,接下来我们分析源码
public class XMLConfigBuilder extends BaseBuilder { private void propertiesElement(XNode context) throws Exception { if (context != null) { /** * 解析properties的子节点并将这些节点内容转换为属性对象Properties * 注意这时候Properties里包含如下属性: * jdbc.username=hello * jdbc.password=world * helloworld=helloworld * * 就是解析的配置文件里的properties标签嘛,没毛病。 */ Properties defaults = context.getChildrenAsProperties(); // 获取properties节点中的resource属性 String resource = context.getStringAttribute("resource"); // 获取properties节点中的url属性 String url = context.getStringAttribute("url"); // 既设置了resource又设置了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) { /** * 从文件系统中加载并解析属性文件 * Resources.getResourceAsProperties(resource);此方法会将resource资源里的属性 * 解析出来放到Properties对象中。 * 然后我们defaults.putAll(properties),所以由于key一样,覆盖掉了原来的Properties * 所以这步骤完成后结果如下: * jdbc.username=123 * jdbc.password=456 * jdbc.url=xxx * helloworld=helloworld */ defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { // 从文url中加载并解析属性文件 defaults.putAll(Resources.getUrlAsProperties(url)); } Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); // 将属性值设置到configuration中 configuration.setVariables(defaults); } } }
大致上明白逻辑了,很简单,获取properties标签的子节点,然后判断设置了url还是resource,分别走不同分支去解析属性配置文件(比如上面的properties文件)。最后将解析出来的属性都放到Configuration对象中。那么具体是如何解析子节点的呢?接着往下看
public class XNode { /** * 不做太多解释了,就是解析出name和value属性并set到Properties对象中 */ public Properties getChildrenAsProperties() { Properties properties = new Properties(); for (XNode child : getChildren()) { // 获取name String name = child.getStringAttribute("name"); // 获取value String value = child.getStringAttribute("value"); if (name != null && value != null) { // name、value设置到Properties中 properties.setProperty(name, value); } } return properties; } }
问:【2.1.1、配置】最终出来的properties属性包含哪些?值是什么?
这是一个面试题,也能讲解为什么我们properties配置文件里写的属性会覆盖掉properties标签下的key-value。
答:
jdbc.username=123
jdbc.password=456
jdbc.url=xxx
helloworld=helloworld为什么不是
jdbc.username=hello
jdbc.password=world
jdbc.url=xxx
helloworld=helloworld自己看上面源码注释。
2.2、解析environments标签
2.2.1、配置
<environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments>
2.2.2、源码
private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { // 获取default属性 environment = context.getStringAttribute("default"); } //遍历子节点 for (XNode child : context.getChildren()) { // 获取每一个子节点的id属性 String id = child.getStringAttribute("id"); /** * 检测当前environment节点的id与上面父节点的default的值是否一致,一致true,否则false */ if (isSpecifiedEnvironment(id)) { // 解析 transactionManager节点 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); // 解析 dataSource节点 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); // 创建DataSource对象 DataSource dataSource = dsFactory.getDataSource(); // 构建environment对象,并将事物管理和数据源设置到Configuration对象中。 Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } }
三、总结
仅对properties和environments标签进行了源码讲解,而且对properties进行了及其详细的讲解,为什么没有全部讲解:
- 1、大的思路和核心源码有了后,其他的源码你们应该自己动手debug去学习。
- 2、篇幅导致过长。
- 3、像核心内容:mapper解析方法:mapperElement,我会在后面详细讲解。
核心流程:
加载配置文件–》通过SQLSessionFactoryBuilder对象的build方法构建SQLSessionFactory对象。
build方法需要Configuration对象,Configuration对象通过XMLConfigBuilder的parse方法进行解析配置文件并将解析出来的内容装配到Configuration中。
四、补充
此篇幅讲解的是mybatis核心SQLSessionFactory的创建过长。后面章节我会讲解mapper文件的解析过程以及到底是怎么跟接口对应起来的。
五、广告