接上一篇博文,這一篇來講述怎么實現SchemaSqlMapperParserDelegate——解析SqlMapper配置文件。
要想實現SqlMapper文件的解析,還需要仔細分析一下mybatis的源碼,我畫了一個圖來協助理解,也可以幫助形成一個整體概念:
當然,這幅圖不止是原生的解析,也包括了XSD模式下的解析,下面對着這幅圖來說明一下。
一、Mybatis全局配置
Mybatis的全局配置,對應內存對象為Configuration,是重量級對象,和數據源DataSource、會話工廠SqlSessionFactory屬於同一級別,一般來說(單數據源系統)是全局單例。從SqlSessionFactoryBean的doGetConfigurationWrapper()方法可以看到,有三種方式構建,優先級依次為:
1.spring容器中注入,由用戶直接注入一個Configuration對象
2.根據mybatis-config.xml中加載,而mybatis-config.xml的路徑由configLocation指定,配置文件使用組件XMLConfigBuilder來解析
3.采用mybatis內部默認的方式,直接new一個配置對象Configuration
這里為了簡單,偷一個懶,不具體分析XMLConfigBuilder了,而直接采用spring中注入的方式,這種方式也給了擴展Configuration一個極大的自由。
二、讀取所有SqlMapper.xml配置文件
也有兩種方式,一種是手工配置,一種是使用自動掃描。推薦的自然是自動掃描,就不多說了。
加載所有SqlMapper.xml配置文件之后就是循環處理每一個文件了。
三、解析單個SqlMapper.xml配置文件
單個SqlMapper.xml文件的解析入口是SqlSessionFactoryBean的doParseSqlMapperResource()方法,在這個方法中,自動偵測是DTD還是XSD,然后分兩條並行路線分別解析:
1、DTD模式:創建XMLMapperBuilder對象進行解析
2、XSD模式:根據ini配置文件,找到sqlmapper命名空間的處理器SchemaSqlMapperNamespaceParser,該解析器將具體的解析工作委托給SchemaSqlMapperParserDelegate類。
四、解析Statement級元素
Statement級元素指的是根元素<mapper>的一級子元素,這些元素有cache|cache-ref|resultMap|parameterMap|sql|insert|update|delete|select,其中insert|update|delete|select就是通常所說的增刪改查,用於構建mybatis一次執行單元,也就是說,每一次mybatis方法調用都是對 insert|update|delete|select 元素的一次訪問,而不能說只訪問select的某個下級子元素;其它的一級子元素則是用於幫助構建執行單元(resultMap|parameterMap|sql)或者影響執行單元的行為的(cache|cache-ref)。
所以一級子元素可以總結如下:
- 執行單元元素:insert | update | delete | select
- 單元輔助元素:resultMap | parameterMap | sql
- 執行行為元素:cache | cache-ref
這些元素是按如下方式解析的:
1、DTD模式:使用XMLMapperBuilder對象內的方法分別解析
上面負責解析的每行代碼都是一個內部方法,比如解析select|insert|update|delete元素的方法:
可以看到,具體解析又轉給XMLStatementBuilder了,而最終每一個select|insert|update|delete元素在內存中表現為一個MappedStatement對象。
2、XSD模式:這里引入一個Statement級元素解析接口IStatementHandler
public interface IStatementHandler { void handleStatementNode(Configuration configuration, SchemaSqlMapperParserDelegate delegate, XNode node); }
每個實現類負責解析一種子元素,原生元素對應實現類有:
然后創建一個注冊器類SchemaHandlers來管理這些實現類。
這個過程主要有兩步:
(1)應用啟動時,將IStatementHandler的實現類和對應命名空間的相應元素事先注冊好
//靜態代碼塊,注冊默認命名空間的StatementHandler register("cache-ref", new CacheRefStatementHandler()); register("cache", new CacheStatementHandler()); register("parameterMap", new ParameterMapStatementHandler()); register("resultMap", new ResultMapStatementHandler()); register("sql", new SqlStatementHandler()); register("select|insert|update|delete", new CRUDStatementHandler());
(2)在解析時,根據XML中元素的命名空間和元素名,找到IStatementHandler的實現類,並調用接口方法

/** * 執行解析 */ public void parse() { if (!configuration.isResourceLoaded(location)) { try { Element root = document.getDocumentElement(); String namespace = root.getAttribute("namespace"); if (Tool.CHECK.isBlank(namespace)) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); doParseStatements(root); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML["+location+"]. Cause: " + e, e); } configuration.addLoadedResource(location); bindMapperForNamespace(); } doParsePendings(); } /** * 解析包含statements及其相同級別的元素[cache|cache-ref|parameterMap|resultMap|sql|select|insert|update|delete]等 * @param parent */ public void doParseStatements(Node parent) { NodeList nl = parent.getChildNodes(); for (int i = 0, l = nl.getLength(); i < l; i++) { Node node = nl.item(i); if (!(node instanceof Element)) { continue; } doParseStatement(node); } } /** * 解析一個和statement同級別的元素 * @param node */ public void doParseStatement(Node node) { IStatementHandler handler = SchemaHandlers.getStatementHandler(node); if (null == handler) { throw new BuilderException("Unknown statement element <" + getDescription(node) + "> in SqlMapper ["+location+"]."); } else { SchemaXNode context = new SchemaXNode(parser, node, configuration.getVariables()); handler.handleStatementNode(configuration, this, context); } }
這樣,只要事先編寫好IStatementHandler的實現類,並調用SchemaHandlers的注冊方法,解析就能順利進行,而不管是原生的元素,還是自定義命名空間的擴展元素。
舉個例子,和select|insert|update|delete對應的實現類如下:

public class CRUDStatementHandler extends StatementHandlerSupport{ @Override public void handleStatementNode(Configuration configuration, SchemaSqlMapperParserDelegate delegate, XNode node) { String databaseId = configuration.getDatabaseId(); if(databaseId != null){ buildStatementFromContext(configuration, delegate, node, databaseId); } buildStatementFromContext(configuration, delegate, node, null); } private void buildStatementFromContext(Configuration configuration, SchemaSqlMapperParserDelegate delegate, XNode node, String requiredDatabaseId) { XMLStatementBuilder statementParser = SqlSessionComponetFactorys.newXMLStatementBuilder(configuration, delegate.getBuilderAssistant(), node, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
這里,也將具體解析轉給XMLStatementBuilder了,只不過這里不是直接new對象,而是通過工廠類創建而已。
五、LanguageDriver
從上面知道DTD和XSD又匯集到XMLStatementBuilder了,而在這個類里面,間接的創建了LanguageDriver的實現類,用來解析腳本級的SQL文本和元素,以及處理SQL腳本中的參數。LanguageDriver的作用實際上就是組件工廠,和我們的ISqlSessionComponentFactory類似:
public interface LanguageDriver { /** * 創建參數處理器*/ ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); /** * 根據XML節點創建SqlSource對象 */ SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType); /** * 根據注解創建SQLSource對象 */ SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType); }
這里因為要再次區分DTD和XSD,需要使用我們自己的實現類,並在Configuration里面配置,又因為是使用XML配置,所以第三個方法就不管了:
public class SchemaXMLLanguageDriver extends XMLLanguageDriver {
// 返回ExpressionParameterHandler,可以處理表達式的參數處理器 @Override public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return SqlSessionComponetFactorys.newParameterHandler(mappedStatement, parameterObject, boundSql); }
// 如果是DTD,則使用XMLScriptBuilder,否則使用SchemaXMLScriptBuilder,從而再次分開處理 @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = SqlSessionComponetFactorys.newXMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); } }
六、解析Script級元素
Script級元素指的是除根元素和一級子元素之外的元素(當然也不包括注釋元素了。。。),是用來構建Statement級元素的,包括SQL文本和動態配置元素(include|trim|where|set|foreach|choose|if),這些元素按如下方式解析:
1、DTD模式:使用XMLScriptBuilder解析,這里mybatis倒是使用了一個解析接口,可惜的是內部的私有接口,並且在根據元素名稱獲取接口實現類時也是莫名其妙(竟然每次獲取都先創建所有的實現類,然后返回其中的一個,這真是莫名其妙的一塌糊塗!):
另外,SQL文本則是使用TextSqlNode解析。
2、XSD模式:和Statement級元素類似,這里引入一個Script級元素解析接口IScriptHandler
public interface IScriptHandler { void handleScriptNode(Configuration configuration, XNode node, List<SqlNode> targetContents); }
每個實現類負責解析一種子元素,也使用SchemaHanders來管理這些實現類。具體也是兩個步驟:
(1)靜態方法中注冊
//注冊默認命名空間的ScriptHandler register("trim", new TrimScriptHandler()); register("where", new WhereScriptHandler()); register("set", new SetScriptHandler()); register("foreach", new ForEachScriptHandler()); register("if|when", new IfScriptHandler()); register("choose", new ChooseScriptHandler()); //register("when", new IfScriptHandler()); register("otherwise", new OtherwiseScriptHandler()); register("bind", new BindScriptHandler());
(2)在使用SchemaXMLScriptBuilder解析時根據元素命名空間和名稱獲取解析器
public static List<SqlNode> parseDynamicTags(Configuration configuration, XNode node) { List<SqlNode> contents = new ArrayList<SqlNode>(); NodeList children = node.getNode().getChildNodes(); for (int i = 0; i < children.getLength(); i++) { XNode child = node.newXNode(children.item(i)); short nodeType = child.getNode().getNodeType(); if (nodeType == Node.CDATA_SECTION_NODE || nodeType == Node.TEXT_NODE) { String data = child.getStringBody(""); data = decorate(configuration.getDatabaseId(), data);//對SQL文本進行裝飾,從而嵌入SQL配置函數的處理 ExpressionTextSqlNode expressionTextSqlNode = new ExpressionTextSqlNode(data);//使用表達式SQL文本,從而具有處理表達式的能力 if (expressionTextSqlNode.isDynamic()) { contents.add(expressionTextSqlNode); setDynamic(true); } else { contents.add(new StaticTextSqlNode(data)); } } else if (nodeType == Node.ELEMENT_NODE) { // issue // #628 IScriptHandler handler = SchemaHandlers.getScriptHandler(child.getNode());//使用處理器機制,從而可以方便、自由地擴展 if (handler == null) { throw new BuilderException("Unknown element <" + child.getNode().getNodeName() + "> in SQL statement."); } handler.handleScriptNode(configuration, child, contents); setDynamic(true); } } return contents; }
七、處理$fn_name{args}、${(exp)}和#{(exp)}
這里引進了兩個概念來擴展mybatis的配置:
1、SQL配置函數
(1)SQL配置函數,只用於配置SQL文本,和SQL函數不同,SQL函數是在數據庫中執行的,而SQL配置函數只是JAVA中生成SQL腳本時候解析
(2)SQL配置函數形如 $fn_name{args},其中函數名是字母或下划線開頭的字母數字下划線組合,不能為空(為空則是mybatis原生的字符串替換語法)
(3)SQL配置函數在mybatis加載時解析一次,並將解析結果存儲至SqlNode對象中,不需要每次運行都解析
(4)SQL配置函數的定義和解析接口ISqlConfigFunction如下:
public interface ISqlConfigFunction { /** * 優先級,如果有多個同名函數,使用order值小的 * @return */ public int getOrder(); /** * 函數名稱 * @return */ public String getName(); /** * 執行SQL配置函數 * @param databaseId 數據庫ID * @param args 字符串參數 * @return */ public String eval(String databaseId, String[] args); }
(5)SQL配置函數的設別表達式如下(匆匆寫就,尚未測試充分)
(6)ISqlConfigFunction也使用SchemaHandlers統一注冊和管理。
(7)SQL配置函數名不區分大小寫,但參數區分大小寫。
2、擴展表達式
(1)作用是擴展mybatis原生的${}和#{}
(2)在原生用法中屬性的外面包一對小括號,就成為擴展表達式,形如${(exp)}、#{(exp)}
(3)擴展表達式每次執行都需要解析,其中${()}表達式解析后直接替換SQL字符串,而#{(exp)}則將解析后的結果作為參數調用JDBC的set族方法設置進數據庫
(4)擴展表達式的定義和解析接口IExpressionHandler如下:
public interface IExpressionHandler { public boolean isSupport(String expression, String databaseId); public Object eval(String expression, Object parameter, String databaseId); }
第一個方法用於判斷是否支持需要解析的表達式,第二個方法用於根據傳入參數和數據庫ID來解析表達式。
如果有多個處理器可以支持需要解析的表達式,將取第一個,這是典型的責任鏈模式,也是Spring MVC中大量使用的模式。
(5)擴展表達式的設別很簡單,就是在mybatis已經識別的基礎上,判斷是否以小括號開頭,並以小括號結尾。
(6)IExpressionHandler也使用SchemaHandlers統一注冊和管理 。
(7)擴展表達式區分大小寫。
上面就是整個解析過程的一個概述了,總結一下引進的幾個接口:
- 語句級元素解析處理器IStatementHandler
- 腳本級元素解析處理器IScriptHandler
- SQL配置函數ISqlConfigFunction
- 擴展表達式處理器IExpressionHandler
今天到此為止,下一篇博客就描述怎么應用這些擴展。