加载配置文件
获取输入流
- myBatis 的配置文档层次架构
- 首先从读入开始查看是怎么加载配置文件的,现在从这里打个断点
public class MybatisTest {
@Test
public void test1() throws IOException {
// 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中
// 注意:配置文件并没有被解析
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
...
}
}
- 然后发现会通过一个 classLoaderWrapper: ClassLoader 的包装器来把数据源读入,返回输入流
public class Resources {
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
...
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
...
}
}
- 继续进入会发现其实会通过 getClassLoaders(classLoader) 返回对应加载器之后再进行读取。现在的 classLoader 是 null
public class ClassLoaderWrapper {
// 默认类加载器
ClassLoader defaultClassLoader;
// 系统类加载器
ClassLoader systemClassLoader;
...
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
}
- 其中 getClassLoaders(classLoader) 代码如下
public class ClassLoaderWrapper {
...
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
// 参数指定的类加载器
classLoader,
// 系统指定的默认加载器
defaultClassLoader,
// 当前线程绑定的类加载器
Thread.currentThread().getContextClassLoader(),
// 当前类使用的类加载器
getClass().getClassLoader(),
systemClassLoader};
}
}
- 拿到加载器集合之后才正在的调用读取源方法,最后返回输入流,判断方法如下
public class ClassLoaderWrapper {
...
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
// 循环 ClassLoader,通过指定或默认的 ClassLoader 读取文件
for (ClassLoader cl : classLoader) {
// 如果加载器不是空的
if (null != cl) {
// 先用这个加载器尝试读一下
InputStream returnValue = cl.getResourceAsStream(resource);
// 如果返回没结果,有可能是因为缺少个 "/",添加后再读一下
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
// 如果读取到,终止循环,返回结果
return returnValue;
}
}
}
return null;
}
}
- 当输入流读成功之后,就返回
public class Resources {
...
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
// 找到输入流之后,就返回了
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}
}
- 问题:为什么需要 ClassLoaderWrapper 包装器,而不直接使用 ClassLoader?
- 因为它需要拿来判断资源路径是否为空
- 根据路径加载好的输入流是否为空
- 使用的类加载器是否为空
- 异常处理
- 总结
- 获取输入流
解析配置文件
- 首先先通过 SqlSessionFactoryBuilder 构建 sqlSessionFactory,我们能知道 sqlSessionFactory 肯定包含了所有配置文件信息
public class MybatisTest {
@Test
public void test1() throws IOException {
...
// 2. (1) 解析了配置文件,封装 configuration 对象
// (2)创建了 DefaultSqlSessionFactory 工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
...
}
}
- 首先是调用 build() 方法,发现我们 environment 输入的是 null
public class SqlSessionFactoryBuilder {
...
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
// environment 运行环境信息的 id 值
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
...
}
- 其实正常来说,我们可以自己配置 environment 参数,因为我们知道 Mybatis 是支持多环境配置,定义就在 sqlMapConfig.xml 中
<configuration>
...
<environment id="development">
...
</environment>
...
</configuration>
- 然后就需要通过 XMLConfigBuilder 来将输入流创建成 document 对象
public class SqlSessionFactoryBuilder {
...
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// XMLConfigBuilder:用来解析XML配置文件
// 使用构建者模式:
// - 好处:降低耦合、分离复杂对象的创建
// 1. 创建 XPathParser 解析器对象,根据 inputStream 解析成了 document 对象
// 2. 创建全局配置对象 Configuration 对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
...
}
}
注意:对于返回至少 4 个参数以上的对象,最好使用 构建者模式
- 在 XMLConfigBuilder 的构造器中就会通过创建一个 XPathParser 对象,里面就会包含一个 document 对象,也就是 XPathParser 其实只会专门负责解析配置文件而已
public class XMLConfigBuilder extends BaseBuilder {
...
protected final Configuration configuration; // 这个是 BaseBuilder 里面得属性
private boolean parsed;
private final XPathParser parser;
private String environment;
...
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
// XPathParser 基于 Java XPath 解析器,用于解析 MyBatis 中的配置文件
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
}
- createDocument() 的东西过于细节,就先不关注了,反正最终就是,通过 dom 解析,获取 Document 对象,然后赋值到成员变量中
public class XPathParser {
...
private final Document document;
...
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
// 解析 XML 文档为 Document 对象
this.document = createDocument(new InputSource(inputStream));
}
}
- 然后开始初始化 XMLConfigBuilder,首先会创建一个 Configuration 对象
public class XMLConfigBuilder extends BaseBuilder {
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 创建全局 Configuration 对象,并通过 TypeAliasRegistry 注册一些 Mybatis 内部相关类的别名
super(new Configuration());
...
}
}
- 简单来看看 Configuration 的构造方法,可以看到注册了一堆别名器
public class Configuration {
...
protected Environment environment;
// 类型注册器, 用于在执行 sql 语句的出入参映射以及 mybatis-config 文件里的各种配置
// 比如 <transactionManager type="JDBC"/><dataSource type="POOLED"> 时使用简写
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
...
// 为什么使用 JDBC 别名就能使用 jdbc 事务管理呢? sqlConfig.xml,原因就是在这
public Configuration() {
// TypeAliasRegistry(类型别名注册器)
// 注册事务工厂的别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
...
}
}
- 问题:为什么使用 JDBC 别名就能使用 jdbc 事务管理呢?
- 原因就是这里, 因为我们在 sqlMapConfig.xml 中,指定了用哪个工厂来赋值,其中的 type 字段就是
<configuration>
...
<environment id="development">
<!-- 使用jdbc事务管理 -->
<transactionManager type="JDBC" />
<!-- 数据库连接池 -->
<dataSource type="POOLED">
...
</dataSource>
</environment>
...
</configuration>
- 回到 build() 得到 XMLConfigBuilder 后,才真正开始执行分析
public class SqlSessionFactoryBuilder {
...
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parser.parse():使用 XPATH 解析 XML 配置文件,将配置文件封装到 Configuration 对象
// 返回 DefaultSqlSessionFactory 对象,该对象拥有 Configuration 对象(封装配置文件信息)
// parse():配置文件就解析完成了
return build(parser.parse());
}
}
XMLConfigBuilder 解析 Document 获取配置存入 configuration
- 继续从 parser.parse() 里面进入会看到 parse() 只会解析一次
public class XMLConfigBuilder extends BaseBuilder {
...
public Configuration parse() {
// 一开始是 false,只会计息一次
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// parser.evalNode("/configuration"):通过 XPATH 解析器,解析 configuration 根节点
// 从 configuration 根节点开始解析,最终将解析出的内容封装到 Configuration 对象中
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
}
- 然后通过 evalNode() 将解析 /configuration 下的所有内容成 Xnode
public class XPathParser {
...
public XNode evalNode(String expression) {
// 根据XPATH语法,获取指定节点
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) { // 获取需选中的内容,转换成 xnode
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
return new XNode(this, node, variables);
}
...
}
- 其中 Xnode 包含的内容就是 sqlMapConfig.xml 标签 <configuration> 下的所有内容
- 进入到 parseConfiguration() 之后会发现很多 **Element() 的函数,很关键,这些都是把 /configuration 节点下的内容解析出来,存到 configuration 对象中
public class XMLConfigBuilder extends BaseBuilder {
...
private void parseConfiguration(XNode root) {
try {
// 解析 </properties> 标签
propertiesElement(root.evalNode("properties"));
// 解析 </settings> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(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"));
settingsElement(settings);
// 解析 </environments> 标签
environmentsElement(root.evalNode("environments"));
// 解析 </databaseIdProvider> 标签
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);
}
}
}
- 跳到 environmentsElement() 看看
- 首先它会从 Xnode 节点中提取 /environments 元素,然后判断存不存在内容
- 然后判断是否传入过 environment 参数,如果没有,就从 /environments 拿到 default 属性 要激活的环境
- 拿到环境后,开始根据每个标签下定义的 type 属性 创建各个工厂类,最后封装到 configuration 中
public class XMLConfigBuilder extends BaseBuilder {
...
private void environmentsElement(XNode context) throws Exception {
// 如果定义了environments
if (context != null) {
// 方法参数如何没有传递 environment,则解析 sqlMapConfig.xml 中的
if (environment == null) {
// <environments default="development" >
environment = context.getStringAttribute("default");
}
//遍历解析 environment 节点,找到生效的 environment
for (XNode child : context.getChildren()) {
// 获得 id 属性值
String id = child.getStringAttribute("id");
// 判断 id 和 environment 值是否相等
if (isSpecifiedEnvironment(id)) {
// <transactionManager type="JDBC" /> 创建事务工厂对象
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// <dataSource type="POOLED"> 创建数据源对象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 将 Environment 存到 configuraion 中
configuration.setEnvironment(environmentBuilder.build());
break;
}
}
}
}
}
- 像 transactionManagerElement(),type 属性 作用就是起的别名里面已经有对应工厂类了,直接初始化即可
public class XMLConfigBuilder extends BaseBuilder {
...
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
}
- 再来看看 mapperElement() 解析 /mappers 的情况
- 它会根据子标签 /package 还是 /mapper 进行处理,先简单看看 /mapper 的解析
- 它会从 /mapper 中解析出 resource 属性、 url 属性、class 属性,这 3 者是互斥的,只能用其中一个
- 像 resource 属性 的解析,还需要使用 XMLMapperBuilder 来专门解析
public class XMLConfigBuilder extends BaseBuilder {
...
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 获取 <mappers> 标签的子标签
for (XNode child : parent.getChildren()) {
// <package> 子标签
if ("package".equals(child.getName())) {
...
} else {// <mapper>子标签
// 获取<mapper>子标签的 resource 属性
String resource = child.getStringAttribute("resource");
// 获取 <mapper> 子标签的 url 属性
String url = child.getStringAttribute("url");
// 获取 <mapper> 子标签的 class 属性
String mapperClass = child.getStringAttribute("class");
// 它们是互斥的
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
// 专门用来解析 mapper 映射文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 通过 XMLMapperBuilder 解析 mapper 映射文件
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 通过 XMLMapperBuilder 解析 mapper 映射文件
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 将指定 mapper 接口以及它的代理对象存储到一个 Map 集合中,key 为 mapper 接口类型,value 为代理对象工厂
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
...
}
- 总结
- 解析配置文件
- 获取 SqlSessionFactory