今天来看下读取xml配置构建SqlSessionFactory是如何实现的,这块整个实现是最全的,对了解整个mybatis工作原理十分有帮助。
由xml配置构建SqlSessionFactory
入口是在SqlSessionFactoryBuilder.build方法。实现很简答:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
看着很长,有用的就两行,这里主要看parser.parse()
干了什么?
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
可以看到这里解析了configuration
标签下面的信息,然后看parseConfiguration
方法,实现如下:
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
root.evalNode
代表解析xml里面某个标签下面的数据,比如root.evalNode("properties")
就是解析properties
标签的数据,承接上文,configuration
标签下面的properties
标签值。
然后再看代码就清晰很多了,这里面会解析所有mybatis-config.xml
里面的标签,然后干嘛呢?讲这些东西都放到configuration
里面去,所以在mybatis
里面你需要的所有信息基本都可以再configuration
里面读到。相应的你也可以根据这些代码去自己手动生成Configuration,mybatis-spring-boot-starter
就是这么实现的。
这块代码你可以根据你自己想要了解的逻辑去细看,比如你想知道settings如果没有配置的默认实现是什么,就可以直接点进去settingsElement
方法去看,
private void settingsElement(Properties props) {
configuration
.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(
AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\我是省略符\\\\\\\\\\\\\\\\\\\\\\\\\\\\
configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
configuration.setArgNameBasedConstructorAutoMapping(
booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false));
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
}
这里可以看到autoMappingBehaviorruguo
如果没配默认值就是PARTIAL
。cacheEnabled
默认是true等。
我们来看下前面说过的typeAliases
的实现方法typeAliasesElement
:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
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) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
可以看出如果是扫描的package,那么就是直接拿的注解或者类的值,否则直接拿的标签里面的alias
参数。
这里重点看下mapper标签的解析逻辑,它在mapperElement
方法里,
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
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) {
ErrorContext.instance().resource(resource);
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments());
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());
mapperParser.parse();
}
} 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.");
}
}
}
}
}
可以看出是分情况放到Configuration
里面,如果是package标签,直接扫描整个包下面的类add到configuration
里面。然后看resource
,url
和mapperClass
这三个哪个有就读哪个,优先级是resource>url>mapperClass
。
然后看下mapperPaser.parse()
,它的核心实现是在XMLMapperBuilder.configurationElement
里面,代码如下:
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);
}
}
可以看到我们能用到的标签这里都有对应的解析,比如cache,cache-ref,mapper标签里面的parameterMap,
resultMap,sql,select|insert|update|delete
等。
这里主要看下buildStatementFromContext
实现,其他的你可以根据自己感兴趣的看。这个方法最后核心实现是XMLStatementBuilder.parseStatementNode()。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
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
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
--------------------------------我是省略符--------------------------------------
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}
在解析完所有的标签信息后,会调用builderAssistant.addMappedStatement
去生成MappedStatement
放到configuration
里面,这个MappedStatement
也是Mybatis
里面非常重要的一个类,最后Executor去执行数据库操作的sql相关信息都是从这里面来的。
总结
今天整体了解了下sqlsessionFactoryBuilder读取配置文件信息整个流程是怎么样的,具体做了些什么事,当然没有说很细,因为东西太多了,只说了一些重要的实现,有兴趣的朋友可以根据自己想知道的地方针对性去看。
明天我们来看下Mybatis的动态代理实现,它到底是如何让Mapper映射到xml文件的。