SqlSessionFactoryBuilder
首先創建了一個SqlSessionFactoryBuilder對象,然后調用該對象的build方法加載全局XML配置的流文件構建出一個SqlSessionFactory對象。
//指定全局配置文件路徑
String resource = "org/mybatis/example/mybatis-config.xml";
//加載配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
//構建者模式創建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
查看一下SqlSessionFactoryBuilder的源碼:
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
SqlSessionFactoryBuilder只有一堆重載的build方法,除了build(Configuration)方法,其他方法的參數都是輸入流,最終由build(Configuration)方法生成SqlSessionFactory對象,下面來看如何構建Configuration對象。
XMLConfigBuilder解析配置文件
從XMLConfigBuilder類名就可以看出,這是用來解析XML配置文件的類,其父類為BaseBuilder。
BaseBuilder還包含了MapperBuilderAssistant, SqlSourceBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLScriptBuilder, XMLStatementBuilder等子類,這些子類都是用來解析MyBatis各個配置文件,他們通過BaseBuilder父類共同維護一個全局的Configuration對象,
XMLConfigBuilder的作用就是解析全局配置文件,調用BaseBuilder其他子類解析其他配置文件,生成最終的Configuration對象。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
查看XMLConfigBuilder源碼,可以得知XML配置文件最終是由org.apache.ibatis.parsing.XPathParser封裝的XPath解析的。通過XpathParser構造方法傳入我們讀取的XML流文件、Properites流文件和environment等參數得到了一個XpathParser實例對象parser,這里parser已包含全局XML配置文件解析后的所有信息,再將parser作為參數傳給XMLConfigBuilder構造方法。XMLConfigBuilder構造方法調用其父類BaseBuilder的構造方法BaseBuilder(Configuration),構造出一個XMLConfigBuilder對象。值得注意的是,這里BaseBuilder構造方法參數是一個初始化的Configuration對象,Configuration對象初始化的時候,內置的別名注冊器TypeAliasRegistry注冊了默認的別名:
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
...
所以XML配置文件里可以直接用這些別名。
此時我們已經得到了XMLConfigBuilder對象,再看SqlSessionFactoryBuilder的build方法,將XMLConfigBuilder實例對象parser調用parser()方法得到的Configuration實例對象config作為參數,調用SqlSessionFactory接口的實現類DefaultSqlSessionFactory構造出SqlSessionFactory對象。
XMLConfigBuilder對象在調用parser()方法時,會讀出所有所有配置文件,將配置文件解析后保存在Configuration對象中。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//參數是<configuraton>標簽根節點
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(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);
}
}
XMLConfigBuilder的parseConfiguration(XNode)方法把XML全局配置文件中每一個節點的信息都讀取出來,保存在一個Configuration對象中,Configuration分別對以下內容做出了初始化:
- properties 屬性
- settings 設置
- typeAliases 類型別名
- typeHandlers 類型處理器
- objectFactory 對象工廠
- plugins 插件
- environments 環境
- databaseIdProvider 數據庫廠商標識
- mappers 映射器
這里對properties和mappers的初始化進行分析:
properties的初始化
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
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 vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
XMLConfigBuilder的getChildrenAsProperties()方法讀取properties標簽的子節點,保存到Configuration對象的Properties屬性里,這里可以看出只能在resource和url兩種方式中二選一來加載外部properties配置文件,如果外部properties文件里面屬性名和主配置XML文件properties標簽的子元素屬性重名,則會覆蓋主配置文件的屬性值,然后將初始化的Configuration對象中的Properties與解析配置文件后封裝好的Properties合並,最后再將Properties保存到Configuration對象中。
mappers的初始化
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);
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);
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.");
}
}
}
}
}
遍歷mappers標簽下所有子節點
- 如果遍歷到package子節點,是以包名引入映射器,則將該包下所有Class注冊到Configuration的mapperRegistry中。
- 如果遍歷到mapper子節點的class屬性,則將制定的Class注冊到注冊到Configuration的mapperRegistry中。
- 如果遍歷到mapper子節點的resource或者url屬性,則直接對資源文件進行解析:
首先構建一個XMLMapperBuilder對象,構建過程如下
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
XPathParser將mapper配置文件解析成Document對象后封裝到一個XPathParser對象,再將XPathParser對象作為參數傳給XMLMapperBuilder構造方法並構造出一個XMLMapperBuilder對象,XMLMapperBuilder對象的builderAssistant字段是一個MapperBuilderAssistant對象,同樣也是BaseBuilder的一個子類,其作用是對MappedStatement對象進行封裝。
有了XMLMapperBuilder對象后,就可以進入解析mapper映射文件的過程:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
調用XMLMapperBuilder的configurationElement方法,mapper映射文件進行解析
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
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);
}
}
mapper映射文件必須有namespace屬性值,否則拋出異常,將namespace屬性保存到XMLMapperBuilder的MapperBuilderAssistant對象中,以便其他方法調用。
該方法對mapper映射文件每個標簽逐一解析並保存到Configuration和MapperBuilderAssistant對象中,最后調用buildStatementFromContext方法解析select、insert、update和delete節點。
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) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
buildStatementFromContext方法中調用XMLStatementBuilder來完成解析,可以看到SQL語句封裝到一個SqlSource對象,SqlSource是個接口,如果是動態SQL就創建DynamicSqlSource實現類,否則創建StaticSqlSource實現類。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
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());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
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;
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
MapperBuilderAssistant通過addMappedStatement方法將MappedStatement對象放入Configuration對象的mappedStatements容器中,得到了最終的Configuration對象后傳入SqlSessionFactoryBuilder的構造方法,生成我們需要的DefaultSqlSessionFactory對象。
這里只分析了MyBatis是如何解析resource和url方式指定的mapper配置文件,如果是通過指定Mapper接口的package或者class全限定名配置方式,則Configuration對象會通過addMappers方法將接口注冊,再通過Java反射技術和JDK動態代理技術,根據接口class的全限定名找到對應的XML配置文件或者注解進行解析,如果是非注解模式的xml配置文件必須和這個class在同一級目錄,且與class同名,這里不再詳解。
最后附上SqlSessionFactory構建過程的簡易流程圖