*************************************優雅的分割線 **********************************
分享一波:程序員賺外快-必看的巔峰干貨
如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程
請關注微信公眾號:HB荷包
一個能讓你學習技術和賺錢方法的公眾號,持續更新
前言
接上一篇博客,解析核心配置文件的流程還剩兩塊。Mybatis初始化1.2 —— 解析別名、插件、對象工廠、反射工具箱、環境
本想著只是兩個模塊,隨便寫寫就完事,沒想到內容還不少,加上最近幾天事情比較多,就沒怎么更新,幾天抽空編寫剩下兩塊代碼。
解析sql片段
sql節點配置在Mapper.xml文件中,用于配置一些常用的sql片段。
/*** 解析sql節點。* sql節點用于定義一些常用的sql片段* @param list*/
private void sqlElement(List<XNode> list) {if (configuration.getDatabaseId() != null) {sqlElement(list, configuration.getDatabaseId());}sqlElement(list, null);
}/*** 解析sql節點* @param list sql節點集合* @param requiredDatabaseId 當前配置的databaseId*/
private void sqlElement(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {// 獲取databaseId和id屬性String databaseId = context.getStringAttribute("databaseId");// 這里的id指定的是命名空間String id = context.getStringAttribute("id");// 啟用當前的命名空間id = builderAssistant.applyCurrentNamespace(id, false);if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {// 如果該節點指定的databaseId是當前配置中的,就啟用該節點的sql片段sqlFragments.put(id, context);}}
}
這里面,SQLFragments用于存放sql片段。在存放sql片段之前,會先調用databaseIdMatchesCurrent方法去校驗該片段的databaseId是否為當前啟用的databaseId
/*** 判斷databaseId是否是當前啟用的* @param id 命名空間id* @param databaseId 待匹配的databaseId* @param requiredDatabaseId 當前啟用的databaseId* @return*/
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {if (requiredDatabaseId != null) {return requiredDatabaseId.equals(databaseId);}if (databaseId != null) {return false;}if (!this.sqlFragments.containsKey(id)) {return true;}// skip this fragment if there is a previous one with a not null databaseIdXNode context = this.sqlFragments.get(id);return context.getStringAttribute("databaseId") == null;
}
解析sql片段的步驟就這么簡單,下面是解析sql節點的代碼。
解析sql節點
在XxxMapper.xml中存在諸多的sql節點,大體分為select、insert、delete、update節點(此外還有selectKey節點等,后面會進行介紹)。每一個sql節點最終會被解析成MappedStatement。
/**
-
表示映射文件中的sql節點
-
select、update、insert、delete節點
-
該節點中包含了id、返回值、sql等屬性
-
@author Clinton Begin
*/
public final class MappedStatement {/**
- 包含命名空間的節點id
/
private String resource;
private Configuration configuration;
/* - 節點id
/
private String id;
private Integer fetchSize;
private Integer timeout;
/* - STATEMENT 表示簡單的sql,不包含動態的
- PREPARED 表示預編譯sql,包含#{}
- CALLABLE 調用存儲過程
*/
private StatementType statementType;
private ResultSetType resultSetType;
/**
- 節點或者注解中編寫的sql
/
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
/* - sql的類型。select、update、insert、delete
*/
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
}
- 包含命名空間的節點id
處理sql節點
/*** 處理sql節點* 這里的Statement單詞后面會經常遇到* 一個MappedStatement表示一條sql語句* @param list*/
private void buildStatementFromContext(List<XNode> list) {if (configuration.getDatabaseId() != null) {buildStatementFromContext(list, configuration.getDatabaseId());}buildStatementFromContext(list, null);
}/*** 啟用當前databaseId的sql語句節點* @param list* @param requiredDatabaseId*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {// 解析sql節點statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}
}
在parseStatementNode方法中,只會啟用當前databaseId的sql節點(如果沒配置就全部啟用)
/*** 解析sql節點*/
public void parseStatementNode() {// 當前節點idString id = context.getStringAttribute("id");// 獲取數據庫idString databaseId = context.getStringAttribute("databaseId");// 啟用的數據庫和sql節點配置的不同if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 獲取當前節點的名稱String nodeName = context.getNode().getNodeName();// 獲取到sql的類型。select|update|delete|insertSqlCommandType 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和selectKey節點......
}
在該方法中,會依次處理include節點、selectKey節點、最后獲取到當前sql節點的各個屬性,去創建MappedStatement對象,并添加到Configuration中。
/*** 解析sql節點*/
public void parseStatementNode() {// 在上面已經進行了注釋......// 解析sql前先處理include節點。XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 獲取parameterType屬性String parameterType = context.getStringAttribute("parameterType");// 直接拿到parameterType對應的ClassClass<?> parameterTypeClass = resolveClass(parameterType);// 獲取到lang屬性String lang = context.getStringAttribute("lang");// 獲取對應的動態sql語言驅動器。LanguageDriver langDriver = getLanguageDriver(lang);// 解析selectKey節點processSelectKeyNodes(id, parameterTypeClass, langDriver);
}
解析parameterType和lang屬性比較簡單,這里只看解析include和selectKey
解析include節點
/*** 啟用include節點** @param source*/
public void applyIncludes(Node source) {Properties variablesContext = new Properties();Properties configurationVariables = configuration.getVariables();Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);applyIncludes(source, variablesContext, false);
}
在applyIncludes方法中,會調用它的重載方法,遞歸去處理所有的include節點。include節點中,可能會存在${}占位符,在這步,也會將該占位符給替換成實際意義的字符串。接著,include節點會被處理成sql節點,并將sql節點中的sql語句取出放到節點之前,最后刪除sql節點。最終select等節點會被解析成帶有動態sql的節點。
/*** 遞歸去處理所有的include節點.** @param source include節點* @param variablesContext 當前所有的配置*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {if (source.getNodeName().equals("include")) {// 獲取到refid并從配置中拿到sql片段Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);// 解析include節點下的Properties節點,并替換value對應的占位符,將name和value鍵值對形式存放到variableContextProperties toIncludeContext = getVariablesContext(source, variablesContext);// 遞歸處理,在sql節點中可能會使用到include節點applyIncludes(toInclude, toIncludeContext, true);if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {toInclude = source.getOwnerDocument().importNode(toInclude, true);}// 將include節點替換成sql節點source.getParentNode().replaceChild(toInclude, source);while (toInclude.hasChildNodes()) {// 如果還有子節點,就添加到sql節點前面// 在上面的代碼中,sql節點已經不可能再有子節點了// 這里的子節點就是文本節點(具體的sql語句)toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);}// 刪除sql節點toInclude.getParentNode().removeChild(toInclude);} else if (source.getNodeType() == Node.ELEMENT_NODE) {if (included && !variablesContext.isEmpty()) {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++) {// 解析include節點applyIncludes(children.item(i), variablesContext, included);}} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)&& !variablesContext.isEmpty()) {// 使用之前解析到的Properties對象替換對應的占位符source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));}
}
第一行代碼的含義是根據include節點的refid屬性去獲取到對應的sql片段,代碼比較簡單
/*** 根據refid查找sql片段* @param refid* @param variables* @return*/
private Node findSqlFragment(String refid, Properties variables) {// 替換占位符refid = PropertyParser.parse(refid, variables);// 將refid前面拼接命名空間refid = builderAssistant.applyCurrentNamespace(refid, true);try {// 從Configuration中查找對應的sql片段XNode nodeToInclude = configuration.getSqlFragments().get(refid);return nodeToInclude.getNode().cloneNode(true);} catch (IllegalArgumentException e) {throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);}
}
到這里,include節點就會被替換成有實際意義的sql語句。
解析selectKey節點
當數據表中主鍵設計為自增,可能會存在業務需要在插入后獲取到主鍵,這時候就需要使用selectKey節點。processSelectKeyNodes方法用于解析selectKey節點。該方法會先獲取到該sql節點所有的selectKey節點,遍歷去解析,解析完畢后刪除selectKey節點。
/*** 解析selectKey節點* selectKey節點可以解決insert時主鍵自增問題* 如果需要在插入數據后獲取到主鍵,就需要使用selectKey節點** @param id sql節點的id* @param parameterTypeClass 參數類型* @param langDriver 動態sql語言驅動器*/
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {// 獲取全部的selectKey節點List<XNode> selectKeyNodes = context.evalNodes("selectKey");if (configuration.getDatabaseId() != null) {parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());}parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);removeSelectKeyNodes(selectKeyNodes);
}
刪除selectKey節點的代碼比較簡單,這里就不貼了,重點看parseSelectKeyNodes方法。
該方法負責遍歷獲取到的所有selectKey節點,只啟用當前databaseId對應的節點(這里的邏輯和sql片段那里一樣,如果開發者沒有配置databaseId,就全部啟用)
/*** 解析selectKey節點** @param parentId 父節點id(指sql節點的id)* @param list 所有的selectKey節點* @param parameterTypeClass 參數類型* @param langDriver 動態sql驅動* @param skRequiredDatabaseId 數據源id*/
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {// 遍歷selectKey節點for (XNode nodeToHandle : list) {// 拼接id 修改為形如 findById!selectKey形式String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;// 獲得當前節點的databaseId屬性String databaseId = nodeToHandle.getStringAttribute("databaseId");// 只解析databaseId是當前啟用databaseId的節點if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);}}
}
在for循環中,會逐個調用parseSelectKeyNode方法去解析selectKey節點。代碼看似復雜其實很簡單,最終selectKey節點也會被解析成MappedStatement對象
/*** 解析selectKey節點** @param id 節點id* @param nodeToHandle selectKey節點* @param parameterTypeClass 參數類型* @param langDriver 動態sql驅動* @param databaseId 數據庫id*/
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {// 獲取 resultType 屬性String resultType = nodeToHandle.getStringAttribute("resultType");// 解析返回值類型Class<?> resultTypeClass = resolveClass(resultType);// 解析statementType(sql類型,簡單sql、動態sql、存儲過程)StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));// 獲取keyProperty和keyColumn屬性String keyProperty = nodeToHandle.getStringAttribute("keyProperty");String keyColumn = nodeToHandle.getStringAttribute("keyColumn");// 是在之前還是之后去獲取主鍵boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));// 設置MappedStatement對象需要的一系列屬性默認值boolean useCache = false;boolean resultOrdered = false;KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;Integer fetchSize = null;Integer timeout = null;boolean flushCache = false;String parameterMap = null;String resultMap = null;ResultSetType resultSetTypeEnum = null;// 生成sqlSourceSqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);// selectKey節點只能配置select語句SqlCommandType sqlCommandType = SqlCommandType.SELECT;// 用這么一大坨東西去創建MappedStatement對象并添加到Configuration中builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);// 啟用當前命名空間(給id前面加上命名空間)id = builderAssistant.applyCurrentNamespace(id, false);// 從Configuration中拿到上面的MappedStatementMappedStatement keyStatement = configuration.getMappedStatement(id, false);configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
至此,selectKey節點已經被解析完畢并刪除掉了,其余代碼就是負責解析其他屬性并將該sql節點創建為MappedStatement對象。
KeyGenerator keyGenerator;// 拼接id。形如findById!selectKeyString keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;// 給這個id前面追加當前的命名空間keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {// 優先取配置的useGeneratorKeys。如果為空就判斷當前配置是否允許jdbc自動生成主鍵,并且當前是insert語句// 判斷如果為真就創建Jdbc3KeyGenerator,如果為假就創建NoKeyGeneratorkeyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}// 獲取當前sql節點的一堆屬性,去創建MappedStatement。// 這里創建的MappedStatement就代表一個sql節點// 也是后面編寫mybatis攔截器時可以攔截的一處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);
結語
在看本博客時,可能會覺得比較吃力,這里建議結合代碼去閱讀。事實上這三篇博客的閱讀和編寫的過程中,對應的mybatis代碼都比較容易,結合代碼閱讀起來并沒有多大難度。最后貼一下我的碼云地址(別問為什么是github,卡的一批)
mybatis源碼中文注釋
*************************************優雅的分割線 **********************************
分享一波:程序員賺外快-必看的巔峰干貨
如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程
請關注微信公眾號:HB荷包
一個能讓你學習技術和賺錢方法的公眾號,持續更新