Spring mybatis源码篇章-动态SQL节点源码深入

通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-动态SQL基础语法以及原理

前话

前文描述到通过mybatis默认的解析驱动类org.apache.ibatis.scripting.xmltags.XMLLanguageDriver,将mapper文件的CRUD节点均解析成对应的SqlNode接口。

本文将在前文的基础上具体分析select|update|insert|delete节点内的其他节点是如何被解析的,例如trim/where/set等嵌套节点,算是深入下mybatis的源码

SqlNode

优先观察下公共接口类org.apache.ibatis.scripting.xmltags.SqlNode,代码如下

public interface SqlNode {
  boolean apply(DynamicContext context);
}

内部只有一个apply()方法,其应该会在上下文context操刀,我们先可以看下有哪些类型的SqlNode

TextSqlNode

对类型为CDATA块或者TEXT的包装为TextSqlNode对象,形如

<select id="testQuery">
	<![CDATA[
		select * from tb_test
	]]>
</select>

或者

<select id="testQuery" parameterType="java.lang.String">
	select * from tb_test where name = #{name}
</select>

笔者此处看下其是如何实现apply()方法把,代码如下

  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context));
    context.appendSql(parser.parse(text));
    return true;
  }

关于如何解析相关的SQL语句就不展开了,大意上是针对含有${}符号的字符串进行相应的替换,从而拼装成完整的SQL保存至相应的org.apache.ibatis.scripting.xmltags.DynamicContext上下文对象中


另外此类还有一个关键的方法isDynamic(),其是为了判断相应的字符串是否为动态SQL

public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
	// same as
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
	// true while the text contains '${}'
    return checker.isDynamic();
  }

只有SQL语句含有${}标志符号,才会返回true

StaticTextSqlNode

保存无特殊字符${}的SQL语句,即其是建立在上述的TextSqlNode#isDynamic()方法的基础上,在返回为false的情况下被包装。
apply()方法也特别的简单

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

MixedSqlNode

其内部只有一个类型为java.util.Listcontents属性,主要作用是保存一个CRUD节点下所含有的所有SqlNode集合,方便统一管理以及解析

public class MixedSqlNode implements SqlNode {
  private List<SqlNode> contents;

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

  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

其他类型的SqlNode基本都是基于MixedSqlNode来进行包装解析的,而它们基本都有对应的NodeHandler类来与之对应解析

NodeHandler

其是org.apache.ibatis.scripting.xmltags.XMLScriptBuilder的私有内部接口类,而相应的动态SQL节点都是提前被保存在了下述代码中的nodeHandlers变量中

  private Map<String, NodeHandler> nodeHandlers = new HashMap<String, NodeHandler>() {
    private static final long serialVersionUID = 7123056019193266281L;

    {
      put("trim", new TrimHandler());
      put("where", new WhereHandler());
      put("set", new SetHandler());
      put("foreach", new ForEachHandler());
      put("if", new IfHandler());
      put("choose", new ChooseHandler());
      put("when", new IfHandler());
      put("otherwise", new OtherwiseHandler());
      put("bind", new BindHandler());
    }
  };

BindHandler

用于解析bind标签节点
例如

<bind name="title" value="'%' + _parameter.getTitle() + '%'"/>

对应的解析语句如下

  private class BindHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
	  //获取name和value属性
      final String name = nodeToHandle.getStringAttribute("name");
      final String expression = nodeToHandle.getStringAttribute("value");
	  //包装成简单的VarDeclSqlNode类
      final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
      targetContents.add(node);
    }
  }

会包装成org.apache.ibatis.scripting.xmltags.VarDeclSqlNode节点进行相应的解析


VarDeclSqlNode#apply()

public boolean apply(DynamicContext context) {
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    context.bind(name, value);
    return true;
}

很明显就是将name和计算后的真实value值对应关系保存至DynamicContext#bindings属性(HashMap类型)中

TrimHandler

用于解析trim标签节点

  private class TrimHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
	  //trim标签下可包含where/set/if/when等标签,将之封装成MixedSqlNode
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
	  // read prefix/preffixOverrides/suffix/suffixOverrides properties
      String prefix = nodeToHandle.getStringAttribute("prefix");
      String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
      String suffix = nodeToHandle.getStringAttribute("suffix");
      String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
	  // delegate TrimSqlNode to process trim sql
      TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
      targetContents.add(trim);
    }
  }

直接包装成org.apache.ibatis.scripting.xmltags.TrimSqlNode对象


TrimSqlNode
首先观察下构造函数

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

protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
   //这里contents一般为MixedSqlNode,内部包含多个SqlNode
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
  }

读取的preffixOverrides/suffixOverrides属性可以是符合以|为分隔符的字符串,比如"and | or"会被解析为List["AND","OR"]形式

笔者此处再简单看下apply()方法,代码如下

  public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // first,parse nested nodes
    boolean result = contents.apply(filteredDynamicContext);
    // aim to prefixOverrides and suffixOverrides,generate corrent sql
    filteredDynamicContext.applyAll();
    return result;
  }

经过上述的代码执行后,生成的SQL语句均是转为大写形式的~~~

WhereHandler

用于解析where标签节点

  private class WhereHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      // same as TrimSqlNode
      WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
      targetContents.add(where);
    }
  }

WhereSqlNode
经过查看,其是TrimSqlNode的子类,简单看下其源码

public class WhereSqlNode extends TrimSqlNode {

  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

很简单,就是固定了参数prefix=WHEREprefixOverrides=List["AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"],其余调用父类方法即可,详见上文

SetHandler

用于解析set标签节点

  private class SetHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      // same as TrimSqlNode
      SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
      targetContents.add(set);
    }
  }

SetSqlNode
经过查看,其也是TrimSqlNode的子类,简单看下其源码

public class SetSqlNode extends TrimSqlNode {

  private static List<String> suffixList = Arrays.asList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", null, null, suffixList);
  }

}

很简单,就是固定了参数prefix=SETsuffix=null(会将suffixOverrides符合的条件置为空)、suffixOverrides=List[","],其余调用父类方法即可,详见上文

ForEachHandler

用于解析foreach标签节点

  private class ForEachHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      // read collection/item/index/open/close/separator properties
      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");
      // independent SqlNode
      ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
      targetContents.add(forEachSqlNode);
    }
  }

foreach节点的属性简析如下

  • collection 代表的是集合的类型,例如list代表参数类型为List.class,array代表参数类型为数组类型
  • item 代表集合的value值
  • index 代表集合的key值,可为下标值也可为HashMap中的key值
  • open 类似于prefix属性
  • close 类似于suffix属性
  • separator 拼装的分隔符号,多为","

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;
  }

IfHandler

用于解析if标签节点

  private class IfHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      // read test properties
      String test = nodeToHandle.getStringAttribute("test");
      // 
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

IfSqlNode
条件判断解析类,内部代码很简单,如下

public class IfSqlNode implements SqlNode {
  private ExpressionEvaluator evaluator;
  private String test;
  private SqlNode contents;

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

  public boolean apply(DynamicContext context) {
    //主要作用即是用于条件的判断
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}

org.apache.ibatis.scripting.xmltags.ExpressionEvaluator主要是应用OGNL语法进行解析类似name !=null,其会读取上下文中是否有对应的属性值。具体的读者可自行分析

OtherwiseHandler/ChooseHandler

用于解析otherwise/choose/when节点,这三者一般搭配使用
1.OtherwiseHandler

  private class OtherwiseHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      targetContents.add(mixedSqlNode);
    }
  }

2.ChooseHandler,内含choose/when的解析

  private class ChooseHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
      List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
      //解析choose...when..otherwise结构
      handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
      //检查otherwise标签是否只有一个,大于一个则报错
      SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
      ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
      targetContents.add(chooseSqlNode);
    }
    
    // when标签使用IfHandler解析,otherwise标签使用OtherwiseHandler
    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 = nodeHandlers.get(nodeName);
        if (handler instanceof IfHandler) {
          handler.handleNode(child, ifSqlNodes);
        } else if (handler instanceof OtherwiseHandler) {
          handler.handleNode(child, defaultSqlNodes);
        }
      }
    }

    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;
    }
  }

ChooseSqlNode
简单看下其构造函数

public class ChooseSqlNode implements SqlNode {
  private SqlNode defaultSqlNode;
  private List<SqlNode> ifSqlNodes;

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

  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;
  }
}

ChooseSqlNode存放多个IfSqlNode和单个TextSqlNode/StaticTextSqlNode,
choose..when..otherwise结构类似于java的switch..case结构

总结

对本文提及的内容作下简单的小结

  1. where/set标签均可看做是trim标签的子类

  2. choosewhen/otherwise搭配使用,可以有多个when标签,只允许至多单个otherwise标签

  3. 除了bind标签,其余标签底下均可以有多个其他标签

  4. TextSqlNode/StaticTextSqlNode可以说是CRUD解析sql的基础类

  5. _parameter属性表示DAO接口方法对应的入参

posted @ 2017-03-29 22:48  南柯问天  阅读(1364)  评论(0编辑  收藏  举报