一、简介

我们上一个篇文章已经配置好了,​​mybatis​​​配置文件和测试类。我们先分析一下​​mybatis​​​的是如何加载​​mybatis-config.xml​​文件的。

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

这里是通过​​mybatis​​​工具类​​Resources​​​加载配置文件,得到一个​​InputStream​​输入流。

二、资源文件加载

接着我们先看一下​​Resources​​的资源加载工具类。

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
// 通过classLoaderWrapper包装类加载
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}

接着通过类加载器,从资源路径(​​classpath​​)中加载资源,

public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}

这时候初始化类加载有五种:

ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader, // 出入的类加载
defaultClassLoader, // 默认类加载
Thread.currentThread().getContextClassLoader(), // 线程类上下文加载器
getClass().getClassLoader(), // 当前类加载器
systemClassLoader // 系统类加载器
};
}

然后会从这些类加载器中加载我们需要资源:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
// 循环遍历获取资源
for (ClassLoader cl : classLoader) {
if (null != cl) {

// try to find the resource as passed
InputStream returnValue = cl.getResourceAsStream(resource);

// 有些加载器需要添加`/`,所以加上这个`/`前缀,重试
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}

if (null != returnValue) {
return returnValue;
}
}
}
return null;
}

资源路径: 是我们编译之后​​target/classes/​​目录下的资源。

这个逻辑其实还是很简单的,重点是我们需要复习一下类加载器相关的技能。下面是我们常见的类加载器基本上是这些:

  • ​ClassLoader.getSystemClassLoader()​​:系统类加载器
  • ​xxx.class.getClassLoader()​​​和​​this.getClass().getClassLoader()​​:当前类加载器
  • ​Thread.currentThread().getContextClassLoader()​​:线程上下文加载器

主要区别:

  1. ​getClassLoader​​:是获取加载当前类的类加载器。可能是***启动类加载器***、拓展类加载器系统类加载器,取决于当前类是有哪个加载器加载的。
  2. ​getClontextCLassLoader​​​:是获取当前线程上下文的类加载器,用户可以自己设置,​​Java SE​​​环境下一般是​​AppClassLoader​​​,​​Java EE​​​环境下是​​WebappClassLoader​​。
  3. ​getSystemClassLoader​​​:是获取系统类加载器​​AppClassLoader​​。

三、构建​​SessionFactory​

接着我们看看一下​​sessionFactory​​是如何构建的

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 构建sessionFactory
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

我们看一下​​SqlSessionFactoryBuilder​​​这个类,这个就一个核心方法:​​build​​。这个方法大体分成两类,都是参数重载的方法。

public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}

这个方法可以将数据源信息和属性一起构建,我们是从配置文件中构建的,所以这两个参数时​​null​​。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 构建配置文件解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 这里面调用的是解析器的解析各个节点
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.
}
}
}

这里面核心方法是​​parser.parse()​​​。这里面用来解析​​mybatis-config.xml​​的各个节点。

public Configuration parse() {
// step 防止别多次读取
if (parsed) {
throw new BuilderException("每个XMLConfigBuilder只能使用一次.");
}
parsed = true;
// step 解析各个节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

/**
* 解析配置文件的各个节点
* @param root 根节点
*/
private void parseConfiguration(XNode root) {
try {
// 解析 properties 节点
propertiesElement(root.evalNode("properties"));
// 解析 解析 settings 配置,并将其转换为 Properties 对象
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载 vfs VFS主要用来加载容器内的各种资源,比如jar或者class文件
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"));
// 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("解析SQL Mapper配置时出错. 原因: " + e, e);
}
}

3.1. ​​properties​​节点的解析

我们在​​mybatis-config.xml​​​中配置​​properties​​​节点, 我们里面配置的是​​resource​​。

<configuration>
<properties resource="jdbc.properties" />
......
</configuration>

​jdbc.properties​​里面配置的是我们的数据库连接相关信息

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.252.139:3306/mybatis-test
jdbc.username=root
jdbc.password=123456

接着看着​​propertiesElement​​​这个是如何解析​​properties​​节点的。

/**
* 解析properties节点
* @param context properties节点
* @throws Exception
*/
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// step 读取子节点数据
Properties defaults = context.getChildrenAsProperties();
// step 获取节点里面的resource和url
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
// step resource和url只能有一个存在
if (resource != null && url != null) {
throw new BuilderException("properties元素不能同时指定URL和基于资源的属性文件引用。请指定其中一个。");
}
// step resource存在,使用Resources工具类加载properties文件
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// step 如果节点里面有属性,在设置到defaults里面
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// step 将属性设置到XPathParser对象里面
parser.setVariables(defaults);
// step 将属性设置到configuration对象里面
configuration.setVariables(defaults);
}
}

这里我们可以看到,在加载​​properties​​​节点的时候,会先加载子节点的数据,然后才会加载​​resource​​​中的数据,所有就会发生​​resource​​​会覆盖​​properties​​节点中子节点数据。

mybatis源码分析之配置文件解析_bc

3.2. ​​setting​​节点解析

​setting​​​配置是​​mybatis​​​中非常重要的配置,一般我们使用默认配置就可以了。我们先看一下​​mybatis​​有哪些全局运行参数:

  • ​cacheEnabled​​​:默认是​​true​​,改配置影响的所有映射器中配置的缓存的全局开关。
  • ​lazyLoadingEnabled​​​:默认是​​true​​​, 延迟加载的全局开关,当开启时,所有关联对象都会延迟加载。特定关联关系中可以通过设置​​fetchType​​属性来覆盖该项的开关状态。
  • ​multipleResultSetsEnabled​​​:默认值是​​true​​,是否允许单一语句返回多结果集(需要兼容驱动)
  • ​useColumnLabel​​​: 默认值是​​true​​,使用列标签代替列名。不同驱动有不同的表现。
  • ​useGeneratedKeys​​​:默认值​​false​​,允许自动生成主键,需要驱动兼容。
  • ​autoMappingBehavior​​​:默认值​​PARTIAL​​​,指定​​mybatis​​​应如何自动映射到字段或者属性。​​NONE​​​:表示取消自动映射,​​PARTIAY​​​:只会自动映射没有定义嵌套结果集映射的结果集,​​FULL​​会自动映射任意复杂的结果集
  • ​autoMappingUnkownColumnBehavior​​​:默认值​​WARNING​​。
  • ​defaultExecutorType​​​:默认值​​SIMPLE​​​,配置默认的执行器。​​SIMPLE​​​:就是普通执行器,​​REUSE​​​:执行器会重用预处理器​​(prepared statements)​​​,​​BATCH​​:执行器将重用语句并执行批量更新。
  • ​defaultStatementTimeout​​​:默认值​​25​​,设置超时时间,它决定驱动等待数据库影响的秒数。
  • ​defaultFetchSize​​​:默认值​​100​​​,为驱动的结果集获取数量(​​fetchSize​​)设置一个建议值。此参数只可以在查询设置中被覆盖。
  • ​safeRowBoundsEnabled​​​:默认值​​false​​​,允许在嵌套语句中使用分页​​RowBounds​​。
  • ​mapUnderscoreToCamelCase​​​:默认值​​false​​,是否开启自动驼峰命名规则映射。
  • ​localCacheScope​​​:​​MyBatis​​​ 利用本地缓存机制(​​Local Cache​​​)防止循环引用和加速重复的嵌套查询。 默认值为 ​​SESSION​​​,会缓存一个会话中执行的所有查询。 若设置值为 ​​STATEMENT​​​,本地缓存将仅用于执行语句,对相同 ​​SqlSession​​ 的不同查询将不会进行缓存。
  • ​jdbcTypeForNull​​​:当没有提供特定的​​JDBC​​​类型时,为空值指定​​JDBC​​​类型。某些驱动需要指定列的​​JDBC​​​类型,多数情况直接用一般类型,比如​​NULL​​​、​​VARCHAR​​​或者​​OTHER​​。
  • ​lazyLoadTriggerMethods​​​:默认值​​equals​​​、​​clone​​​、​​hashCode​​​、​​toString​​,指定哪个对象的方法触发一次延迟加载。

其他的配置可以参看官网:​​https://mybatis.org/mybatis-3/zh/configuration.html​​。

一般来说这些配置我们保持默认就可以。先添加一些​​setting​​的配置。

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

接着我们看一下如何解析​​setting​​节点的。

private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 获取setting属性内容
Properties props = context.getChildrenAsProperties();
// 检查配置类是否知道所有设置
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
// 查看系统的全局配置项中是否存在我们配置的,不存在则跑出异常。
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}

​context.getChildrenAsProperties​​可以获取我们配置的那些配置项。

mybatis源码分析之配置文件解析_类加载器_02

​MetaClass.forClass​​​将用于获取所有的全局配置信息。在里面或包含我们方才配置的那两个属性。这个​​MetaClass​​​具体如何运行的,我们后面分析。(这个地方我们可以先去省略​​MetaClass​​中操作,这个里面方法还是挺多的,我们可以知道其的作用是啥,具体怎么实现后面再看,当成一个黑盒子,不影响我们整体去看源码的结构。)

mybatis源码分析之配置文件解析_加载_03

最后需要将​​setting​​​解析出来的数据设置到​​configuration​​中:

mybatis源码分析之配置文件解析_类加载器_04

这里面我们和​​3.2​​上面的那些全局属性相互对应的看看就可以了。

3.3. ​​typeAliases​​别名节点解析

现在我们先配置一些别名,我们先来测试​​debug​​跟一下代码我们就知道怎么解析别名了

mybatis源码分析之配置文件解析_加载_05

在​​mybatis​​中,我们可以一些类定义一个别名,在使用的时候用直接用别名就可以了,不需要出入类所在类路径。

/**
* 解析typeAliases
* @param parent
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// step 1. 遍历typeAliases节点
for (XNode child : parent.getChildren()) {
// step 2. 判断是否节点名字为 package
if ("package".equals(child.getName())) {
// step 2.1. 获取节点属性的name
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else { // step 3. 判断节点名为typeAlias
// step 3.1. 获取节点属性的alias和type
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// step 3.2. 获取class
Class<?> clazz = Resources.classForName(type);
// step 3.3. 注册别名到映射
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) { // step 4. class没有找到抛出异常
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}

从这个大体的解析过程,我们可以看到​​mybatis​​​解析别名的时候是有两种方法的,第一种是解析​​<package name="com.mly.learn.entity"/>​​​这一中通过扫描包的方式将别名注册到映射,第二种是解析​​<typeAlias type="com.mly.learn.entity.Student" alias="Student" />​​这两个是不可以并存的,我们只能选择其中一种。

接着我们看一下这两种方法是如何注册到映射中的。

首先​​mybatis​​已经注册一堆类型映射。

mybatis源码分析之配置文件解析_bc_06

在有的别名也需要注册到​​typeAliases​​​的​​map​​​中。我们看一下​​mybatis​​​是如何将别名注册到​​map​​中的呢!

public void registerAliases(String packageName, Class<?> superType) {
// 包扫描或者我们在package下的类
ResolverUtil<Class<?>> resolverUtil = new 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);
}
}
}

接着在​​registerAlias​​​判断类上是否有​​Alias​​​注解,有的话将​​value​​​作为​​key​​​,或者将类名作为​​key​​​,然后判断​​typeAliases​​​中是否存在或者​​value​​相同,否则抛出异常。

/**
* 注册别名
* @param type
*/
public void registerAlias(Class<?> type) {
// step 获取类名
String alias = type.getSimpleName();
// step 2. 判断是否存在Alias 注解, 存在的话,将注解中value值作为key, 否则将类名作为key
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}

/**
* 注册alias:class到map中
* @param alias
* @param value
*/
public void registerAlias(String alias, Class<?> value) {
// step 1. 别名== null, 抛出异常
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// step 2. 转成小写字母
String key = alias.toLowerCase(Locale.ENGLISH);
// step 3. 判断typeAliases是否包含key或者value相同,抛出异常
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}

另一种方法,实际调用的还是​​registerAlias​​这个方法一样的。就不做累述。

3.4. ​​typeHandlers​​类型转换节点解析

我们数据库的类型和​​Java​​​类型是不一样的,我们需要进行转换才可以,例如我们在使用​​Java​​​的​​String​​​类型,但是在​​MySql​​​数据类型是​​char​​​或者​​varchar​​​类型。这种数据类型之后的对应就需要​​typeHanlder​​进行准换。

看一下默认注册的一些类型转换器:

mybatis源码分析之配置文件解析_bc_07

接着看一下​​mybatis​​​的是如何处理​​typeHandler​​​的​​xml​​节点的。

private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
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);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}

扫描包下面的​​class​​将其注册到类型转换映射器中,这里面的就是各种重载方法之间的调用,有点回看晕,先画个图看看啊。

先看一下扫描包这个方法执行的过程, 代码就不一一展示了。

mybatis源码分析之配置文件解析_mybatisj配置文件解析_08

public void register(String packageName) {
// 扫描包路径下的class
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
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);
}
}
}

其他的就不一一贴代码了,可以跟着上面的图一一看一下就OK,逻辑不复杂。

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
typeHandlerMap.put(javaType, map);
}
allTypeHandlersMap.put(handler.getClass(), handler);
}

3.5. ​​environments​​节点解析

在​​MyBatis​​​ 中,事务管理器和数据源是配置在​​<environments>​​节点中的。接着我们来看是如何解析的:

private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// step 获取默认节点
environment = context.getStringAttribute("default");
}
// step 遍历environment节点
for (XNode child : context.getChildren()) {
// step 获取id的子节点
String id = child.getStringAttribute("id");
// step 判断是否为指定的节点
if (isSpecifiedEnvironment(id)) {
// step 配置事务
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// step 构建 DataSourceFactory 对象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// step 将事务处理器、数据源和id组装到一起,并设置到configuration中
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}

当我们配置两个数据源的时候,​​mybatis​​​只会读取​​default​​​的数据源​​ID​​​。然后将事务管理器和数据源组装成一个​​Environment​​对象。

mybatis源码分析之配置文件解析_类加载器_09

这个是比较简单。

3.6. ​​plugins​​插件节点解析

这个部分在分析分页插件的时候在去具体分析吧。先挖个坑。