首页 > 代码库 > 优化与扩展Mybatis的SqlMapper解析

优化与扩展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的实现类和对应命名空间的相应元素事先注册好

//静态代码块,注册默认命名空间的StatementHandlerregister("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)静态方法中注册

//注册默认命名空间的ScriptHandlerregister("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

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

优化与扩展Mybatis的SqlMapper解析