阅读 120

Mybatis源码之美:3.10.1.探究CRUD元素解析工作前的知识准备

探究CRUD元素的解析工作前的知识准备

在前面的几篇文章中,我们深入的探究了CRUD元素的定义和用法,在对CRUD元素有了一定的了解之后,在这篇文章中,我们主要探究一下mybatisCRUD元素的解析工作.

解析CRUD元素的入口在XMLMapperBuilder对象的configurationElement()方法中:

private void configurationElement(XNode context) {
    // ... 省略 ...
    // 构建声明语句(CRUD)
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    // ... 省略 ...
}
复制代码

该方法将获取到的select,insert,update,delete四种类型的元素配置交给buildStatementFromContext()方法来进行统一的处理.

buildStatementFromContext()方法是一个中转方法,它调用了自身的一个重载实现来完成真正的处理工作.

/**
 * 处理所有的【select|insert|update|delete】节点构建声明语句
 *
 * @param list 所有的声明语句节点
 */
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        // 解析专属于当前数据库类型的Statement
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    // 解析未指定数据库类型的Statement
    buildStatementFromContext(list, null);
}
复制代码

在实现上,buildStatementFromContext()方法本身调用了两次重载方法:

buildStatementFromContext()

类似sql元素的解析,这两次调用相结合,mybatis就可以完成跨数据库语句支持的功能.

重载方法buildStatementFromContext()的实现也并不复杂,针对具体的CRUD元素,他将解析该元素所需的数据整合在一起,并为其创建一个XMLStatementBuilder对象来完成后续的处理操作.

/**
    * 构建声明语句
    *
    * @param list               所有的声明语句
    * @param requiredDatabaseId 必须的数据库类型唯一标志
    */
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        // 解析每一个声明
        // 配置Xml声明解析器
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 委托给XMLStatementBuilder完成Statement的解析
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            // 发生异常,添加到未完成解析的声明语句集合
            configuration.addIncompleteStatement(statementParser);
        }
    }
}
复制代码

这一点和cache-ref,resultMap元素的解析工作十分相似,XMLMapperBuilder对象本身不完成后续的处理操作,而是将后续操作所需的数据整合起来创建一个新的对象去完成后续操作.

这是因为cache-ref,resultMap以及CRUD元素都是可以进行跨Mapper引用的,因此也就会产生在解析时被引用者尚未解析的场景,这时候,就需要缓存起来本次处理的所有数据,待被引用者完成解析操作之后,再重新尝试解析.

XMLStatementBuilder对象也是BaseBuilder对象的实现类,从定义上来看,这也就意味着XMLStatementBuilder对象具有mybatis基础组件的解析构建能力.

但是和前面了解的BaseBuilder实现不同的是,XMLStatementBuilder对象对外提供的解析方法不是parse(),而是parseStatementNode().

XMLStatementBuilder对象的实现并不复杂,buildStatementFromContext()方法创建该对象时,通过该对象的构造方法完成了必要属性的赋值工作:

public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
    this(configuration, builderAssistant, context, null);
}

public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
    super(configuration);
    this.builderAssistant = builderAssistant;
    this.context = context;
    this.requiredDatabaseId = databaseId;
}
复制代码

在被赋值的属性中,唯一值得一提的是context属性,该属性维护了当前需要解析的CURD元素.

/**
    * Statement节点
    */
private final XNode context;
复制代码

parseStatementNode()方法的实现涉及到的知识点就相对要多一些,除了常规的属性获取工作之外,在解析过程中,我们还将接触到语言解释器LanguageDriver,以及主键生成器KeyGenerator等对象.

为了更好的连贯的去学习CURD元素的解析,我们先去探究一下LanguageDriverKeyGenerator然后再回来继续后面的解析工作.

什么叫语言解释器

在介绍mybatis全局配置解析工作的时候,我们稍微提及了一下脚本语言解释器:

Sql脚本语言处理器注册表

参考文章:Mybatis源码之美:2.11.通过settings配置初始化全局配置

那时候我将LanguageDriver称为脚本语言处理器,后来发现语言解释器更形象一些,事实上,二者是一个意思.

同时,因为想更细致的探究myabtis源码的解析处理工作,所以决定提前学习语言解释器相关的内容.


LanguageDriver作为语言解释器,他的职能是将用户配置的数据转换成mybatis可理解和使用的对象.

LanguageDriver定义了两方法,一类方法用于解析用户配置,获取SqlSource对象 ,一类用于在运行时为CRUD方法入参创建ParameterHandler对象.

  • SqlSource维护了用户配置的原始SQL信息,他提供了一个getBoundSql()方法来获取BoundSql对象,BoundSql对象的getSql()方法可以获取真正用于执行的SQL数据.

  • ParameterHandler对象负责将用户调用CRUD方法时传入的参数转换成合适的类型用于SQL语句的执行.

LanguageDriver定义中用于获取SqlSource对象的createSqlSource()方法有两种重载形式,一种用于解析处理通过XML文件配置的SQL信息,一种用于解析处理通过注解配置的SQL信息.

// 处理XML配置
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
// 处理注解配置
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
复制代码

用于创建ParameterHandler对象的方法只有一种定义:

// 创建ParameterHandler对象实例
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
复制代码

createParameterHandler()方法的入参MappedStatement对象是我们完成CRUD元素解析工作之后得到的最终对象,每个CRUD元素配置都会有一个与之相对应的MappedStatement对象,BoundSql则是通过上面两个方法创建的SqlSource对象间接生成的,最后一个Object类型的parameterObject参数,则是我们在调用CRUD方法是传入的方法入参.

LanguageDrivercreateParameterHandler()方法我们在后面的文章中再深入了解,本篇主要深入探究createSqlSource()方法的实现.

mybatisLanguageDriver提供两种实现:RawLanguageDriverXMLLanguageDriver.

LanguageDriver类图

其中XMLLanguageDriverLanguageDriver默认的,也是最主要的实现类.

在探究XMLLanguageDriver对象的实现之前,我们需要简单了解一下如何使用注解配置SQL.

和使用XML文件配置CRUD语句相似,mybatis提供了四个注解:Select,Insert,Update以及Delete,这四个注解的效果和用法基本等同于XML配置中的同名元素.

针对于普通的SQL语句定义,二者的用法基本一致:

简单实例

如果我们需要在SQL定义中包含动态SQL,只需要将SQL配置包含在script标签内即可:

动态SQL配置

LanguageDriver中定义的两个createSqlSource()重载方法就分别用于处理上面这两种配置.

SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
复制代码
  • XML配置转换为SqlSource对象

将XML配置转换为SqlSource对象

  • 将注解配置转换为SqlSource对象

将注解配置转换为SqlSource对象

XMLLanguageDriver对象对这两个方法的实现并不算复杂,负责处理XML配置的createSqlSource()方法把具体的实现基本都委托给了XMLScriptBuilder对象:

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 动态元素解析器
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 构架SQL源
    return builder.parseScriptNode();
}
复制代码

XMLScriptBuilder用于解析用户的CRUD配置,并创建相应的SqlSource对象,关于XMLScriptBuilder的实现细节,我们待会再展开.

负责处理注解配置的createSqlSource()方法的实现有两个分支,一种是处理包含动态SQL元素的配置,一种是普通的SQL配置.

public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    // 处理包含动态SQL元素的配置
    if (script.startsWith("<script>")) {
        XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
        return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
        // 常规SQL配置
        // issue #127
        script = PropertyParser.parse(script, configuration.getVariables());
        TextSqlNode textSqlNode = new TextSqlNode(script);
        if (textSqlNode.isDynamic()) {
            return new DynamicSqlSource(configuration, textSqlNode);
        } else {
            return new RawSqlSource(configuration, script, parameterType);
        }
    }
}
复制代码

在实现上,处理包含动态SQL元素配置的操作是交给其重载方法完成的:

注解处理

处理常规SQL配置的操作,XMLLanguageDriver对象则亲力亲为:

处理常规SQL配置的流程不算复杂,首先借助于PropertyParser对象的parse()方法利用mybatis现有的参数配置替换掉用户配置的CRUD元素中包含的${}占位符.

然后利用处理后的文本内容创建一个TextSqlNode对象实例,并根据TextSqlNode对象中是否包含尚未处理的${}占位符来决定创建何种SqlSource对象实例.

script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
    return new DynamicSqlSource(configuration, textSqlNode);
} else {
    return new RawSqlSource(configuration, script, parameterType);
}
复制代码

很多人可能会对这一句话有些许疑问,为什么我们前面已经替换过了${}占位符,这里还要再次判断处理后的内容中是否包含${}占位符呢?

第一次解析占位符

仔细看上面的图示,在经过第一次解析之后,最终用于生成TextSqlNode对象的SQL语句中依然包含${gender}占位符.


负责解析占位符的PropertyParser对象的parse()方法有两个入参,其中类型为String的参数string是可能包含占位符的文本内容,类型为Propertiesvariables属性则负责提供用于替换占位符的参数配置.

public static String parse(String string, Properties variables)
复制代码

${}占位符介绍

mybatis提供的单元测试PropertyParserTest中包含了PropertyParser的应用场景.

mybatis中关于占位符${}的用法比较简单,具体的使用可以参考[官方文档:参数:字符串]替换部分

后面将会单开一篇文章详细介绍PropertyParser对象的实现.

在对createSqlSource()方法有了整体认知之后,我们先深入了解TextSqlNode,再回头去了解XMLScriptBuilder对象的实现.

SqlNode

TextSqlNodeSqlNode接口的实现类,SqlNode实现类的作用是维护用户配置的SQL数据.

SqlNode的接口定义应用了常见的设计模式中的组合模式.

组合模式的定义:将对象组合成树形结构以表示"部分-整体"的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性.

组合模式的作用: 组合模式弱化了简单元素和复杂元素的概念,客户端可以像处理简单元素一样来处理复杂元素.

SqlNode接口只对外暴露了一个apply()方法,该方法的作用是根据运行上下文筛选出有效的SQL配置.

boolean apply(DynamicContext context);
复制代码

这里的运行上下文指的是DynamicContext对象,该对象有一个重要的属性定义:

/**
    * 当前绑定的上下文参数集合
    */
private final ContextMap bindings;
复制代码

ContextMap类型的bindings属性用于缓存解析指定SQL时所需的上下文数据.

ContextMapHashMap的实现,他通过重写HashMapget()方法实现了对点式分隔形式的复杂属性导航的支持:

/**
    * 对象元数据
    */
private MetaObject parameterMetaObject;

public ContextMap(MetaObject parameterMetaObject) {
    this.parameterMetaObject = parameterMetaObject;
}

@Override
public Object get(Object key) {
    // 从当前集合中获取指定的key对应的值
    String strKey = (String) key;
    if (super.containsKey(strKey)) {
        return super.get(strKey);
    }

    // 如果没有,则从参数对象元数据中获取
    if (parameterMetaObject != null) {
        // issue #61 do not modify the context when reading
        return parameterMetaObject.getValue(strKey);
    }

    return null;
}
复制代码

实现原理比较简单,优先从Map本身取值,之后再借助于MetaObject来完成属性的取值.

关于MetaObject的内容,可以参考文章Mybatis源码之美:2.9.解析ObjectWrapperFactory元素,解析并配置对象包装工厂中的相关内容.


DynamicContext对象的创建需要两个参数,一个是mybatis的全局配置对象Configuration,另一个是用户调用CRUD方法时传入的方法入参.

DynamicContext对象的构造方法中,首先会根据用户的方法入参创建相应的ContextMap对象,之后将方法入参数据唯一标志存放进ContextMap对象中.

public DynamicContext(Configuration configuration, Object parameterObject) {

    if (parameterObject != null && !(parameterObject instanceof Map)) {
        // 获取对象元数据
        MetaObject metaObject = configuration.newMetaObject(parameterObject);

        // 保存当前对象的元数据
        bindings = new ContextMap(metaObject);
    } else {
        bindings = new ContextMap(null);
    }
    /*
        * 保存参数集合
        */
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);

    // 保存数据库唯一标志
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
复制代码
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
复制代码

构造方法的执行过程大致如下:

@startuml
autonumber
hide footbox
participant DynamicContext as dc
participant Configuration as c
activate dc
[-> dc: 调用构造方法
alt 有入参,且入参不为Map类型
dc -> c ++ : newMetaObject
c -> MetaObject ** : 创建
return
dc -> ContextMap ** :使用MetaObject创建
return bindings
else 无入参,或者入参对象为Map类型
dc -> ContextMap ** :使用空对象创建
return  bindings
end
dc -> ContextMap ++: put("_parameter", 入参对象)
return
dc -> ContextMap ++: put("_databaseId", 数据库唯一标志)
return
[<- dc
@enduml
复制代码

构造方法的执行过程

为了更好的理解DynamicContext对象在构造过程中如何处理的数据,我们做一个简单的小测试:

@Slf4j
class DynamicContextTest {
    @Test
    public void createDynamicContextTest() {
        Configuration configuration = new Configuration();
        configuration.setDatabaseId("数据库标志");
        User user = new User("panda", 18, "男");
        DynamicContext dynamicContext = new DynamicContext(configuration, user);
        Map<String, Object> binds = dynamicContext.getBindings();
        log.debug("最终数据为:{}", binds.toString());
        log.debug("获取用户名称:{}", binds.get("name"));
    }

    @Data
    @AllArgsConstructor
    public static class User {
        private String name;
        private Integer age;
        private String gender;
    }
}
复制代码

上面的代码输入下列日志:

DEBUG [main] - 最终数据为:{_parameter=DynamicContextTest.User(name=panda, age=18, gender=男), _databaseId=数据库标志}
DEBUG [main] - 获取用户名称:panda
复制代码

实际数据在代码中的变化如下图:

创建DynamicContext对象

输出的日志内容不包含parameterMetaObject内容,是因为AbstractMap对象的toString()方法只输出其维护的键值对数据.

DynamicContext对象中还有一个静态方法:

static {
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
复制代码

这意味着在DynamicContext对象被加载时,将会为OGNL注册一个属性访问器,有关于OGNL的相关知识,不在本篇文章的考虑之内,因此这里就不展开了.


mybatisSqlNode提供了10个实现类,这些实现类大部分和myabtis动态SQL元素一一对应,只有极少数的三个实现具有特殊的意义.

SqlNode

这三个具有特殊意义的实现分别是:TextSqlNode,StaticTextSqlNodeMixedSqlNode.

单纯的,生硬的去介绍,大家可能有些迷惑,不能很好的了解SqlNode实现类的区别,但是不用担心,我们接下来就深入的探究不同SqlNode的区别以及协作流程.

回顾XMLLanguageDriver对象中关于处理注解中常规SQL配置的代码(createSqlSource()方法):

script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
    return new DynamicSqlSource(configuration, textSqlNode);
} else {
    return new RawSqlSource(configuration, script, parameterType);
}
复制代码

mybatis在拿到用户通过注解提供的的SQL配置之后,将其封装成了TextSqlNode对象,并根据TextSqlNode对象的isDynamic()方法的返回值来决定创建何种类型的SqlSource对象.

TextSqlNode对象是对含有占位符${}SQL配置的包装,他所修饰的SQL配置中可能含有使用${}修饰的占位符.

对含有${}占位符的Sql配置我称其为动态SQL,对不含有${}占位符的配置我称之为静态SQL.

TextSqlNode有两个属性定义,一个是String类型的text属性,该属性用于缓存SQL配置,另一个是Pattern类型的injectionFilter属性,该属性负责维护一个正则表达式,主要作用是防止SQL注入.

/**
    * 内容
    */
private final String text;
/**
    * 条件正则表达式,主要用于防注入
    */
private final Pattern injectionFilter;
复制代码

TextSqlNode额外对外暴露了一个isDynamic()方法,该方法的作用是判断当前text属性中是否包含${}占位符:

public boolean isDynamic() {
    /*
        * 动态节点检查解析器
        */
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    // 解析${}
    GenericTokenParser parser = createParser(checker);
    // 解析文本
    parser.parse(text);
    // 只要包含占位符就是动态标签
    return checker.isDynamic();
}
复制代码

该方法的实现借助了DynamicCheckerTokenParserGenericTokenParser两个实现类,在这里GenericTokenParser的主要作用是解析出指定文本中被${}修饰的占位符内容,并将该占位符的内容交给DynamicCheckerTokenParserhandleToken()来处理.

DynamicCheckerTokenParserTokenHandler接口定义的实现,他的handlerToken()方法的实现比较简单,只需更新负责记录是否有${}占位符的isDynamic标记即可:

/**
    * 动态节点检查解析器
    */
private static class DynamicCheckerTokenParser implements TokenHandler {

    private boolean isDynamic;

    public DynamicCheckerTokenParser() {
        // Prevent Synthetic Access
    }

    public boolean isDynamic() {
        return isDynamic;
    }

    @Override
    public String handleToken(String content) {
        this.isDynamic = true;
        return null;
    }
}
复制代码

TextSqlNode对象的isDynamic()方法最终返回的也是DynamicCheckerTokenParser对象的isDynamic标记.

GenericTokenParser对象与TokenHandler接口定义相关的内容我们会和PropertyParser对象一起探究.

TextSqlNode对象还有一个用于获取SQL数据的apply()方法,该方法的实现也很简单:

@Override
public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
}
复制代码

在借由GenericTokenParser对象获取到需要处理的占位符数据之后,后续的操作交给了BindingTokenParser对象来完成.

BindingTokenParserTextSqlNode的内部类,他只有两个通过构造方法初始化的属性:

/**
    * 方法运行上下文
    */
private DynamicContext context;
/**
    * 防止SQL注入的表达式
    */
private Pattern injectionFilter;

public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
    this.context = context;
    this.injectionFilter = injectionFilter;
}
复制代码

他的handlerToken()方法负责以DynamicContext维护的参数集合作为基础数据来解析占位符所描述的参数对应的值.

public String handleToken(String content) {
    Object parameter = context.getBindings().get("_parameter");
    if (parameter == null) {
        context.getBindings().put("value", null);
    } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
    }
    Object value = OgnlCache.getValue(content, context.getBindings());
    String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
    // 检测注入
    checkInjection(srtValue);
    return srtValue;
}
复制代码

SimpleTypeRegistry

在实现上,该方法可能会为DynamicContext对象持有的ContextMap额外添加一个名为value的特殊值,该属性值的添加条件是入参对象为空,或者入参对象理论上不可拆分的简单类型.

添加Value

之后,使用ContextMap维护的参数集合来解析占位符中的OGNL表达式对应的值,并通过checkInjection()方法完成防SQl注入的校验工作:

private void checkInjection(String value) {
    if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
        throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
    }
}
复制代码

有关OgnlCache相关的内容,这里不展开,如果条件允许的话,后面会单开一篇文章探究OgnlCache的实现.

上面就是TextSqlNode对象的实现了,总体来说,并不算复杂,回到XMLLanguageDrivercreateSqlSource()方法中,如果创建的TextSqlNode对象包含${}占位符,mybatis就会将其包装成DynamicSqlSource对象,否则就包装成RawSqlSource对象.

DynamicSqlSourceRawSqlSource都是SqlSource接口的实现类,DynamicSqlSource负责维护包含${}占位符的SqlNode,我称之为动态SQL源,RawSqlSource负责维护不含${}占位符的SqlNode,我称之为静态SQL源.

单纯的从名称上来看也不难发现,在获取SQL时,DynamicSqlSource将会比RawSqlSource多做一次${}占位符的解析工作.


到这里,我们算是大致了解了负责处理由注解提供的常规SQL配置createSqlSource()方法的实现,虽然有很多细节我们还没有去特别深入的了解,但是不要紧,现在我们先回过头来看XMLScriptBuilder对象的实现.

XMLScriptBuilder对象也是BaseBuilder的子类,他对外暴露的解析方法名为parseScriptNode().

XMLScriptBuilder的构造方法需要三个参数,这三个参数分别是mybatisConfiguration对象,维护具体SQL配置的XNode对象,以及预期的方法入参的类型:

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    initNodeHandlerMap();
}
复制代码

在构造方法中,除了简单的属性赋值操作之外,XMLScriptBuilder还执行了一个initNodeHandlerMap()方法,这个方法的作用是初始化待解析动态SQL元素负责解析该元素的处理器之间的关系.

private void initNodeHandlerMap() {
    // 注册(动态元素)子节点的解析器
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
}
复制代码

几乎与动态SQL元素的名称一一相对应,mybatisNodeHandler接口提供了8种实现:

NodeHandler接口

NodeHandler接口的设计目的是为指定的动态SQL元素创建与其相对应的SqlNode对象.

处理逻辑

如果仔细看上图的话,我们可以发现trim,where,set,foreach,choose,if6个元素都由同名的NodeHandler实现将其转换为同名的SqlNode对象,bind元素则由同名的BindHandler将其转换为VarDesSqlNode对象.

至于多出来的choose元素的两个子元素:whenotherwise,鉴于when的逻辑和if元素一致,因此when元素会被IfHandler转换为IfSqlNode.

otherwise则由OtherWiseHandler将其转换为MixedSqlNode.

在上图中,还对三个特殊的SqlNode实现做了标记.

其中TextSqlNode是对含有占位符${}SQL配置的包装,它所对应的SQL配置中可能含有${}占位符数据,比如:SELECT * FROM USER WHERE NAME=${name}.

StaticTextSqlNode是最基本的SQL配置的封装对象,它封装的SQL配置可能包含#{}占位符,但是绝不可能包含${}占位符.

MixedSqlNode本身不维护SQL配置,但是他持有一组用于维护SQL配置的SqlNode集合,他获取所需SQL的操作,实际是拼接SqlNode集合中所有有效的SQL数据得来的.

听不大懂不要紧,我们马上就会一一了解这些实现.

还是XMLLanguageDrivercreateSqlSource()方法,在创建了XMLScriptBuilder实例之后,就会调用其parseScriptNode()方法来获取SqlSource对象了:

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 动态元素解析器
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 构架SQL源
    return builder.parseScriptNode();
}
复制代码

XMLScriptBuilderparseScriptNode()方法的实现有些像前面讲的解析注解配置的常规SQL的实现,先根据用户配置生成SqlNode对象,之后根据SqlNode对象是否是动态SqlNode来创建相应的DynamicSqlSource或者RawSqlSource.

public SqlSource parseScriptNode() {
    // 解析动态标签
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
        // 配置动态SQL源
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        // 配置静态SQL源
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
复制代码

唯一的不同就在于这里创建的SqlNode对象是MixedSqlNode,而不是TextSqlNode.

前面说了MixedSqlNode本身并不直接维护SQL配置,而是通过维护一组SqlNode集合来间接管理SQL配置.

他的实现格外简单,创建MixedSqlNode实例时,需要为其构造方法传入所维护的SqlNode集合,这样在调用其apply()方法时,MixedSqlNode将会依次调用其维护的每个SqlNode对象的apply()方法来得到最终的SQL数据.

public class MixedSqlNode implements SqlNode {
    /**
     * 包含的Sql节点集合
     */
    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        for (SqlNode sqlNode : contents) {
            sqlNode.apply(context);
        }
        return true;
    }
}
复制代码

负责创建MixedSqlNode对象的parseDynamicTags()方法从逻辑上来看,可以分为两类,一类负责处理动态SQL元素,一类是负责处理普通文本配置的SQL.

下面是一个简单的select语句的SQL配置:

配置示例

通过上图可以看到,一个简单的CRUD配置会涉及到三种不同类型的元素:ELEMENT_NODE,TEXT_NODE以及CDATA.

其中,针对TEXT_NODECDATA块中包裹的数据,解析器不需要额外的处理,保持数据原样即可,ELEMENT_NODE元素的配置则需要进一步的解析才能使用.

负责解析用户配置的parseDynamicTags()方法,遵循的就是这个道理,针对每一个用户配置,如果子元素的类型是TEXT_NODE或者CDATA块,parseDynamicTags()方法会将其转换成TextSqlNode对象,如果通过TextSqlNode对象的isDynamic()方法判断用户配置的SQL不是动态SQL(即:不包含${}占位符),那么parseDynamicTags()方法会使用用户配置的SQL重新生成StaticTextSqlNode对象来取代TextSqlNode对象.

如果子元素的类型是ELEMENT_NODE,parseDynamicTags()方法就会为该元素寻找相应的NodeHandler来处理该元素配置.

protected MixedSqlNode parseDynamicTags(XNode node) {
    // 用于维护最终所有的SQL节点对象
    List<SqlNode> contents = new ArrayList<>();
    // 获取内部的所有子节点,其实就是七种动态标签。
    NodeList children = node.getNode().getChildNodes();
    // 循环处理每一个动态标签
    for (int i = 0; i < children.getLength(); i++) {
        // 生成一个新的XNode对象
        XNode child = node.newXNode(children.item(i));
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE /*子节点一定为TextNode*/
                || child.getNode().getNodeType() == Node.TEXT_NODE /*<![CDATA[]]>中括着的纯文本,它没有子节点*/
        ) {
            // 获取子节点中的文本内容
            String data = child.getStringBody("");

            // 使用文本内容生成一个文件Sql节点
            TextSqlNode textSqlNode = new TextSqlNode(data);
            // 只要文本中包含了${},就是动态节点
            if (textSqlNode.isDynamic()) {
                // 添加动态SQL节点
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                // 添加静态SQl节点
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE/*DTD实体定义,无子节点*/
        ) {
            // 进入该方法,则表示该子节点是动态节点
            // issue #628
            // 获取动态节点的名称
            String nodeName = child.getNode().getNodeName();
            /*获取动态节点处理器*/
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            /*委托给动态节点处理器来处理动态节点*/
            handler.handleNode(child, contents);
            /*重置动态节点标志*/
            isDynamic = true;
        }
    }
    /*返回一个混合SQL节点对象*/
    return new MixedSqlNode(contents);
}
复制代码

认真看上述方法实现,parseDynamicTags()方法会遍历用户配置的CRUD语句中的每一个子元素,并为每个子元素合理的创建相应的SqlNode对象,并将得到的SqlNode对象放入有序集合contents中,最后利用有序集合作为构造参数,创建一个MixedSqlNode对象返回给调用方, 这就证明无论用户配置如何,通过XML进行的CRUD配置最终一定会被转换成MixedSqlNode对象来使用.

关于MixedSqlNode的实现我们前面已经做了了解,现在让我们看一下负责维护静态SQL配置(即:配置中不包含${}占位符)的StaticTextSqlNode的实现.

StaticTextSqlNode的实现更为简单,他只有一个Stringtext属性用来维护对应的SQL配置,当用户调用他的apply()方法时,他就简单的将该SQL追加到DynamicContext中即可.

public class StaticTextSqlNode implements SqlNode {
    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }

    @Override
    public boolean apply(DynamicContext context) {
        context.appendSql(text);
        return true;
    }

}
复制代码

DynamicContext对象本身维护了一个StringBuilder类型的sqlBuilder参数:

/**
    * 维护最终生成的SQL数据
    */
private final StringBuilder sqlBuilder = new StringBuilder();
复制代码

并为该参数暴露了一个appendSql()方法用来往sqlBuilder中追加SQL内容:

public void appendSql(String sql) {
    sqlBuilder.append(sql);
    sqlBuilder.append(" ");
}
复制代码

以及一个getSql()方法用来获取当前维护的SQL内容:

public String getSql() {
    return sqlBuilder.toString().trim();
}
复制代码

我们继续回到parseDynamicTags()方法的实现上来,针对用户CURD配置中的元素配置(ELEMENT_NODE),parseDynamicTags()方法会根据元素的名称从nodeHandlerMap中获取相应的NodeHandler,并利用得到的NodeHandler来完成元素的处理操作.

前面我们讲过nodeHandlerMap集合的初始化过程是在构造方法中完成的,并提供了一张图来描述不同NodeHandler实现类处理的元素,以及最终对应的SqlNode对象类型.

处理逻辑

NodeHandler接口只对外暴露了一个handleNode()方法,该方法有两个入参,一个是XNode类型的nodeToHandle参数,该参数表示当前处理的子元素.另一个是List<SqlNode>类型的targetContents集合参数,targetContents中有序的存放在当前已解析出的SQL配置.

private interface NodeHandler {
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
复制代码

下面按照上图的顺序,我们来一次了解每一个NodeHandler子类的实现.

首先我们看一下TrimHandler,TrimHandlerXMLScriptBuilder的内部类定义:

private class TrimHandler implements NodeHandler {
    public TrimHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 包含的子节点在解析后SQL文本不为空时需要添加的前缀内容
        String prefix = nodeToHandle.getStringAttribute("prefix");
        // 需要覆盖掉的子节点解析后的SQL文本的前缀内容
        String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
        // 包含的子节点在解析后SQL文本不为空时需要添加的后缀内容
        String suffix = nodeToHandle.getStringAttribute("suffix");
        // 需要覆盖掉的子节点解析后的SQL文本的后缀内容
        String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
        // 构建trimSQl节点
        TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
        targetContents.add(trim);
    }
}
复制代码

根据trim元素的DTD定义来看,trim元素下是可以继续嵌套其他动态SQL元素配置的:

<!ELEMENT trim (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
复制代码

因此在实现上,TrimHandler会调用XMLScriptBuilder方法来递归解析子元素配置并获取对应的MixedSqlNode对象.

之后依次读取trim元素的属性配置:

<!ATTLIST trim
prefix CDATA #IMPLIED
prefixOverrides CDATA #IMPLIED
suffix CDATA #IMPLIED
suffixOverrides CDATA #IMPLIED
>
复制代码

并利用这些属性创建TrimSqlNode对象,将其添加到targetContents集合中.

TrimSqlNode的实现看起来略显复杂,但原理相对比较简单,他现有的六个属性定义,除了Configuration对象之外,基本都和trim元素的DTD定义息息相关:

/**
    * Sql节点的内容
    */
private final SqlNode contents;
/**
    * 前缀
    */
private final String prefix;
/**
    * 后缀
    */
private final String suffix;
/**
    * 需要被覆盖的前缀(多个需要被覆盖的前缀之间可以通过|来分割)
    */
private final List<String> prefixesToOverride;
/**
    * 需要被覆盖的后缀(多个需要被覆盖的后缀之间可以通过|来分割)
    */
private final List<String> suffixesToOverride;
/**
    * Mybatis配置
    */
private final Configuration configuration;
复制代码

这些属性中,比较值得注意的是prefixesToOverridesuffixesToOverride这两个属性,他们是集合类型的,因为在配置这两个属性时,可以通过|作为分隔符来得到多个子字符串.

TrimSqlNode的对外提供的构造方法中针对prefixesToOverridesuffixesToOverride这两个属性做了额外的处理:

public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
复制代码

其实现就是借助于parseOverrides()方法,处理字符串中的|分隔符,将这两个属性转换为集合类型,并调用另一个构造方法,完成基本属性的赋值工作:

/**
 * 解析要被覆盖的(前缀|后缀),处理 | ,生成替换列表
 * @param overrides 声明
 */
private static List<String> parseOverrides(String overrides) {
    if (overrides != null) {
        final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
        final List<String> list = new ArrayList<>(parser.countTokens());
        while (parser.hasMoreTokens()) {
            list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
        }
        return list;
    }
    return Collections.emptyList();
}
复制代码

受保护的构造方法:

protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
}
复制代码

TrimSqlNodeapply()方法在实现上,借助于名为FilteredDynamicContext的动态参数对象:

@Override
public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
}
复制代码

仔细看上面的实现,当完成子元素对应的SqlNode对象的apply()方法的调用之后,TrimHandler额外的调用了FilteredDynamicContext对象的applyAll()方法.

FilteredDynamicContextDynamicContext的子实现,在创建该对象时需要一个DynamicContext对象实例,FilteredDynamicContext对象的大多数方法实现都会委托给现有的DynamicContext实例来完成:

FilteredDynamicContext对象有四个属性定义:

private DynamicContext delegate;
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;

public FilteredDynamicContext(DynamicContext delegate) {
    super(configuration, null);
    this.delegate = delegate;
    this.prefixApplied = false;
    this.suffixApplied = false;
    this.sqlBuffer = new StringBuilder();
}
复制代码

其中prefixAppliedsuffixApplied分别用来记录当前是否已经处理了前缀/后缀配置,sqlBuffer则用来缓存trim元素中对应的SQL数据.

至于delegate则是FilteredDynamicContext对象的委托对象.

委托者模式虽然不属于常见的23中设计模式,但其也是常用的设计模式之一.

FilteredDynamicContext对象主要重写了父类的appendSql()方法,将得到的SQL数据,缓存到自身提供的sqlBuffer属性中,以此来实现对SQL数据额外处理的能力:

@Override
public void appendSql(String sql) {
    sqlBuffer.append(sql);
}
复制代码

FilteredDynamicContext对象额外对外暴露了applyAll()方法,该方法负责处理缓存SQL前/后缀,并最终将处理后的SQL数据保存到原始DynamicContext委托类中:

public void applyAll() {
    sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
    String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
    if (trimmedUppercaseSql.length() > 0) {
        applyPrefix(sqlBuffer, trimmedUppercaseSql);
        applySuffix(sqlBuffer, trimmedUppercaseSql);
    }
    delegate.appendSql(sqlBuffer.toString());
}
复制代码

仔细看上面的实现,if判断语句确保了只有在存在有效SQL配置的前提下,前/后缀配置才会生效.

负责处理前/后缀的两个方法的applyPrefix()applySuffix()在实现上比较相似:

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
    if (!prefixApplied) {
        prefixApplied = true;
        if (prefixesToOverride != null) {
            for (String toRemove : prefixesToOverride) {
                if (trimmedUppercaseSql.startsWith(toRemove)) {
                    sql.delete(0, toRemove.trim().length());
                    break;
                }
            }
        }
        if (prefix != null) {
            sql.insert(0, " ");
            sql.insert(0, prefix);
        }
    }
}

private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
    if (!suffixApplied) {
        suffixApplied = true;
        if (suffixesToOverride != null) {
            for (String toRemove : suffixesToOverride) {
                if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
                    int start = sql.length() - toRemove.trim().length();
                    int end = sql.length();
                    sql.delete(start, end);
                    break;
                }
            }
        }
        if (suffix != null) {
            sql.append(" ");
            sql.append(suffix);
        }
    }
}
复制代码

值得注意的就是prefixAppliedsuffixApplied这两个属性,理论上这两个属性定义存在的目的是为了避免因为多次调用applyAll()方法,导致多次生成前后缀的问题.

但实际上在目前的代码中,并不会重复调用同一个FilteredDynamicContext对象的applyAll()方法.


WhereHandler用于解析where元素配置,因为where元素也可以嵌套配置动态SQL元素:

<!ELEMENT where (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
复制代码

因此在实现上,WhereHandler也会调用XMLScriptBuilder方法来递归解析子元素配置并获取对应的MixedSqlNode对象.

private class WhereHandler implements NodeHandler {
    public WhereHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
        targetContents.add(where);
    }
}
复制代码

WhereSqlNodeTrimSqlNode的子类,他的实现基本依托于TrimSqlNode,在他的构造方法中,为TrimSqlNode指定了prefix属性为where字符串,并且指定了需要移除的字符串前缀是:"AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t".


WhereHandler类似,SetHandler用于将set元素处理成SetSqlNode;

<!ELEMENT set (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
复制代码
/**
 * set标签主要用于解决动态更新字段
 */
private class SetHandler implements NodeHandler {
    public SetHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
        targetContents.add(set);
    }
}
复制代码

SetSqlNode也是TrimSqlNode的实现类,他指定了TrimSqlNode需要添加的前缀为SET,并指定了需要移除的前/后缀,:

public class SetSqlNode extends TrimSqlNode {

    private static final List<String> COMMA = Collections.singletonList(",");

    public SetSqlNode(Configuration configuration, SqlNode contents) {
        // 在字段前添加 SET 并覆盖前置和后置的 【,】符号。
        super(configuration, contents, "SET", COMMA, null, COMMA);
    }

}
复制代码

ForEachHandler用于处理foreach元素,基于同样的原因,ForEachHandler同样会借助于parseDynamicTags()反方解析foreach元素中嵌套的配置:

<!ELEMENT foreach (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST foreach
collection CDATA #REQUIRED
item CDATA #IMPLIED
index CDATA #IMPLIED
open CDATA #IMPLIED
close CDATA #IMPLIED
separator CDATA #IMPLIED
>
复制代码

并获取foreach元素的属性配置来创建ForEachSqlNode对象:

private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        String collection = nodeToHandle.getStringAttribute("collection");
        String item = nodeToHandle.getStringAttribute("item");
        String index = nodeToHandle.getStringAttribute("index");
        String open = nodeToHandle.getStringAttribute("open");
        String close = nodeToHandle.getStringAttribute("close");
        String separator = nodeToHandle.getStringAttribute("separator");
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
        targetContents.add(forEachSqlNode);
    }
}
复制代码

ForEachSqlNode对象的属性定义中,除了ConfigurationExpressionEvaluator之外,基本都和foreach元素的配置息息相关:

/**
    * OGNL表达式解析器
    */
private final ExpressionEvaluator evaluator;
/**
    * collection对应的OGNL表达式
    */
private final String collectionExpression;
/**
    * 对应的Sql节点
    */
private final SqlNode contents;
/**
    * 在开始部分添加的标签
    */
private final String open;
/**
    * 在结束部分添加的标签
    */
private final String close;
/**
    * 分隔符
    */
private final String separator;
/**
    * 子名称
    */
private final String item;
/**
    * 索引
    */
private final String index;
/**
    * Mybatis配置
    */
private final Configuration configuration;
复制代码

ExpressionEvaluatormybatis提供的OGNL表达式解析器,用于解析OGNL表达式,这里就不展开了.

除此之外,ForEachSqlNode还提供了一个取值为__frch_的常量ITEM_PREFIX,该常量用于修饰集合元素中的下标.

ForEachSqlNode的构造方法中,完成了上述属性的赋值工作:

public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
}
复制代码

ForEachSqlNodeapply()看起来比较复杂,但是逻辑相对比较简单:

public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    // 解析迭代器对象
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
        return true;
    }
    boolean first = true;
    // 添加开始标签
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
        DynamicContext oldContext = context;
        if (first || separator == null) {
            // 第一个不需要添加分隔符
            context = new PrefixedContext(context, "");
        } else {
            // 在每项里面添加分隔符
            context = new PrefixedContext(context, separator);
        }
        // 获取唯一标记
        int uniqueNumber = context.getUniqueNumber();
        // Issue #709
        if (o instanceof Map.Entry) {
            @SuppressWarnings("unchecked")
            Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
            // 绑定索引
            applyIndex(context, mapEntry.getKey(), uniqueNumber);
            // 绑定至
            applyItem(context, mapEntry.getValue(), uniqueNumber);
        } else {
            // 非MAP 直接绑定索引
            applyIndex(context, i, uniqueNumber);
            // 非MAP 直接绑定值
            applyItem(context, o, uniqueNumber);
        }
        // 解析出具体的SQL
        contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));

        if (first) {
            first = !((PrefixedContext) context).isPrefixApplied();
        }
        context = oldContext;
        i++;
    }
    // 添加结束标签
    applyClose(context);
    // 移除条目
    context.getBindings().remove(item);
    // 移除索引
    context.getBindings().remove(index);
    return true;
}
复制代码

首先借助于OGNL解析器获取用户通过foreach元素的collection属性配置的集合,之后依次处理集合中的每一个元素,在处理集合前后,会分别调用applyOpen()applyClose()方法完成前后标签的插入工作:

private void applyOpen(DynamicContext context) {
    if (open != null) {
        context.appendSql(open);
    }
}
private void applyClose(DynamicContext context) {
    if (close != null) {
        context.appendSql(close);
    }
}
复制代码

具体到处理每一个元素的过程中,首先将现有的DynamicContext对象包装成PrefixedContext实例,PrefixedContext也是DynamicContext的实现类,他的构造方法除了需要一个DynamicContext实例作为委托类之外,还需要一个prefix参数,prefix参数对应的是foreach元素的separator属性.

在创建PrefixedContext对象时,针对集合中的第一个元素是不需要指定prefix参数的取值的,如果用户没有为foreach元素配置separator属性,默认使用空字符串"";

PrefixedContext有三个属性定义,分别是委托类delegate,需要添加的前缀prefix,以及是否应用了前缀的标志符prefixApplied.

/**
    * 动态上下文委托处理类
    */
private final DynamicContext delegate;
/**
    * 前缀
    */
private final String prefix;
/**
    * 是否应用了前缀
    */
private boolean prefixApplied;
复制代码

PrefixedContext的大部分方法都交给了委托类来完成,他只重写了appendSql()方法,在该方法中,PrefixedContext会为原始SQL添加统一的前缀:

@Override
public void appendSql(String sql) {
    // 尚未处理
    if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        // 添加前缀
        delegate.appendSql(prefix);
        prefixApplied = true;
    }
    // 添加Sql内容
    delegate.appendSql(sql);
}
复制代码

除此之外,PrefixedContext还额外暴露了用于判断是否应用了前缀的isPrefixApplied()方法:

public boolean isPrefixApplied() {
    return prefixApplied;
}
复制代码

回到ForEachSqlNodeapply()方法上来,在将DynamicContext对象包装成PrefixedContext之后,ForEachSqlNode开始处理每一个集合元素.

首先调用DynamicContextgetUniqueNumber()方法,来获取当前处理的元素在集合中的位置:

private int uniqueNumber = 0;

public int getUniqueNumber() {
    return uniqueNumber++;
}
复制代码

之后分别调用applyIndex()applyItem()方法来处理用户配置的index属性和item属性.

private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
        context.bind(index, o);
        context.bind(itemizeItem(index, i), o);
    }
}

private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
        context.bind(item, o);
        context.bind(itemizeItem(item, i), o);
    }
}
复制代码

上面代码中涉及到的itemizeItem方法用于为当前处理的元素生成唯一标志,并保存到DynamicContext中:

private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
}
复制代码

后续该标志将被用来获取实际数据.

applyIndex()applyItem()方法的入参基本一致,都是(DynamicContext context, Object o, int i),其中第二个Object类型的参数表示的是当前元素在集合中的下标,第三个int类型的元素表示的是当前元素在整个集合中的位置.

针对第二个参数,当集合的类型不同时,取值的方式也略有不同,针对Map集合,第二个参数的取值是Map.Entrykey值,普通集合对应的取值则是元素在集合中索引位置.

在处理元素的indexitem配置之后,ForEachSqlNode就会将DynamicContext包装成FilteredDynamicContext对象来处理SQL配置获取所需的SqlNode实例,需要注意的是,这里的FilteredDynamicContext对象时ForEachSqlNode的内部类,和前面提到的FilteredDynamicContext不是同一个对象.

当前FilteredDynamicContext对象,缓存了foreach的几个属性配置,并重写了DynamicContext对象的appendSql()方法:

private final DynamicContext delegate;
private final int index;
private final String itemIndex;
private final String item;

public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) {
    super(configuration, null);
    this.delegate = delegate;
    this.index = i;
    this.itemIndex = itemIndex;
    this.item = item;
}
复制代码

appendSql()方法的作用主要是借助于GenericTokenParser对象将用户配置通过#{}占位符配置的itemindex转换为运行时的唯一标志:

@Override
public void appendSql(String sql) {
    // 解析#{}标签
    GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        // 新内容
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));

        if (itemIndex != null && newContent.equals(content)) {
            newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        // 生成占位符
        return "#{" + newContent + "}";
    });

    // 解析占位符
    delegate.appendSql(parser.parse(sql));
}
复制代码

比如:

数据转换

这一步和前面往DynamicContext对象中存放数据的操作相对应.

在处理完集合元素之后,appendSql()方法就会移除掉前面通过applyIndex()applyItem()方法存储的部分数据,只保留通过itemizeItem()生成数据映射即可:

// 移除条目
context.getBindings().remove(item);
// 移除索引
context.getBindings().remove(index);
复制代码

ChooseHandler负责处理的是choose元素,根据choose元素的子元素定义,ChooseHandlerhandleNode()方法提供了两个集合分别用于存储when元素配置和otherwise元素配置,并利用这两个集合创建对应的ChooseSqlNode对象存放到targetContents集合中.

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    // When节点集合
    List<SqlNode> whenSqlNodes = new ArrayList<>();
    // otherwise节点集合
    List<SqlNode> otherwiseSqlNodes = new ArrayList<>();

    handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
    // 获取otherwise节点,只能有一个
    SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
    // 构造ChooseSql节点
    ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
    targetContents.add(chooseSqlNode);
}
复制代码

在获取到whenotherwise配置对应的集合之后,ChooseHandler还调用了getDefaultSqlNode()方法对otherwise配置的唯一性做了校验:

private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
    SqlNode defaultSqlNode = null;
    if (defaultSqlNodes.size() == 1) {
        defaultSqlNode = defaultSqlNodes.get(0);
    } else if (defaultSqlNodes.size() > 1) {
        throw new BuilderException("Too many default (otherwise) elements in choose statement.");
    }
    return defaultSqlNode;
}
复制代码

不过,根据choose元素的DTD定义,该校验在大多数下是可以省略的:

<!ELEMENT choose (when* , otherwise?)>
复制代码

因为,通常我们在使用mybtais都会启用DTD校验.

负责解析when子元素和otherwise子元素的方法是handleWhenOtherwiseNodes(),该方法将具体元素的解析处理工作交给了相应的NodeHandler来处理.

private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
    List<XNode> children = chooseSqlNode.getChildren();
    for (XNode child : children) {
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler instanceof IfHandler) {
            handler.handleNode(child, ifSqlNodes);
        } else if (handler instanceof OtherwiseHandler) {
            handler.handleNode(child, defaultSqlNodes);
        }
    }
}
复制代码

需要注意的是,在调用NodeHandler对象的handleNode()方法时,第二个入参分别是ifSqlNodes集合和otherwiseSqlNodes集合.

下面我们就来分别看一下负责处理whenotherwise元素的IfHandlerOtherWiseHandler的实现,之后再回头看ChooseSqlNode的实现.

IfHandler本意上是用来处理if元素配置将其转换为IfSqlNode,但是鉴于if元素和when元素的作用和属性定义基本一致,因此IfHandler也被用来处理when元素.

<!ELEMENT if (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST if
test CDATA #REQUIRED
>
<!ATTLIST when
test CDATA #REQUIRED
>
复制代码

IfHandler实现比较简单,他借助与parseDynamicTags()方法完成子元素的解析,同时获取if/whentest属性配置,以此来生成相应的IfSqlNode对象,添加到targetContents集合中(这里的targetContents集合是ifSqlNodes集合):

private class IfHandler implements NodeHandler {
    public IfHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析动态标签
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 获取IF语句的值
        String test = nodeToHandle.getStringAttribute("test");
        // 作为IfSqlNode节点
        IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
        targetContents.add(ifSqlNode);
    }
}
复制代码

IfSqlNode对象有三个属性定义:OGNL表达式解析器evaluator,通过test属性配置的OGNL表达式,以及if/when中子元素对应的SqlNode对象,这三个属性是在构造方法中完成赋值的:

/**
 * 表达式解析器
 */
private final ExpressionEvaluator evaluator;
/**
 * 表达式
 */
private final String test;
/**
 * SQL节点
 */
private final SqlNode contents;

public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
}
复制代码

他的apply()方法借助于ExpressionEvaluator对象的evaluateBoolean来获取在运行时test条件能否被满足,并执行相应的处理操作:

@Override
public boolean apply(DynamicContext context) {
    // 解析内容
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
        contents.apply(context);
        return true;
    }
    return false;
}
复制代码

看上面的实现,在条件不满足时,返回的false,该返回值在choose元素中将决定是否继续执行下一个when或者otherwise配置.

OtherwiseHandler的实现更为简单,他借助与parseDynamicTags()otherwise的子元素配置解析成MixedSqlNode节点,并将其添加到targetContents集合中(这里的targetContents集合是otherwiseSqlNodes集合).

private class OtherwiseHandler implements NodeHandler {
    public OtherwiseHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        targetContents.add(mixedSqlNode);
    }
}
复制代码

了解了if/when元素和otherwise元素涉及到的内容之后,我们回过头来继续看ChooseSqlNode.

ChooseSqlNode只有两个属性定义,一个是SqlNode类型的defaultSqlNode,它用来存储otherwise元素对应的SqlNode,另一个是List<SqlNode>类型的ifSqlNodes属性,它用来存储所有when元素对应的SqlNode集合,这两个属性的初始化工作也是在构造方法中完成的:

/**
    * 默认分支节点
    */
private final SqlNode defaultSqlNode;
/**
    * 条件语分支节点
    */
private final List<SqlNode> ifSqlNodes;

public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
}
复制代码

ChooseSqlNode方法的实现原理很简单,优先校验when元素的配置,如果其test条件能够满足,则相应的SqlNode生效,否则继续校验下一个when元素配置,如果所有的when元素的条件都不能够满足,在配置了otherwise的前提下,otherwise配置生效.

@Override
public boolean apply(DynamicContext context) {

    // 遍历所有的分支节点,当遇到第一个满足条件的就返回
    for (SqlNode sqlNode : ifSqlNodes) {
        if (sqlNode.apply(context)) {
            return true;
        }
    }
    // 如果没有满足条件的分支节点,则处理默认分支节点
    if (defaultSqlNode != null) {
        defaultSqlNode.apply(context);
        return true;
    }
    return false;
}
复制代码

现在,我们最后还剩下负责将bind元素配置解析成VarDecSqlNodeBindHandler还没有了解.

BindHandler的实现也比较简单,根据bind元素的DTD定义:

<!ELEMENT bind EMPTY>
<!ATTLIST bind
 name CDATA #REQUIRED
 value CDATA #REQUIRED
>
复制代码

BindHandler将会读取bind元素的namevalue属性的配置来生成的VarDeclSqlNode对象,并将其添加到targetContents集合中:

private class BindHandler implements NodeHandler {
    public BindHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 定义的变量名称
        final String name = nodeToHandle.getStringAttribute("name");
        // 定义的OGNL表达式
        final String expression = nodeToHandle.getStringAttribute("value");
        final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
        targetContents.add(node);
    }
}
复制代码

VarDeclSqlNodenameexpression两个属性定义,他们分别用来存储参数名称值的OGNL表达式,这两个属性的赋值工作也是在构造方法中完成的:

private final String name;
private final String expression;

public VarDeclSqlNode(String var, String exp) {
    /**
     * 属性名称
     */
    name = var;
    /**
     * 属性值
     */
    expression = exp;
}
复制代码

VarDeclSqlNodeapply()方法的作用实际上是根据用户的bind配置往DynamicContext中赋值.

@Override
public boolean apply(DynamicContext context) {

    // 解析表达式并获取值
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    // 绑定值
    context.bind(name, value);
    return true;
}
复制代码

注意看上面的实现,expression属性是通过OgnlCachegetValue()方法来取值的,因此bind元素的value属性是支持OGNL语法表达式的.


至此,我们算是了解了所有的NodeHandler以及SqlNode的实现与应用.

回到XMLScriptBuilderparseScriptNode()方法中:

public SqlSource parseScriptNode() {
    // 解析动态标签
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
        // 配置动态SQL源
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        // 配置静态SQL源
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
复制代码

通过上面的代码我们可以看到SqlNode对象最终会被转换成SqlSource的实现类来对外提供服务.

前面我们简单的提到过SqlSource定义了getBoundSql()方法用来获取BoundSql对象.

现在我们先来看一下BoundSql对象的具体实现,之后再看一下SqlSource接口相关的实现.

BoundSql维护了mybatis执行一条SQL语句所必需的数据,他在维护真正待执行的SQL语句的同时,还保留了用于执行该SQL语句的参数配置和参数对象.

BoundSql5个属性定义:

/**
 * 传递给JDBC的SQL文本
 */
private final String sql;
/**
 * 静态参数说明
 */
private final List<ParameterMapping> parameterMappings;
/**
 * 运行时的参数对象
 */
private final Object parameterObject;
/**
 * 额外的参数对象,也就是for loops、bind生成的
 */
private final Map<String, Object> additionalParameters;
/**
 * 额外参数的facade模式包装
 */
private final MetaObject metaParameters;
复制代码

其中String类型的sql属性用于维护真正传递给数据库执行的SQL语句,List<ParameterMapping>类型的parameterMappings集合则维护了用于执行该语句的参数映射配置,集合中的每一条配置都和待执行SQL语句中的?按顺序一一对应.

Object类型的parameterObject参数,表示用户调用CRUD方法时传入的方法入参,该方法入参是经过特殊处理的,因此可以用一个Object类型的参数来表示多个方法入参.

关于方法入参的特殊处理,会在后面的文章中展开.

Map<String, Object>类型的additionalParameters集合负责存储除方法入参之外的参数映射关系,比如通过bind,for元素生成的临时参数,还有默认添加的_parameter_databaseId参数.

最后一个MetaObject类型的metaParameters属性是additionalParameters的包装对象.

上述这5个属性的赋值操作都是在构造方法中完成的:

public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
}
复制代码

除此之外,BoundSql还对外暴露了一些方法用来操作这些属性:

比如,用来获取需要执行的sql语句的getSql()方法:

public String getSql() {
    return sql;
}
复制代码

获取方法参数映射配置的getParameterMappings()方法:

public List<ParameterMapping> getParameterMappings() {
    return parameterMappings;
}
复制代码

获取方法入参对象的getParameterObject()方法:

public Object getParameterObject() {
    return parameterObject;
}
复制代码

上述的三个方法都是简单的赋值操作,除此之外,还有三个用来操作额外参数配置的方法:

public boolean hasAdditionalParameter(String name) {
    String paramName = new PropertyTokenizer(name).getName();
    return additionalParameters.containsKey(paramName);
}

public void setAdditionalParameter(String name, Object value) {
    metaParameters.setValue(name, value);
}

public Object getAdditionalParameter(String name) {
    return metaParameters.getValue(name);
}
复制代码

这三个方法用来分别用来判断是否存在某参数,添加参数映射,和获取参数值,需要注意的是,这里操作的参数名称是支持点式分隔形式的.


看完了BoundSql对象,我们回头继续看SqlSource相关的内容.

mybatis中默认为SqlSource对象提供了四种实现:DynamicSqlSource,ProviderSqlSource,RawSqlSource以及StaticSqlSource.

SqlSource

通过前面的学习,我们了解到可以将SQL配置分为动态SQL配置静态SQL配置两种.

如果SQL配置中包含${}占位符或者使用了动态SQL元素,那么该配置就是动态SQL配置,反之就是静态SQL配置:

驱动动态SQL和静态SQL

DynamicSqlSource对应的就是动态SQL配置,RawSqlSource则对应着静态SQL配置.

无论是动态SQL配置还是静态SQL配置,所对应的SQL语句中都允许使用#{}占位符,因此在实际使用时还需要进一步解析其中的#{}占位符,得到最终用于数据库执行的SQL语句.

这里所得到的最终的SQL配置对应的就是StaticSqlSource,还剩下一个ProviderSqlSource,是用来处理通过InsertProvider,SelectProvider,UpdateProvider以及DeleteProvider四个注解提供的SQL配置的.

我们先来看一下负责维护动态SQL配置DynamicSqlSource的实现.

DynamicSqlSource定义了两个属性,分别是configurationrootSqlNode.

// Mybatis配置
private final Configuration configuration;
// Sql节点
private final SqlNode rootSqlNode;
复制代码

其中configurationmybatis的配置类,rootSqlNode则是当前SQL配置对应的SqlNode对象,这两个属性是在构造方法中被赋值的:

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
}
复制代码

DynamicSqlSourcegetBoundSql()方法的实现不算复杂,从理论上来讲,该方法只需要根据当前运行上下文筛选出SQL配置中的生效部分,在处理了其中的#{}占位符之后,创建相对应的BoundSql实例并返回即可.

public BoundSql getBoundSql(Object parameterObject) {
    // 生成动态内容解析器
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 处理动态标签节点
    rootSqlNode.apply(context);

    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 解析Sql占位符内容
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
        // 添加当前上下文动态绑定的内容
        boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
}
复制代码

在实现上,该方法首先会利用mybatisConfiguration实例和用户调用CRUD方法时传入的方法入参创建一个DynamicContext实例,并以此来调用SqlNodeapply()方法,完成有效SQL的筛选获取工作.

之后利用SqlSourceBuilder处理有效SQL中的#{}占位符,并以此来获取最终的BoundSql实例.

然后将DynamicContext中的参数配置添加到BoundSql实例中,最后返回BoundSql实例.

SqlSourceBuilder对象的作用是将指定SQL中的#{}占位符替换为?,同时将该占位符对应的参数信息转换为ParameterMapping对象供后续使用.

SqlSourceBuilder对象的实现并不复杂,他的parse()方法负责处理SQL配置中的#{}占位符,并返回相应的StaticSqlSource实例.

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 解析 【#{】和【}】直接的内容
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
复制代码

在实现上,该方法借助于GenericTokenParser来获取所有匹配的#{}占位符,并将占位符内容交给ParameterMappingTokenHandler来处理.

ParameterMappingTokenHandler将指定SQL中的#{}占位符替换为?,同时将该占位符对应的参数信息转换为ParameterMapping缓存起来.

最后SqlSourceBuilder利用处理后的SQL内容和缓存起来的ParameterMapping集合创建相应的StaticSqlSource实例返回给调用方.

有关于ParameterMappingTokenHandler的具体实现细节,我们待会会展开.


RawSqlSource用来表示静态SQL配置,因此不会包含动态SQL元素配置也不会包含${}占位符,所以在实现上,他只需要解析#{}占位符即可.

需要注意的是RawSqlSource提供了一个SqlSource类型的sqlSource属性,用来缓存当前静态SQL配置对应的StaticSqlSource对象.

private final SqlSource sqlSource;
复制代码

该属性的赋值工作是在RawSqlSource对象的构造方法中完成的:

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration,
            getSql(configuration, rootSqlNode)
            , parameterType);
}

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    // SqlSource建造器
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 解析SQL
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
}
复制代码

之后在调用RawSqlSourcegetBoundSql()时,就不会重复创建StaticSqlSource对象了:

@Override
public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
}
复制代码

因此在多次调用同一个SqlSource对象的getBoundSql()方法时,RawSqlSource的效率要高于DynamicSqlSource.


作为最终进化形态的StaticSqlSource对象的实现就更为简单了,他提供了三个属性用来缓存创建BoundSql对象所需的数据:

private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;

public StaticSqlSource(Configuration configuration, String sql) {
    this(configuration, sql, null);
}

public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
}
复制代码

在调用其getBoundSql()方法时直接利用现有参数创建BoundSql实例并返回即可:

@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
复制代码

最后,还剩下一个ProviderSqlSource对象,因为该对象负责处理的是基于注解提供的SQL配置,因此我们需要简单了解一下Provider注解配置的使用.

除了通过XML文件和XML元素同名注解配置SQL之外,mybatis还允许通过provider系列注解指定一个java方法来更灵活的配置SQL.

provider系列注解一共有四个,它们分别是:InsertProvider,SelectProvider,UpdateProvider以及DeleteProvider,这四个注解分别对应着CRUD四种数据库操作.

Provider注解

provider系列注解对外暴露了相同的两个方法:type()method().

其中type()方法返回负责提供SQL语句的类定义,method()则负责返回具体的方法名称.

我们以查询语句为例,简单感受一下三种配置方式的使用:

UserMapper.java:

public interface UserMapper {

    @SelectProvider(type = SqlProvider.class, method = "selectUserByName")
    List<User> selectUserByNameWithProvider(@Param("name") String name);

    class SqlProvider {
        public String selectUserByName() {
            return "SELECT * FROM USER WHERE name= #{name}";
        }
    }

    @Select("SELECT * FROM USER WHERE name= #{name}")
    List<User> selectUserByNameWithSimpleAnnotation(@Param("name") String name);

    List<User> selectUserByNameWithXML(@Param("name") String name);

}
复制代码

UserMapper.xml:

<mapper namespace="org.apache.learning.provider.UserMapper">

    <select id="selectUserByNameWithXML" resultType="org.apache.learning.provider.User">
        SELECT *
        FROM USER
        WHERE name = #{name}
    </select>
</mapper>
复制代码

单元测试:

@Test
public void selectProviderTest() {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    log.info("使用SelectProvider注解查询.结果为:{}", userMapper.selectUserByNameWithProvider("Panda"));
    log.info("使用Select注解查询.结果为:{}", userMapper.selectUserByNameWithSimpleAnnotation("Panda"));
    log.info("使用XML配置查询.结果为:{}", userMapper.selectUserByNameWithXML("Panda"));
}
复制代码

运行结果日志(关键):

...省略...
DEBUG [main] - ==>  Preparing: SELECT * FROM USER WHERE name= ?
DEBUG [main] - ==> Parameters: Panda(String)
DEBUG [main] - <==      Total: 1
 INFO [main] - 使用SelectProvider注解查询.结果为:[User(id=38400000-8cf0-11bd-b23e-10b96e4ef00d, name=Panda, gender=男)]
DEBUG [main] - ==>  Preparing: SELECT * FROM USER WHERE name= ?
DEBUG [main] - ==> Parameters: Panda(String)
DEBUG [main] - <==      Total: 1
 INFO [main] - 使用Select注解查询.结果为:[User(id=38400000-8cf0-11bd-b23e-10b96e4ef00d, name=Panda, gender=男)]
DEBUG [main] - ==>  Preparing: SELECT * FROM USER WHERE name = ?
DEBUG [main] - ==> Parameters: Panda(String)
DEBUG [main] - <==      Total: 1
 INFO [main] - 使用XML配置查询.结果为:[User(id=38400000-8cf0-11bd-b23e-10b96e4ef00d, name=Panda, gender=男)]
 ...省略...
复制代码

仔细看上面的日志,我们会发现三种配置的最终效果是一致的.

查看详细单元测试SqlProviderTest

或者访问链接:gitee.com/topanda/myb…

在了解了SelectProvider的简单用法之后,我们来看一下ProviderSqlSource对象的实现.

ProviderSqlSource对象有8个属性定义:

/**
 * Mybatis配置类
 */
private final Configuration configuration;
/**
 * SqlSource对象构建器
 */
private final SqlSourceBuilder sqlSourceParser;
/**
 * 提供Sql的对象类型
 */
private final Class<?> providerType;
/**
 * 提供Sql的方法
 */
private Method providerMethod;
/**
 * 提供Sql方法的入参名称集合
 */
private String[] providerMethodArgumentNames;
/**
 * 提供Sql方法的入参类型集合
 */
private Class<?>[] providerMethodParameterTypes;
/**
 * 用于Provider方法的上下文对象
 */
private ProviderContext providerContext;
/**
 * 配置使用Provider方法对应的上下文对象对应的参数位置索引
 */
private Integer providerContextIndex;
复制代码

我们来看一下除了ConfigurationSqlSourceBuilder之外的其余6个属性定义,providerType属性对应着Provider注解中的type()方法的返回值,用来缓存负责提供SQL的对象类型.

providerMethod负责记录用于提供SQL的方法名称,providerMethodArgumentNamesproviderMethodParameterTypes则负责记录用于提供SQL的方法的入参名称和类型.

ProviderContext类型的providerContext属性负责记录调用Provider方法的上下文信息.

providerContextIndex则负责记录ProviderContext类型的参数在Provider方法参数列表中索引位置.

ProviderContext的实现十分简单,他定义了两个属性.并为这两个属性提供了相应的getter方法:

/**
 * 用于Provider方法的上下文对象
 *
 * @author Kazuki Shimizu
 * @since 3.4.5
 */
public final class ProviderContext {

  /**
   * 对应的映射器类型
   */
  private final Class<?> mapperType;
  /**
   * 对应的映射器方法
   */
  private final Method mapperMethod;

  /**
   * Constructor.
   *
   * @param mapperType 指定了Provider注解的Mapper接口类型
   * @param mapperMethod 指定了Provider注解的Mapper方法
   */
  ProviderContext(Class<?> mapperType, Method mapperMethod) {
    this.mapperType = mapperType;
    this.mapperMethod = mapperMethod;
  }

  public Class<?> getMapperType() {
    return mapperType;
  }

  public Method getMapperMethod() {
    return mapperMethod;
  }

}
复制代码

其中mapperType属性用于记录指定了Provider注解的Mapper接口类型,mapperMethod则负责记录指定了Provider注解的Mapper方法.

我们来看一下,在实际应用在ProviderContext中的属性对应的具体数据来源:

Provider系列注解的相关介绍

(同一组数据使用相同颜色进行标注和连接)


回到ProviderSqlSource的源码解析上来,ProviderSqlSource提供了两个构造方法,这两个构造方法的主要作用就是为上述的8个属性赋值.

其中一个构造方法的实现是通过调用另一个构造方法来完成的:

@Deprecated
public ProviderSqlSource(Configuration configuration, Object provider) {
    this(configuration, provider, null, null);
}
复制代码

需要注意的是,上述的这个构造方法已经被弃用,因为该构造不需要传入标注了Provider注解的Mapper接口Mapper方法,因此,也就导致通过该构造方法构建的ProviderSqlSource对象无法创建所需的ProviderContext属性.

ProviderSqlSource另一个构造方法的实现不算复杂,其主要的作用就是处理Provider注解和标注了Provider注解的Mapper接口Mapper方法,并初始化ProviderSqlSource所需的属性定义.

public ProviderSqlSource(Configuration configuration, Object provider, Class<?> mapperType, Method mapperMethod) {
    String providerMethodName;
    try {
        // 初始化Mybatis全局配置
        this.configuration = configuration;
        // SqlSource对象的构建器
        this.sqlSourceParser = new SqlSourceBuilder(configuration);
        // 通过注解获取提供Sql内容的对象类型
        this.providerType = (Class<?>) provider.getClass().getMethod("type").invoke(provider);
        // 通过注解获取提供Sql内容的方法名称
        providerMethodName = (String) provider.getClass().getMethod("method").invoke(provider);

        for (Method m : this.providerType.getMethods()) {
            if (providerMethodName.equals(m.getName()) && CharSequence.class.isAssignableFrom(m.getReturnType())) {
                // 方法名称匹配,同时返回内容是可读序列的子类,其实简单来讲就是看看方法的返回对象是不是能转成字符串
                if (providerMethod != null) {
                    throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
                            + providerMethodName + "' is found multiple in SqlProvider '" + this.providerType.getName()
                            + "'. Sql provider method can not overload.");
                }
                // 配置提供Sql的方法
                this.providerMethod = m;
                // 配置提供Sql方法的入参名称集合
                this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();
                // 配置提供Sql方法的入参类型集合
                this.providerMethodParameterTypes = m.getParameterTypes();
            }
        }
    } catch (BuilderException e) {
        throw e;
    } catch (Exception e) {
        throw new BuilderException("Error creating SqlSource for SqlProvider.  Cause: " + e, e);
    }
    if (this.providerMethod == null) {
        throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
                + providerMethodName + "' not found in SqlProvider '" + this.providerType.getName() + "'.");
    }

    // 解析参数类型
    for (int i = 0; i < this.providerMethodParameterTypes.length; i++) {
        // 获取方法入参类型
        Class<?> parameterType = this.providerMethodParameterTypes[i];
        if (parameterType == ProviderContext.class) {
            // 查找ProviderContext类型的参数
            if (this.providerContext != null) {
                throw new BuilderException("Error creating SqlSource for SqlProvider. ProviderContext found multiple in SqlProvider method ("
                        + this.providerType.getName() + "." + providerMethod.getName()
                        + "). ProviderContext can not define multiple in SqlProvider method argument.");
            }
            // 构建用于Provider方法的上下文对象
            this.providerContext = new ProviderContext(mapperType, mapperMethod);
            // 配置使用Provider方法对应的上下文对象对应的参数位置索引
            this.providerContextIndex = i;
        }
    }
}
复制代码

在实现上,该方法会通过反射调用Provider注解的type()method()方法以此来获取Provider对象的类型和具体方法名称.

鉴于java方法是允许重载的,所以就可能会出现同时拥有多个同名不同参的重载方法的场景,因此还需要进一步的对方法名称的唯一性做校验

因为Provider方法的作用是返回合适的SQL语句,因此mybatis会忽略掉所有返回类型非CharSequence类型及其子类型的方法.

在得到唯一有效的Provider方法之后,mybatis通过反射获取其形参名称列表和形参类型列表,并记录可能存在的ProviderContext类型的参数在形参列表中的位置和创建相应的ProviderContext对象.

在这个构造方法的处理过程中,借用了ParamNameResolver对象来获取方法的形参名称列表:

// 配置提供Sql方法的入参名称集合
this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();
复制代码

该对象的作用我们在Mybatis源码之美:3.5.5.配置构造方法的constructor元素一文中稍作提及,限于篇幅,具体的实现我们会在后面的文章中展开.

最后是ProviderSqlSource用于获取BoundSqlgetBoundSql()方法:

public BoundSql getBoundSql(Object parameterObject) {
    SqlSource sqlSource = createSqlSource(parameterObject);
    return sqlSource.getBoundSql(parameterObject);
}
复制代码

该方法在实现通过createSqlSource()方法得到StaticSqlSource对象,并借助于StaticSqlSource对象的getBoundSql()方法返回最终BoundSql实例.

createSqlSource()方法的实现理论来讲就是通过前面获取到的Provider类型Provider方法利用反射得到SQL内容,传递给SqlSourceBuilder来获取StaticSqlSource实例:

private SqlSource createSqlSource(Object parameterObject) {
    try {
        // 计算除ProviderContext类型的参数之外的形参数量.
        int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);

        // 获取SQL
        String sql;
        if (providerMethodParameterTypes.length == 0) {
            sql = invokeProviderMethod();
        } else if (bindParameterCount == 0) {
            sql = invokeProviderMethod(providerContext);
        } else if (bindParameterCount == 1 &&
                (parameterObject == null || providerMethodParameterTypes[providerContextIndex == null || providerContextIndex == 1 ? 0 : 1].isAssignableFrom(parameterObject.getClass()))) {
            sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
        } else if (parameterObject instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> params = (Map<String, Object>) parameterObject;
            sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
        } else {
            throw new BuilderException("Error invoking SqlProvider method ("
                    + providerType.getName() + "." + providerMethod.getName()
                    + "). Cannot invoke a method that holds "
                    + (bindParameterCount == 1 ? "named argument(@Param)" : "multiple arguments")
                    + " using a specifying parameterObject. In this case, please specify a 'java.util.Map' object.");
        }
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        // 创建并返回SqlSource
        return sqlSourceParser.parse(replacePlaceholder(sql), parameterType, new HashMap<String, Object>());
    } catch (BuilderException e) {
        throw e;
    } catch (Exception e) {
        throw new BuilderException("Error invoking SqlProvider method ("
                + providerType.getName() + "." + providerMethod.getName()
                + ").  Cause: " + e, e);
    }
}
复制代码

实际实现可能要稍显复杂一些,主要是在执行反射调用时需要考虑到不同的入参场景,以及填充额外的ProviderContext类型的参数.

实际负责获取方法实际入参的方法是extractProviderMethodArguments()方法的两个重载形式,该方法除了负责根据形参列表加载实际参数之外,还会额外处理ProviderContext类型的参数.

private Object[] extractProviderMethodArguments(Object parameterObject) {
    if (providerContext != null) {
        Object[] args = new Object[2];
        args[providerContextIndex == 0 ? 1 : 0] = parameterObject;
        args[providerContextIndex] = providerContext;
        return args;
    } else {
        return new Object[]{parameterObject};
    }
}

private Object[] extractProviderMethodArguments(Map<String, Object> params, String[] argumentNames) {
    Object[] args = new Object[argumentNames.length];
    for (int i = 0; i < args.length; i++) {
        if (providerContextIndex != null && providerContextIndex == i) {
            args[i] = providerContext;
        } else {
            args[i] = params.get(argumentNames[i]);
        }
    }
    return args;
}
复制代码

实际负责完成方法调用,并获取SQL内容的是invokeProviderMethod()方法:

private String invokeProviderMethod(Object... args) throws Exception {
    Object targetObject = null;
    if (!Modifier.isStatic(providerMethod.getModifiers())) {
        targetObject = providerType.newInstance();
    }
    CharSequence sql = (CharSequence) providerMethod.invoke(targetObject, args);
    return sql != null ? sql.toString() : null;
}
复制代码

方法实现较简单,属于常规的反射操作.

还有一点要注意的是,在createSqlSource()返回SqlSource对象时,对得到的SQL语句进行了${}占位符的处理操作:

return sqlSourceParser.parse(replacePlaceholder(sql), parameterType, new HashMap<String, Object>());
复制代码

在替换占位符时,使用的Configuration对象的variables属性作为上下文参数:

private String replacePlaceholder(String sql) {
    return PropertyParser.parse(sql, configuration.getVariables());
}
复制代码

到这里,我们基本了解了ProviderSqlSource对象的源码实现了.

最后,我们再总结一下SqlSource和不同配置之间的关系.

CRUD配置转换为BoundSql


再补充了这么多零碎的知识点之后,再回头看XMLLanguageDrivercreateSqlSource()方法,是不是就好理解了呢?

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 动态元素解析器
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 构架SQL源
    return builder.parseScriptNode();
}

复制代码

现在我们再回头梳理一下XMLLanguageDriver的调用链:

@startuml
hide footbox
participant XMLLanguageDriver as xld
[-> xld ++:createSqlSource()

xld -> XMLScriptBuilder**

xld -> XMLScriptBuilder++:parseScriptNode()

XMLScriptBuilder->MixedSqlNode**
alt 动态SQL配置
XMLScriptBuilder -> DynamicSqlSource**

else
XMLScriptBuilder -> RawSqlSource**

end
return SqlSource
return SqlSource
@enduml
复制代码

调用链路


除了XMLLanguageDriver之外,LanguageDriver还有一种实现:RawLanguageDriver.

RawLanguageDriverXMLLanguageDriver的子类,他重写了父类的createSqlSource方法,限制方法只能返回RawSqlSource类型的SqlSource对象.

public class RawLanguageDriver extends XMLLanguageDriver {

    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        // 解析占位符和参数的关系
        SqlSource source = super.createSqlSource(configuration, script, parameterType);
        checkIsNotDynamic(source);
        return source;
    }

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        SqlSource source = super.createSqlSource(configuration, script, parameterType);
        checkIsNotDynamic(source);
        return source;
    }

    private void checkIsNotDynamic(SqlSource source) {
        if (!RawSqlSource.class.equals(source.getClass())) {
            throw new BuilderException("Dynamic content is not allowed when using RAW language");
        }
    }

}
复制代码

探究主键生成器

了解完了LanguageDriver之后,让我们来简单了解一下主键生成器.

KeyGeneratormybatis提供的主键生成器接口定义,它定义了两个方法,分别用来在CRUD语句执行前后来完成主键的处理操作.

public interface KeyGenerator {
    void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
    void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
复制代码

mybatis默认为KeyGenerator提供了3个实现:

KeyGenerator

其中Jdbc3KeyGenerator的实现依赖于JDBC3.0规范中的Statement#getGeneratedKeys()方法,他只支持processAfter()方法.

自JDBC3.0开始,Statement对象的getGeneratedKeys()可以获取因为执行当前 Statement对象而生成的主键,如果Statement没有生成主键,则返回空的ResultSet对象

关于Statement#getGeneratedKeys()的内容,可以参见文章Mybatis源码之美:3.8.探究insert,update以及delete元素的用法中关于useGeneratedKeys属性的相关内容.

SelectKeyGenerator的实现则对应着SelectKey元素的配置,他可以根据用户配置的SelectKey元素在执行目标CRUD语句的前后来完成主键的获取操作.

至于NoKeyGeneratormybatisKeyGenerator提供的一个空实现.

在这里,我们只是简单的了解一下KeyGenerator相关的内容,至于更详细的源码实现,我们会在后面的文章中一一展开.

关注下面的标签,发现更多相似文章
评论