接上一篇博文,這一篇來說述怎麼實現SchemaSqlMapperParserDelegate——解析SqlMapper配置文件。node
要想實現SqlMapper文件的解析,還須要仔細分析一下mybatis的源碼,我畫了一個圖來協助理解,也能夠幫助造成一個總體概念:spring
固然,這幅圖不止是原生的解析,也包括了XSD模式下的解析,下面對着這幅圖來講明一下。sql
1、Mybatis全局配置數據庫
Mybatis的全局配置,對應內存對象爲Configuration,是重量級對象,和數據源DataSource、會話工廠SqlSessionFactory屬於同一級別,通常來講(單數據源系統)是全局單例。從SqlSessionFactoryBean的doGetConfigurationWrapper()方法能夠看到,有三種方式構建,優先級依次爲:express
1.spring容器中注入,由用戶直接注入一個Configuration對象mybatis
2.根據mybatis-config.xml中加載,而mybatis-config.xml的路徑由configLocation指定,配置文件使用組件XMLConfigBuilder來解析app
3.採用mybatis內部默認的方式,直接new一個配置對象Configurationide
這裏爲了簡單,偷一個懶,不具體分析XMLConfigBuilder了,而直接採用spring中注入的方式,這種方式也給了擴展Configuration一個極大的自由。函數
2、讀取全部SqlMapper.xml配置文件測試
也有兩種方式,一種是手工配置,一種是使用自動掃描。推薦的天然是自動掃描,就很少說了。
加載全部SqlMapper.xml配置文件以後就是循環處理每個文件了。
3、解析單個SqlMapper.xml配置文件
單個SqlMapper.xml文件的解析入口是SqlSessionFactoryBean的doParseSqlMapperResource()方法,在這個方法中,自動偵測是DTD仍是XSD,而後分兩條並行路線分別解析:
一、DTD模式:建立XMLMapperBuilder對象進行解析
二、XSD模式:根據ini配置文件,找到sqlmapper命名空間的處理器SchemaSqlMapperNamespaceParser,該解析器將具體的解析工做委託給SchemaSqlMapperParserDelegate類。
4、解析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)。
因此一級子元素能夠總結以下:
這些元素是按以下方式解析的:
一、DTD模式:使用XMLMapperBuilder對象內的方法分別解析
上面負責解析的每行代碼都是一個內部方法,好比解析select|insert|update|delete元素的方法:
能夠看到,具體解析又轉給XMLStatementBuilder了,而最終每個select|insert|update|delete元素在內存中表現爲一個MappedStatement對象。
二、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對象,而是經過工廠類建立而已。
5、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(); } }
6、解析Script級元素
Script級元素指的是除根元素和一級子元素以外的元素(固然也不包括註釋元素了。。。),是用來構建Statement級元素的,包括SQL文本和動態配置元素(include|trim|where|set|foreach|choose|if),這些元素按以下方式解析:
一、DTD模式:使用XMLScriptBuilder解析,這裏mybatis卻是使用了一個解析接口,惋惜的是內部的私有接口,而且在根據元素名稱獲取接口實現類時也是莫名其妙(居然每次獲取都先建立全部的實現類,而後返回其中的一個,這真是莫名其妙的一塌糊塗!):
另外,SQL文本則是使用TextSqlNode解析。
二、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; }
7、處理$fn_name{args}、${(exp)}和#{(exp)}
這裏引進了兩個概念來擴展mybatis的配置:
一、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配置函數名不區分大小寫,但參數區分大小寫。
二、擴展表達式
(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)擴展表達式區分大小寫。
上面就是整個解析過程的一個概述了,總結一下引進的幾個接口:
今天到此爲止,下一篇博客就描述怎麼應用這些擴展。