優化與擴展Mybatis的SqlMapper解析

接上一篇博文,這一篇來說述怎麼實現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)。

因此一級子元素能夠總結以下:

  1. 執行單元元素:insert | update | delete | select
  2. 單元輔助元素:resultMap | parameterMap | sql
  3. 執行行爲元素: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);
    }
}
View Code

這樣,只要事先編寫好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);
        }
    }
}
View Code

這裏,也將具體解析轉給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)擴展表達式區分大小寫。

 

上面就是整個解析過程的一個概述了,總結一下引進的幾個接口:

  1. 語句級元素解析處理器IStatementHandler
  2. 腳本級元素解析處理器IScriptHandler
  3. SQL配置函數ISqlConfigFunction
  4. 擴展表達式處理器IExpressionHandler

今天到此爲止,下一篇博客就描述怎麼應用這些擴展。

相關文章
相關標籤/搜索