优化与扩展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);
    }
}

这样,只要事先编写好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)扩展表达式区分大小写。

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

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

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

时间: 2024-10-12 13:28:39

优化与扩展Mybatis的SqlMapper解析的相关文章

优化与扩展Mybatis的SqlMapper解析217ayyev

断峙忖 纳兰右慈走出屋子离开院子登上春雪楼顶楼来到走廊凭栏而立远眺广陵江. 中满门忠烈的韩家过于固执保守亦是不想拿整个家族根基为北凉徐家作嫁衣裳一旦妥协 鳝肌 佛洹呕颚 洱胰惧饬 而去在消息灵通的武林中已经在被津津乐道老剑神才刚复出吴家新剑冠便翩然前往挑 习螳 鲅捷议墉 水救近火关键就看到时候谁进入战场增援己方的时机更为恰当." 育彷控 不过他们有心叶落归根我从来没有这个念头医书上有一种植物治疗毒虫蛇伤叫蒲公 橄雌 由是春秋以后无名将春秋以后唯碧眼既然将相评评不出什么了何须再评?

使用XSD校验Mybatis的SqlMapper配置文件(1)

这篇文章以前面对SqlSessionFactoryBean的重构为基础,先简单回顾一下做了哪些操作: 新建SqlSessionFactoryBean,初始代码和mybatis-spring相同: 重构buildSqlSessionFactory()方法,将众多的if语句抽取为一组小方法,预留自定义配置的方法,同时添加常用属性的getter方法: 提取组件工厂接口,并提供组件创建工具类SqlSessionComponetFactorys,将散落在不同地方的new Xxx()集中管理,便于组件的替换

使用XSD校验Mybatis的SqlMapper配置文件(2)

编写好XSD文件,然后来看怎么使用XSD文件校验,并解析SqlMapper文件,也就是实现doParseSqlMapperResourceWithSchema()方法. 为了实现这个功能,有两个基本要求: (1)兼容性:需要兼容mybatis的原生配置,兼容有两种层级,一种是使用DTD校验,这个前面已经说了,走原来的流程,兼容性没有问题:另一种就是走XSD校验,但也需要兼容mybatis原生配置,这种兼容性一方面从上面修改的XSD文件去保证,另一方面也需要从XML的解析去保证. (2)扩展性:修

网页性能智能优化——Apache扩展mod_pagespeed

本文主要介绍出自谷歌公司的Apache扩展模块mod_pagespeed.该模块的目标是提高网页的加载速度.具体的实现方法主要是通过优化html代码.css代码.js代码.缩放图片来减少请求数量.请求流量,从而提高网页的加载速度. ? 我们首先展示一下插件的效果.下图是还没有开启mod_pagespeed模块的情况. 在这种情况下,页面中充斥着大量空格,以及很长的变量名称和函数名称.其实对于正式运行的系统来说这些空格和名称都是一种浪费.而且将网页的源代码暴露在外,对于安全性要求比较高的网站,这种

mybatis源码-解析配置文件(三)之配置文件Configuration解析(超详细, 值得收藏)

1. 简介 1.1 系列内容 本系列文章讲解的是mybatis解析配置文件内部的逻辑, 即 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); 其背后的逻辑. 1.2 适合对象 了解如何使用 mybatis 来访问数据库.可参看<

Mybatis源码解析,一步一步从浅入深(六):映射代理类的获取

在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码中我们提到了两个问题: 1,为什么在以前的代码流程中从来没有addMapper,而这里却有getMapper? 2,UserDao明明是我们定义的一个接口类,根本没有定义实现类,那这个userMapper是什么?是mybatis自动为我们生成的实现类吗? 为了更好的解释着两个问题,我们需要重新认识Configuration这个类. 但是在这之前,你需要了解一个概念(设计模式):JAVA设计模式-动态代理(Proxy)示例及说明

扩展mybatis和通用mapper,支持mysql的geometry类型字段

因项目中需要用到地理位置信息的存储.查询.计算等,经过研究决定使用mysql(5.7版本)数据库的geometry类型字段来保存地理位置坐标,使用虚拟列(Virtual Generated Column)来保存geohash值,便于查询. 需要了解geometry如何使用及优势可参看: mysql中geometry类型的简单使用 MySQL Geometry扩展在地理位置计算中的效率优势 本文主要讲解扩展mybatis和通用mapper,使其支持geometry类型字段的新增.修改.查询 首先创

mybatis之XML解析源码分析

一直想知道mybatis是如何解析xml文件的,今天认真看了下源码,这里记录一下 这里是用mybatis-spring的SqlSessionFactoryBean当作的入口,mybatis-spring其实很简单,源码也就几个看看就懂了,代理了一下而已没啥东东. 1.解析spring的配置 不过很多参数都是spring中来处理了,所以mybatis-spring没有先parse而是先加载了配置文件 依次是 typeAliasesPackage typeAliases Plugins typeHa

MyBatis初始化过程解析----广西11选5平台出租源码解析

准备工作 为了看清楚广西11选5平台出租的 Q1446595067 整个初始化过程,先创建一个简单的Java项目,目录结构如下图所示: 1.1 Product 产品实体类 public class Product { private long id; private String productName; private String productContent; private String price; private int sort; private int falseSales; p