念奴娇 赤壁怀古
     [北宋]苏轼
大江东去,浪淘尽,千古风流人物。
故垒西边,人道是,三国周郎赤壁。
乱石穿空,惊涛拍岸,卷起千堆雪。
江山如画,一时多少豪杰。

遥想公瑾当年,小乔初嫁了,雄姿英发。
羽扇纶巾,谈笑间,樯橹灰飞烟灭。
故国神游,多情应笑我,早生华发。
人生如梦,一樽还酹江月。

优化与扩展Mybatis的SqlMapper解析

接上一篇博文,这一篇来讲述怎么实现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)。

所以一级子元素可以总结如下:

  1. 执行单元元素:insert | update | delete | select
  2. 单元辅助元素:resultMap | parameterMap | sql
  3. 执行行为元素: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);
    }
}
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对象,而是通过工厂类创建而已。

五、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)扩展表达式区分大小写。

 

上面就是整个解析过程的一个概述了,总结一下引进的几个接口:

  1. 语句级元素解析处理器IStatementHandler
  2. 脚本级元素解析处理器IScriptHandler
  3. SQL配置函数ISqlConfigFunction
  4. 扩展表达式处理器IExpressionHandler

今天到此为止,下一篇博客就描述怎么应用这些扩展。

posted @ 2016-11-07 22:30  linjisong  阅读(3995)  评论(0编辑  收藏  举报