MyBatis的執行原理1:構建SqlSessionFactory過程
首先建立了一個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各個配置檔案,這裡暫時只討論 XMLConfigBuilder:解析全域性配置檔案。
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解析的,並非我們熟悉的DOM和SAX,這裡不對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物件。
構建Configuration物件
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物件中,其中buildStatementFromContext將mapper對映檔案中SQL語句解析成MappedStatement物件,儲存到Configuration的mappedStatements集合中。
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);
}
}
}
使用XMLStatementBuilder將select、insert、update和delete節點的屬性及SQL語句封裝成MappedStatement物件
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將解析完成的資料封裝成MappedStatement物件放入Configuration物件的mappedStatements容器中,得到了最終的Configuration物件後傳入SqlSessionFactoryBuilder的構造方法,生成我們需要的SqlSessionFactory物件。