本文是Mybatis相关文章的第七篇,在本篇文章中,我们会介绍下builder模块,这个模块位于org.apache.ibatis.builder包下,这个模块是负责在Mybatis启动时加载配置文件和映射文件的解析。
builder模块对XML文件的解析,使用的前面介绍的解析器模块,在这里我们不会再对这个模块的内容进行介绍,不熟悉的可以先去看第三篇文章。
1 BaseBuilder
BaseBuilder是一个抽象类,这个类是解析配置文件相关builder的顶层父类,这个类的属性如下:
// Configuration是Mybatis中的核心对象 存储了Mybatis中的所有配置
protected final Configuration configuration;
// 类型别名注册器
protected final TypeAliasRegistry typeAliasRegistry;
// 类型转换器注册器
protected final TypeHandlerRegistry typeHandlerRegistry;
BaseBuilder主要的子类如下,这些类我们会在后面的源码跟踪时陆续的进行讲解,在这里只需有个大致的了解即可。
XMLConfigBuilderXMLMapperBuilderXMLStatementBuilderXMLScriptBuilderMapperBuilderAssistantSqlSourceBuilderParameterMappingTokenHandler
2 Mybatis配置文件的解析
配置文件的解析是Mybatis启动时需要完成的工作,大家是否还记得我们示例代码中创建SqlSessionFactory对象的入口是什么吗?是SqlSessionFactoryBuilder.build方法,在这个方法中会进行配置文件的解析,这个方法的逻辑如下:
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.
}
}
}
这里会创建XMLConfigBuilder对象,这个类的属性如下:
// 是否已经解析过mybatis配置文件
private boolean parsed;
// XPathParser对象,Mybatis中的解析器,为XML文件的解析提供了支持
private final XPathParser parser;
// 环境代号
private String environment;
XMLConfigBuilder.parse方法如下:
// 解析Mybatis配置文件的入口
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析<configuration>节点
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"));
// 设置vfsImpl字段
loadCustomVfs(settings);
// 设置logImpl字段
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>节点
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);
}
}
在这个方法中会调用解析各个节点的方法,接下来我们分别看看这些节点的解析逻辑。
2.1 properties节点解析
Mybatis会将properties节点的数据解析成一个个的键值对存放到Properties对象中,propertiesElement方法的逻辑如下:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// XNode中的方法,会将子节点的name和value字段存储到Properties对象中
Properties defaults = context.getChildrenAsProperties();
// 获取<properties>节点的resource和url属性
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.");
}
// 加载properties文件中的配置
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 获取variables属性的值
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 更新XPathParser和Configuration中的variables属性
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
2.2 setting节点解析
setting节点解析会将setting节点的子节点的name和value属性组成键值对,存放到一个Properties对象中,settingsAsProperties方法如下:
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// XNode中的方法 获取子节点的name和value属性 组成键值对
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 获取Configuration类的MetaClass对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
// 遍历解析出的键值对
for (Object key : props.keySet()) {
// 判断Configuration类中是否存在该属性的setter方法 不存在抛出异常
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;
}
这个方法的逻辑也是比较简单的,这里调用了XNode中的方法,之前的文章中介绍过,这里不做过多介绍,处理逻辑如下:
- 调用
XNode.getChildrenAsProperties方法,将settings子节点中的name和value属性组成键值对存储到properties中 - 校验
name属性是否在Configuration对象中存在setter方法,不存在抛出异常
当解析出settings中的配置后,会调用loadCustomVfs和loadCustomLogImpl方法设置Configuration类中的vfsImpl和logImpl属性,这里不粘贴代码了。
之后会在settingsElement方法中使用解析出的值,这个方法是给Configuration中的一些属性设置值,如果没有配置则设置默认值,大家自行看源码吧。
2.3 typeAliases节点解析
typeAliass节点的解析,会将我们配置的类型别名进行注册,其逻辑如下:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 获取<typeAliases>的子节点
for (XNode child : parent.getChildren()) {
// 如果是<package>节点
if ("package".equals(child.getName())) {
// 获取name属性
String typeAliasPackage = child.getStringAttribute("name");
// 按照包名注册配置好的类型别名
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 获取别名
String alias = child.getStringAttribute("alias");
// 获取类型
String type = child.getStringAttribute("type");
try {
// 通过反射加载类
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
// 如果别名为空,使用mybatis的默认名 类名
typeAliasRegistry.registerAlias(clazz);
} else {
// 调用通过别名和类型的注册方法
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
这里使用到了TypeAliasRegistry类,我们在前面的文章中已经进行过详细的介绍了,这里就不进行说明了。
2.4 plugins节点的解析
plugins用来解析我们配置的插件,会将解析出的插件对象存储到Configuration.interceptorChain属性中,源码如下:
private void pluginElement(XNode parent) throws Exception {
// 如果节点不存在则不处理
if (parent != null) {
// 遍历子节点
for (XNode child : parent.getChildren()) {
// 获取interceptor属性
String interceptor = child.getStringAttribute("interceptor");
// 获取子节点配置的属性
Properties properties = child.getChildrenAsProperties();
// 通过反射创建类 会先从TypeAliasRegistry中查找 不存在使用反射加载
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 设置属性
interceptorInstance.setProperties(properties);
// 将解析出的Interceptor对象添加到InterceptorChain对象中
configuration.addInterceptor(interceptorInstance);
}
}
}
2.5 objectFactory、objectWrapperFactory和reflectorFactory节点解析
这三个节点的解析逻辑很相似,我们一起贴出如下:
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// 获取type属性
String type = context.getStringAttribute("type");
// 获取配置的属性
Properties properties = context.getChildrenAsProperties();
// 创建对象
ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 设置properties
factory.setProperties(properties);
// 存储到Configuration.objectFactory属性中
configuration.setObjectFactory(factory);
}
}
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance();
configuration.setObjectWrapperFactory(factory);
}
}
private void reflectorFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ReflectorFactory factory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance();
configuration.setReflectorFactory(factory);
}
}
2.6 environments节点解析
在Mybatis中支持我们根据不同的环境进行配置不同的数据库,这些配置会放到environments节点下,我们调用SqlSessionFacotryBuild.build方法时可以传递环境标识进来,如果不传会使用我们配置的default属性的值,会将解析出的数据存储到Configuration.environment属性中,其逻辑如下:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
// environment如果为空 取default属性配置的值
if (environment == null) {
environment = context.getStringAttribute("default");
}
// 遍历子节点
for (XNode child : context.getChildren()) {
// 获取id属性
String id = child.getStringAttribute("id");
// 判断环境代号是否为environment的值
if (isSpecifiedEnvironment(id)) {
// 获取TransactionFactory对象
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 获取DataSourceFactory对象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 获取DataSource对象
DataSource dataSource = dsFactory.getDataSource();
// 创建Environment.Builder对象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 设置configuration.environment属性
configuration.setEnvironment(environmentBuilder.build());
break;
}
}
}
}
2.7 typeHandlers节点解析
typeHandlers中定义了我们自定义的类型转换器,解析这个节点会将我们配置的类型转换器添加到TypeHandlerRegistry中,这个类在前面的章节中已经详细的介绍过,这里我们直接看解析的逻辑,源码如下:
private void typeHandlerElement(XNode parent) {
if (parent != null) {
// 遍历<typeHandlers>节点的子节点
for (XNode child : parent.getChildren()) {
// 如果是<package>节点
if ("package".equals(child.getName())) {
// 获取配置的包名
String typeHandlerPackage = child.getStringAttribute("name");
// 通过包名注册类型转换器
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 获取配置的javaType、jdbcType和handler属性
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
// 获取javaType对应的Class对象
Class<?> javaTypeClass = resolveClass(javaTypeName);
// 获取JdbcType
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
// 获取handler对应的Class文件
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
// 注册类型转换器
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
2.8 mappers节点解析
mappers节点中定义了我们的Mapper位置,Mybatis通过解析这个标签,将Mapper注册到MapperRegistry对象中,这个对象我们在前面的章节中也介绍过了,这里我们直接看代码逻辑,如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果是package节点
if ("package".equals(child.getName())) {
// 获取节点的name属性
String mapperPackage = child.getStringAttribute("name");
// 通过报名注册mapper
configuration.addMappers(mapperPackage);
} else {
// 获取resource属性
String resource = child.getStringAttribute("resource");
// 获取url属性
String url = child.getStringAttribute("url");
// 获取class属性
String mapperClass = child.getStringAttribute("class");
// 如果recourse属性不为空 其他两个属性为空
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
// 加载Mapper映射文件
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
// 创建XMLMapperBuilder对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析映射文件
mapperParser.parse();
}
// 如果url属性不为空,其他两个属性为空
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 加载映射文件
try(InputStream inputStream = Resources.getUrlAsStream(url)){
// 创建XMLMapperBuilder对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 解析映射文件
mapperParser.parse();
}
// 如果class属性不为空 其他两个属性为空
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
// 如果同时配置了多个属性则抛出异常
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
通过上面的代码我们发现,我们配置了映射文件地址属性的节点,会创建XMLMapperBuilder对象去进行解析,而package节点或者配置mapperClass属性的节点似乎没有配置文件进行加载。
我们知道Mybatis是支持配置文件和注解两种方式来自定义SQL的,其实上面的两种处理逻辑对应的就是这两种方式,无需解析映射文件的是配置是会处理Mapper类中注解。这两种的处理逻辑我们会在下面的内容中进行详细介绍。
Mybatis配置文件的解析我们就介绍到这里,接下来我们看看映射文件的解析。
3 Mapper映射文件的解析
通过第二部分内容的学习,负责解析映射文件的类为XMLMapperBuilder,这个类的属性如下:
// XPathParser对象 解析器
private final XPathParser parser;
// 映射文件解析辅助类
private final MapperBuilderAssistant builderAssistant;
private final Map<String, XNode> sqlFragments;
// mapper文件地址
private final String resource;
MapperBuilderAssistant类是映射文件解析的一个辅助类,这也是BaseBuilder的一个实现类,其属性如下:
// 命名空间
private String currentNamespace;
// 映射文件地址
private final String resource;
// 二级缓存
private Cache currentCache;
// 是否未解析chche-ref标记
private boolean unresolvedCacheRef;
XMLMapperBuilder.parse()方法是解析映射文件的入口,这个方法的逻辑如下:
public void parse() {
// 判断是否已经加载过该映射文件
if (!configuration.isResourceLoaded(resource)) {
// 处理mapper节点
configurationElement(parser.evalNode("/mapper"));
// 将映射文件记录到Configuration.loadedResources字段中 是一个Set
configuration.addLoadedResource(resource);
// 注册Mapper
bindMapperForNamespace();
}
// 处理解析失败的ResultMap
parsePendingResultMaps();
// 处理解析失败的cache-ref
parsePendingCacheRefs();
// 处理解析失败的SQL
parsePendingStatements();
}
3.1 解析标签
confirurationElement方法是解析标签的方法,这个方法如下:
private void configurationElement(XNode context) {
try {
// 获取namespace属性
String namespace = context.getStringAttribute("namespace");
// namespace不存在抛出异常
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置builderAssistant字段的currentNamespace属性
builderAssistant.setCurrentNamespace(namespace);
// 解析cache-ref节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析cache节点
cacheElement(context.evalNode("cache"));
// 解析parameterMap节点
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析resultMap节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析sql节点
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 select insert update delete 节点
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);
}
}
3.1.1 cache-ref标签解析
cache-ref标签可以为当前mapper文件引入其他mapper的二级缓存,这个标签的解析逻辑如下:
private void cacheRefElement(XNode context) {
if (context != null) {
// Configuration.cacheRefMap属性中添加数据 当前命名空间和节点的namespace属性进行关联
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// 创建CacheRefResolver对象
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 解析cacheRef
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
// 如果解析发生异常添加到incompleteCacheRef字段中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
上面的逻辑会调用CacheRefResolver相关的接口,这个类的源码如下:
public class CacheRefResolver {
// Mapper解析辅助类
private final MapperBuilderAssistant assistant;
// cache-ref节点的namespace属性值
private final String cacheRefNamespace;
public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
this.assistant = assistant;
this.cacheRefNamespace = cacheRefNamespace;
}
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
}
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 从configuration中通过命名空间获取Cache
Cache cache = configuration.getCache(namespace);
// 如果不存在抛出异常
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
3.1.2 cache标签解析
cache标签是用来配置二级缓存的,这个标签的解析我们在上篇文章中进行介绍过,其源码如下:
private void cacheElement(XNode context) {
if (context != null) {
// 获取type属性 默认为PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
// 通过别名获取对应的类型
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获取eviction属性 默认为LRU
String eviction = context.getStringAttribute("eviction", "LRU");
// 获取eviction别名对应的类型
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 获取flushInterval属性
Long flushInterval = context.getLongAttribute("flushInterval");
// 获取size属性
Integer size = context.getIntAttribute("size");
// 获取redaOnly属性 默认为false
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 获取blocking属性 默认为false
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取子节点数据
Properties props = context.getChildrenAsProperties();
// 创建Cache对象并添加到Configuration.caches中
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 创建Cache对象
Cache cache = new CacheBuilder(currentNamespace)// namespace作为缓存id
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 将cache添加到configuration中
configuration.addCache(cache);
// 记录当前命名空间使用的cache
currentCache = cache;
return cache;
}
通过这个代码我们发现,cache和cache-ref节点的解析最后都会修改MapperBuilderAssistant.currentCache属性的值,因此,当这两个标签都存在时会存在覆盖的问题。
3.1.3 parameterMap标签解析
通过官方文档我们看到这个标签已经被废弃了,这里我们就不再进行说明了。
3.1.4 resultMap节点解析
在介绍这个标签前我们先看看resultMap节点下都可能存在哪些节点,从官网复制个例子出来如下:
Mybatis对这个节点的解析,会将一条条的映射关系解析成一个个的ResultMapping对象,将整个resultMap解析成ResultMap对象。
ResultMapping的属性如下:
// Configuration对象
private Configuration configuration;
// property属性
private String property;
// column属性
private String column;
// 字段对应的Java类型 javaType属性指定
private Class<?> javaType;
// 字段对应的jdbc类型 jdbcType属性指定
private JdbcType jdbcType;
// 类型转换器 typeHandler属性
private TypeHandler<?> typeHandler;
// 这个是通过resultMap属性解析出的值
private String nestedResultMapId;
// select属性解析出的值
private String nestedQueryId;
// notNullColumn属性解析的值
private Set<String> notNullColumns;
// columnPrefix属性
private String columnPrefix;
// 标记 用来记录是否为 constructor和id节点
private List<ResultFlag> flags;
private List<ResultMapping> composites;
// resultSet属性
private String resultSet;
// foreignColumn属性
private String foreignColumn;
// 是否懒加载
private boolean lazy;
ResultMap的属性如下:
// Configuration对象
private Configuration configuration;
// resultMap节点的id属性
private String id;
// resultMap节点的type属性
private Class<?> type;
// resultMapping列表 不包含discriminator节点解析的ResultMapping
private List<ResultMapping> resultMappings;
// 映射关系中带有id节点 resultMapping
private List<ResultMapping> idResultMappings;
// constructor节点的resultMapping
private List<ResultMapping> constructorResultMappings;
private List<ResultMapping> propertyResultMappings;
// column属性列表
private Set<String> mappedColumns;
// properties属性列表
private Set<String> mappedProperties;
// discriminator节点解析出的数据
private Discriminator discriminator;
// 是否有resultMap属性
private boolean hasNestedResultMaps;
// 是否有select属性
private boolean hasNestedQueries;
private Boolean autoMapping;
解析逻辑源码如下:
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取标签的类型属性
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 获取到类型的Class对象
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
// ResultMapping列表 会将子节点解析成一个个的ResultMapping
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
// 解析constructor节点 这个节点用于配置构造方法所需的属性
if ("constructor".equals(resultChild.getName())) {
// 解析constructor节点的子节点
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
// 解析discriminator节点
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// 标记列表
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 如果是id标签加上ResultFlag.ID标签
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 获取id属性
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
// 获取extends属性
String extend = resultMapNode.getStringAttribute("extends");
// 获取autoMapping属性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 创建ResultMapResolver对象
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 创建ResultMap对象并返回
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
// 如果解析失败,将resultMapResolver添加到Configuration.incompleteResultMap中 在后面进行处理
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
processConstructorElement方法用来解析constructor节点,逻辑如下:
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
List<XNode> argChildren = resultChild.getChildren();
// 遍历子节点
for (XNode argChild : argChildren) {
// 标记列表
List<ResultFlag> flags = new ArrayList<>();
// 添加CONSTRUCTOR标记
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
// 如果是idArg节点 添加ID标记
flags.add(ResultFlag.ID);
}
// 解析出子节点对应的ResultMapping对象
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
processDiscriminatorElement方法用来解析discriminator节点,逻辑如下:
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
// 获取column属性
String column = context.getStringAttribute("column");
// 获取javaType属性
String javaType = context.getStringAttribute("javaType");
// 获取jdbcType属性
String jdbcType = context.getStringAttribute("jdbcType");
// 获取typeHandler属性
String typeHandler = context.getStringAttribute("typeHandler");
// 获取javaType对应的Class对象
Class<?> javaTypeClass = resolveClass(javaType);
// 获取typeHandler对应的Class对象
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Map<String, String> discriminatorMap = new HashMap<>();
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute("value");
String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType));
discriminatorMap.put(value, resultMap);
}
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}
buildResultMappingFromContext方法是用来创建ResultMapping对象,逻辑如下:
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
// 如果是Constructor节点下的节点 获取name属性
property = context.getStringAttribute("name");
} else {
// 获取property属性
property = context.getStringAttribute("property");
}
// 获取column属性
String column = context.getStringAttribute("column");
// 获取javaType属性
String javaType = context.getStringAttribute("javaType");
// 获取jdbcType属性
String jdbcType = context.getStringAttribute("jdbcType");
// 获取select属性
String nestedSelect = context.getStringAttribute("select");
// 获取resultMap属性
String nestedResultMap = context.getStringAttribute("resultMap", () ->
processNestedResultMappings(context, Collections.emptyList(), resultType));
// 获取notNullColumn属性
String notNullColumn = context.getStringAttribute("notNullColumn");
// 获取columnPrefix属性
String columnPrefix = context.getStringAttribute("columnPrefix");
// 获取typeHandler属性
String typeHandler = context.getStringAttribute("typeHandler");
// 获取resultSet属性
String resultSet = context.getStringAttribute("resultSet");
// 获取foreignColumn属性
String foreignColumn = context.getStringAttribute("foreignColumn");
// 是否支持懒加载
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
// 解析出javaType对应的Class对象
Class<?> javaTypeClass = resolveClass(javaType);
// 解析出typeHandler对应的Class对象
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
// 获取到对应的JdbcType
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 创建ResultMapping对象
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
解析完resultMapping后会创建ResultMapResolver对象来创建ResultMap并添加到Configuration.resultMaps属性中,逻辑如下:
public class ResultMapResolver {
private final MapperBuilderAssistant assistant;
private final String id;
private final Class<?> type;
private final String extend;
private final Discriminator discriminator;
private final List<ResultMapping> resultMappings;
private final Boolean autoMapping;
public ResultMapResolver(MapperBuilderAssistant assistant, String id, Class<?> type, String extend, Discriminator discriminator, List<ResultMapping> resultMappings, Boolean autoMapping) {
this.assistant = assistant;
this.id = id;
this.type = type;
this.extend = extend;
this.discriminator = discriminator;
this.resultMappings = resultMappings;
this.autoMapping = autoMapping;
}
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}
}
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
// 在id属性前面配置上namespace.作为id
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
// extend不为空 判断对应的resultMap是否存在
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
// 获取extend对应的ResultMap
ResultMap resultMap = configuration.getResultMap(extend);
// 获取对应的ResultMapping列表
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
// 移除当前resultMap中包含的ResultMapping
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
boolean declaresConstructor = false;
// 判断当前ResultMapping中是否存在CONSTRUCTOR标签的
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
// 如果当前ResultMap中存在CONSTRUCTOR标签的ResultMapping 移除扩展ResultMap中含有CONSTRUCTOR标记的ResultMapping
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
// 添加扩展ResultMapping
resultMappings.addAll(extendedResultMappings);
}
// 创建ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
// 添加到Configuration.resultMaps属性中
configuration.addResultMap(resultMap);
return resultMap;
}
3.1.5 sql节点解析
sql的节点解析是添加到sqlFragments属性中,其源码如下:
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
// 获取id属性值
String id = context.getStringAttribute("id");
// 在id属性值前加上namespace
id = builderAssistant.applyCurrentNamespace(id, false);
// 校验databaseId是否匹配
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// 存储到sqlFragments中
sqlFragments.put(id, context);
}
}
}
3.1.6 insert update delete select节点 解析
buildStatementFromContext是解析我们写的SQL语句节点的入口,这个方法的逻辑如下:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 创建XMLStatementBuilder对象
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析StatementNode
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
通过上面的逻辑我们发现最终的解析逻辑调用的是XMLStatementBuilder中的方法。接下来我们看看这个类的解析过程
3.1.7 XMLStatementBuilder
XMLStatmentBuilder也是BaseBuilder的一个实现类,通过上面的源码我们知道XMLStatmentBuilder.parseStatementNode()方法是解析SQL语句的入口,其逻辑如下:
public void parseStatementNode() {
// 获取id属性
String id = context.getStringAttribute("id");
// 获取databaseId属性
String databaseId = context.getStringAttribute("databaseId");
// 获取到的databaseId和创建时设置的databaseId不一致 不处理
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点名称
String nodeName = context.getNode().getNodeName();
// 获取sql类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 是否为查询
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 是否刷新缓存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 是否使用缓存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
// 处理include节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 获取parameterType属性
String parameterType = context.getStringAttribute("parameterType");
// 获取parameterType类型
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取lang属性
String lang = context.getStringAttribute("lang");
// 获取LanguageDriver对象
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
// selectKey节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这段代码看着很长,大部分内容是来获取相应的属性值,刨除这些内容,主要的流程如下:
- 处理
include节点 - 创建
LanguageDriver对象 - 创建
SqlSource对象 - 创建
MappedStatement对象
处理include数据的入口为XMLIncludeTransformer.applyIncludes方法,逻辑如下:
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
// 获取配置文件中的配置内容
Properties configurationVariables = configuration.getVariables();
Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
applyIncludes(source, variablesContext, false);
}
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
// 判断是否为include节点
if ("include".equals(source.getNodeName())) {
// 查找refid对应的sql语句
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
// 获取子节点中的name和value属性组成Properties对象 会替换value中的占位符
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// 递归处理
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
// 获取子节点
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 处理子节点
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// replace variables in text node
// 替换其中的占位符
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
获取LanguageDriver的获取逻辑比较简单哈,默认情况下返回的是Configuration.languageRegistry.defaultDriver,这个值的设置我们可以在Configuration类的构造方法中看到,默认是XMLLanguageDriver。
获取到LanguageDriver对象后会调用这个类的createSqlSource方法获取SqlSource对象,逻辑如下:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 创建XMLScriptBuilder对象
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 调用XMLScriptBuilder.parseScriptNode方法创建SqlSource对象
return builder.parseScriptNode();
}
XMLScriptBuilder.parseScriptNode()方法逻辑如下:
public SqlSource parseScriptNode() {
// 判断是否为动态sql并获取MixedSqlNode
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 判断是否为动态sql
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 获取子标签
NodeList children = node.getNode().getChildNodes();
// 遍历子标签
for (int i = 0; i < children.getLength(); i++) {
// 创建XNode对象
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 判断是否为动态sql 包含${}判定为动态sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 如果是动态sql标签则是动态sql
String nodeName = child.getNode().getNodeName();
// 通过节点名称获取对应的NodeHandler对象
NodeHandler handler = nodeHandlerMap.get(nodeName);
// NodeHandler不存在 抛出异常
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 会创建对应的SqlNode对象添加到列表中
handler.handleNode(child, contents);
// 标记为是动态sql
isDynamic = true;
}
}
// 创建MixedSqlNode对象并返回
return new MixedSqlNode(contents);
}
创建MappedStatement对象,这个是调用了MapperBuilderAssistant.addMappedStatement方法,将创建的对象存储到Configuration.mappedStatements属性中,这里不进行粘贴了。
通过这部分代码我们能够发现,我们在Mapper映射文件中定义的select insert update delete节点会被转换成MappedStatement对象,这个节点中写的sql会被转换成SqlSource对象,动态sql会被转换成SqlNode对象。
3.2 绑定Mapper
当解析完成映射文件后,会调用bindMapperForNamespace方法,逻辑如下:
private void bindMapperForNamespace() {
// 获取当前namespace
String namespace = builderAssistant.getCurrentNamespace();
// 如果namespace不为空
if (namespace != null) {
Class<?> boundType = null;
try {
// 获取namespace对应的Class对象
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
// 如果没有进行绑定
if (boundType != null && !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
// loadedResource中添加数据
configuration.addLoadedResource("namespace:" + namespace);
// 注册mapper
configuration.addMapper(boundType);
}
}
}
3.3 parsePending*方法
通过上面的方法源码,我们发现当解析resultMap、cache-ref和statement失败后会添加到相对应的集合中,这里面的逻辑比较简单,大家自行查看吧。
在这里我们梳理下同时配置cache和cache-ref的覆盖逻辑,如下:
- 解析
cache-ref,解析成功会设置当前缓存对象,解析失败会添加到一个集合中。 - 解析
cache标签时如果上一步成功的解析,这个标签的解析会覆盖掉上步的缓存。 - 处理解析失败列表,覆盖掉上步解析的缓存对象。
3.4 注解方法的解析
相信大家都知道Mybats除了映射文件的方式,还支持我们使用注解进行SQL编写,那么Mybatis对Mapper上的注解是在那里解析的呢?我们在这部分看一下。
大家是否还记得,当我们使用MapperRegistry注册Mapper后,会调用MapperAnnotationBuilder.parse方法,这个咱们在前面的章节跳过了没有说明,这个便是解析Mapper注解的入口,其逻辑如下:
public void parse() {
// Mapper全限定名
String resource = type.toString();
// 如果已经处理过则不处理
if (!configuration.isResourceLoaded(resource)) {
// 加载映射文件
loadXmlResource();
configuration.addLoadedResource(resource);
// 设置当前命名空间
assistant.setCurrentNamespace(type.getName());
// 解析cache @CacheNamespace注解
parseCache();
// 解析cache-ref @CacheNamespaceRef注解
parseCacheRef();
// 获取类型中的方法
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
// 解析ResultMap
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// 解析statement
parseStatement(method);
} catch (IncompleteElementException e) {
// 解析失败添加到incompleteMethod中
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 处理解析失败的方法
parsePendingMethods();
}
其他的代码大家自行查看吧。
4 总结
今天的内容到这里就结束了,今天我们详细的介绍了下Mybatis的配置解析相关的内容,篇幅较长,需要大家耐心的查看,在本篇文章中设计到比较重要的类如下:
XMLConfigBuilder解析Mybatis配置文件XMLMapperBuilder解析Mapper映射文件XMLStatementBuilder解析Mapper映射文件中的insert update delete select节点MapperBuilderAssistant解析Mapper映射文件的辅助类XMLScriptBuilder解析SQL语句 处理动态SQLMappedStatement用来表示insert update delete select节点的数据ResultMapping存储resultMap节点下的子节点 一个子节点会封装成一个该对象ResultMapresultMap对应的Java类SqlSource用来表示一条SQL语句SqlNode用来表示语句中的动态SQL
希望大家通过本文能对Mybatis的配置解析流程有一个大概的了解,能够知道Mybatis在这个阶段都做了哪些工作。
欢迎关注公众号:Bug搬运小能手