Mybatis随笔(二) 配置文件加载
- 先看纯 Mybatis 下的配置文件加载
- 再看在 SpringBoot 容器中的一些不同
一、概述
Mybatis
配置文件加载的方式非常简单, 就是读取并解析配置文件然后放到一个指定的数据结构中
而纯Mybatis
项目的配置文件就两种
mybatis-config.xml
项目配置文件, 配置数据库信息/基本属性等
*Mapper.xml
SQL配置文件, 一般每个配置文件都对应一张表, 配置了具体执行SQL
Mybatis
在解析mybatis-config.xml
时就会顺带着把*Mapper.xml
给解析了, 这归功于mybatis-config.xml
的<mappers />
标签了
Mybatis
初始化过程:
SqlSessionFactoryBuilder
通过对配置文件的解析,把配置文件放入Configuration
对象中,再以这个Configuration
来构造SqlSessionFactory
,这里就是DefaultSqlSessionFactory
了
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
所以解析的重点是这个Configuration
类了。
二、解析
本流程不深入讲解
XNode
/XpathParser
等基础解析
-
XML****Builder
这种名字的类的作用都是解析指定文件,比如XMLConfigBuilder
解析Mybatis
的配置文件,XMLMapperBuilder
解析*Mapper.xml
之类的SQL配置文件 -
XML****Builder
都有一个基类BaseBuilder
,此类有三个属性
![](https://img.haomeiwen.com/i15085536/96c37baf953cba89.png)
其中一个属性就是Configuration
,所以各种解析类解析来解析去,其实都是设置这个Configuration
,抓住这条主线,理解配置解析整个过程就没那么难了
就一个一个来看下吧
- 首先调用
SqlSessionFactoryBuilder#build
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
XMLConfigBuilder
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 开始解析配置文件
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析标签 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);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析标签 environments
environmentsElement(root.evalNode("environments"));
// 解析标签 databaseIdProvider 数据库厂商标识, 用于识别同项目中不同的数据库厂商(e.g. Mysql/Oracle)
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);
}
}
上面这些标签解析的源码没什么难懂的。也可以看到有哪些标签,而这些标签又有哪些属性,都能一一看到,就挑几个随便看看
properties
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析标签 <properties/> 为 Properties 对象
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 将解析出来的 Properties 对象放入 configuration
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
就两步
<1> 解析标签 <properties/>
为 Properties
对象
<2> 将解析出来的 Properties
对象放入 configuration
settings
主要分三步
<1>vfsImpl
loadCustomVfs(settings);
vfs是虚拟文件系统,主要是通过该配置可以加载自定义的虚拟文件系统应用程序,实现是一种单例
<2>logImpl
loadCustomLogImpl(settings);
指定日志实现
<3>其他配置
settingsElement(settings);
->
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
...
-
objectFactory
/objectWrapperFactory
/reflectorFactory
这是三个典型的工厂模式,实现都非常简单,就是确定这个类存在后构建对应的factory
,然后将之放入configuration
<1>objectFactory
对象工厂,重写DefaultObjectFactory
,还支持外部配置传参,对应方法实现为setProperties
,此类可以自定义构造实例
<2>objectWrapperFactory
默认实现 DefaultObjectWrapperFactory
<3>reflectorFactory
默认实现 DefaultReflectorFactory
,用于构造反射器
environments
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// 数据源唯一标识
String id = child.getStringAttribute("id");
// 初始化只添加默认数据源
if (isSpecifiedEnvironment(id)) {
// 构造 environment (TransactionFactory/DataSource)
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
<1>初始化只添加默认数据源,如果没有默认数据源会报错
<2>这里对 environment
赋值了后面马上用到
databaseIdProvider
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
Properties properties = context.getChildrenAsProperties();
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}
<1>这个标签的作用是为了兼容不同数据库厂商,注意,这里不是说多数据源,而是MYSQL
ORACLE
之类数据库厂商间的共存
<2>可以在SQL执行上添加一个databaseId
来指定数据库厂商
<insert id="insert" parameterType="Account" databaseId="mysql">
...
<insert id="insert" parameterType="Account" databaseId="oracle">
...
不过这种并非动态切换,而是根据你配置的数据源是什么厂商,也就是说MYSQL
和 ORACLE
你只能配一个
<3>这个标签只接受一个type
和N个<property>
,属性里配置的就是数据库厂商的别名
<4>可见configuration
里面存的是当前数据源的databaseId
, 因为数据源是从environment
里面取出来的,这个databaseId
就是配置中对应的value
,不过需要name
写对数据库的productName
,参考源码可以如下方式获取productName
try (InputStream is = Resources.getResourceAsStream(configPath)) {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
DatabaseMetaData metaData = sqlSession.getConnection().getMetaData();
log.info("---- version: " + metaData.getDatabaseProductVersion());
log.info("---- name: " + metaData.getDatabaseProductName());
} catch (Exception ignore) {
}
输出
---- version: 5.7.26
---- name: MySQL
mysql
的正确配置应该是 (注意大小写)
<property name="MySQL" value="mysql" />
<5>最后就是,这个type
其实是个别名或间写,比如源码中的VENDOR
和DB_VENDOR
了解一下就行
typeHandlers
这个配置不再是往configuration
中扔值了,而是换成了typeHandlerRegistry
,代码就不贴了
mappers
这个是配置解析中最复杂的了,因为它里面包含了解析*Mapper.xml
和 *Mapper.xml
与*Mapper.java
互相引用就加载注册
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// package - name
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// mapper - resource
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// mapper - url
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// mapper - class
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
// only one
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
解析流程非常清晰,拿到mappers
下的子节点,然后遍历处理
接下来我们一个一个看下具体实现,但在这之前我们需要明确两点
<1>子节点只有两种<mapper>
或<package>
<2>如果子节点是<mapper>
,那么其三个属性中只能有一个有值,两个以上有值的会报错
1.package - name / mapper - class
这两种配置实现本质一样,只不过一个是批量一个是单个,放一起讲了
--- Configuration ---
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
--- MapperRegistry ---
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
传入Object.class
表示只要目录下所有类都要, 不做父类过滤
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 找出 packageName 目录下是 superType 子类的
// if (child.endsWith(".class")) { addIfMatching(test, child); }
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
// 遍历这些类进行 addMapper
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
到目前为止,拿到配置的包下面所有class
文件,遍历调用addMapper(String)
,这个方法非常重要(后面几个方式也会用到),重点看一下
public <T> void addMapper(Class<T> type) {
// 必须是接口 SQLMapper
if (type.isInterface()) {
// 避免重复注册
// return knownMappers.containsKey(type);
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
这里有一个很重要的判断,hasMapper(type)
源码我直接贴在注释上了,就是通过一个Map<Class<?>, MapperProxyFactory<?>>
来判断是否已经处理(存在),很显然,方法内将当前处理的type
放入了knownMappers
接下来又是一个解析,这里就好玩了
--- MapperAnnotationBuilder ---
public void parse() {
String resource = type.toString();
// 确保没有解析过
// return loadedResources.contains(resource);
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
这里可以暂时只看两个方法,一个是loadXmlResource();
,加载同级目录下同名 xml 配置,源码如下
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 加载同级目录下同名 xml 配置
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
// 解析 *Mapper.xml
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
而且最后在构造XMLMapperBuilder
时设置了namespace
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
this(inputStream, configuration, resource, sqlFragments);
this.builderAssistant.setCurrentNamespace(namespace);
}
XMLMapperBuilder # parse
方法我们等下跟mapper
两个属性解析一起讲
还有一个是assistant.setCurrentNamespace(type.getName());
,这个方法也设置了namespace
public void setCurrentNamespace(String currentNamespace) {
if (currentNamespace == null) {
throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
}
if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
throw new BuilderException("Wrong namespace. Expected '"
+ this.currentNamespace + "' but found '" + currentNamespace + "'.");
}
this.currentNamespace = currentNamespace;
}
这里两处用的都是type.getName()
当然一致,但其他方式设置时就需要注意这点。
总结一下
- 注册目录下所有
class
类,并加载解析同级目录下的同名XML
文件 - 设置类名为
namespace
,因为这里已经是类名了所以肯定能加载到
package
我们就解析到这,继续看下mapper
的另外两种配置的实现
mapper - resource/ mapper - url
不知道有没有注意到,在一些解析步骤开始前,都会判断一下是否解析过。Mybatis
初始化是单线程执行的,不存在并发执行那么为什么会发现重复解析的情况?
很简单,两种情况
- 防止重复配置,比如你配置了两个一模一样的路径
- 若解析包名,先注册
class
文件,然后又会找到xml
文件进行解析,而解析xml
文件时,又会将与之关联的class
文件再解析一遍!
只是入口不同,一个从
class
到xml
,一个是从xml
到class
,为了精简代码,加个容器字段加个判断就行了没必要再额外写一个解析方法
就是XMLMapperBuilder # parse
这个方法,XMLMapperBuilder
这个类前面我们说过是专门处理SQL
配置文件的,那么很明显<mapper url/resource>
配置的都是 XML
文件路径,一个相对,一个绝对
这两种方式的处理几乎是一模一样的,所以我们直接来看下方法XMLMapperBuilder # parse
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析 mapper 内部标签
configurationElement(parser.evalNode("/mapper"));
// 避免重复处理
configuration.addLoadedResource(resource);
// 处理 namespace
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
这里主要看两个方法configurationElement("/mapper")
和bindMapperForNamespace()
-
configurationElement("/mapper")
全部标签解析
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
-
namespace
的非空校验 - 其他标签的解析用于填充
configuration
, 尤其buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
值得一读,限于篇幅就不展开了 - 简单来说就是将
SQL方法
的ID
与MappedStatement
作为键值对放入configuration
的字段mappedStatements
中,其中id
有两种,一种是带namespace
的,一种不带,所以mappedStatements
的大小是实际SQL
方法数量的两倍,实现如下
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
// 一次
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
// 二次
return super.put(key, value);
}
// 取出方法名
private String getShortName(String key) {
final String[] keyParts = key.split("\\.");
return keyParts[keyParts.length - 1];
}
另外,方法名直接对应的StrickMap
为其内部类Ambiguity
protected static class Ambiguity {
final private String subject;
public Ambiguity(String subject) {
this.subject = subject;
}
public String getSubject() {
return subject;
}
}
-
bindMapperForNamespace()
非常简单
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 尝试对 namespace 进行类加载
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
由此可以看出,namespace
属性如果没有配置成XML
文件对应的实体接口会加载不到导致configuration
的loadedResources
没有存下对应的namespace
,可能有以下不可预测的情况
-
mappedStatement
找不到对应执行SQL
- 重复加载
三、构造
配置文件解析完成后就可以构造SqlSessionFactory
了
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
返回的是SqlSessionFactory
的默认实现DefaultSqlSessionFactory
,至此配置文件的加载流程就结束了。
四、其他
类Configuration
的校验是否存在都是通过其某个容器字段来判断,大致分两种
mapperRegistry
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
而MapperRegistry
又有字段
private final Configuration config;
Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
注意,其内部也有一个Configuration
这个容器主要用来判断 Class
是否加载过,判断方式
if (configuration.hasMapper(Class<T> type)) {}
loadedResources
protected final Set<String> loadedResources = new HashSet<>();
这就很简单,一个不会重复的HashSet
- 通过命名空间判断是否加载 XML (
namespace: + clazz.getName()
) - 命名空间是否有效取决于是否能被类加载器加载
判断方式
if (configuration.isResourceLoade(String resource)) {}
本来以为能写很多的,结果写起来查漏补缺,堪堪写完这部分就已经十二点多了,我要泡杯奶粉反省一下自己,明天继续GAN
~