Ibatis 关于DynamicSql源码的解读

一. 业务背景

由于最近在做数据罗盘的开发,其中会有挺多的排序,聚会运算的要求。而我们利用了集团的garuda运算平台,想利用它建的智能索引和智能查询优化器来优化sql处理复杂业务,所以sql会堆上一些较复杂业务。为了更好的实践动态sql,了解它背后的原理,于是对它的源码进行了一番探索。


二. 分析Ibatis DynamicSql原理

1.实现动态SQL的核心流程

  动态sql的动态语句是通过动态标签来表达语义的,它们维护在配置文件中。生成动态sql主要有两个步骤:
  a. 初始化过程中,解析配置文件中的动态标签与sql配置,生成由SqlChild节点组成的抽象语法树。
  b. 请求处理过程中,根据运行期的参数对象解释抽象语法树,生成当前请求的动态SQL语句。

与动态sql解析生成的核心类有:
(1) SqlResource
该接口含义是作为sql对象的来源,通过该接口可以获取sql对象。其唯一的实现类是XmlSqlResource,表示通过xml文件生成sql对象。
(2) Sql
该接口可以生成sql语句和获取sql相关的上下文环境(如ParameterMap、ResultMap等),有三个实现类: RawSql表示为原生的sql语句,在初始化即可确定sql语句;SimpleDynamicSql表示简单的动态sql,即sql语句中参数通过$property$方式指定,参数在sql生成过程中会被替换,不作为sql执行参数;DynamicSql表示动态sql,即sql描述文件中包含isNotNull、isGreaterThan等条件标签。
(3)SqlChild
该接口表示sql抽象语法树的一个节点,包含sql语句的片段信息。该接口有两个实现类: SqlTag表示动态sql片段,即配置文件中的一个动态标签,内含动态sql属性值(如prepend、property值等);SqlText表示静态sql片段,即为原生的sql语句。每条动态sql通过SqlTag和SqlText构成相应的抽象语法树。
(4)SqlTagHandler
该接口表示SqlTag(即不同的动态标签)对应的处理方式。比如实现类IsEmptyTagHandler用于处理isEmpty标签,IsEqualTagHandler用于处理isEqual标签等。
(5)SqlTagContext
用于解释sql抽象语法树时使用的上下文环境。通过解释语法树每个节点,将生成的sql存入SqlTagContext。最终通过SqlTagContext获取完整的sql语句。
我使用的ibatis的版本是2.3.4.726,下面就结合源码分析下整个流程

2.解读源码

(1)初始化过程
a.XmlSqlSource.getSql()

   public Sql getSql() {
 state.getConfig().getErrorContext().setActivity("processing an SQL statement");

    boolean isDynamic = false;
    StringBuffer sqlBuffer = new StringBuffer(); 
    // 根据解析sqlmap的xml文件的配置,初始化DynamicSql对象
    DynamicSql dynamic = new DynamicSql(state.getConfig().getClient().getDelegate());
    //parseDynamicTags()方法通过配置文件生成DynamicSql的抽象语法树
    isDynamic = parseDynamicTags(parentNode, dynamic, sqlBuffer, isDynamic, false);
    String sqlStatement = sqlBuffer.toString();
    //判断是否是动态sql
    if (isDynamic) {
      return dynamic;
    //如果不是动态sql,返回原生sql
    } else {
      return new RawSql(sqlStatement);
    }
  }

该段代码主要是XmlSqlSource通过xml文件的配置生成sql对象,parseDynamicTags()生成抽象语法树方法是核心。

b.XmlSqlSource.parseDynamicTags()

private boolean parseDynamicTags(Node node, DynamicParent dynamic, StringBuffer sqlBuffer, boolean isDynamic, boolean postParseRequired) {
    state.getConfig().getErrorContext().setActivity("parsing dynamic SQL tags");

    // 遍历子节点
    NodeList children = node.getChildNodes(); 
    for (int i = 0; i < children.getLength(); i++) {
      Node child = children.item(i);
      String nodeName = child.getNodeName();
      // 如果是文本类型的子节点
      if (child.getNodeType() == Node.CDATA_SECTION_NODE
          || child.getNodeType() == Node.TEXT_NODE) {
        // 获得该节点的文本数据
        String data = ((CharacterData) child).getData();
        data = NodeletUtils.parsePropertyTokens(data, state.getGlobalProps());

        SqlText sqlText;
        // 如果不需要解析sql,直接将data的纯文本传入sqlText
        if (postParseRequired) {
          sqlText = new SqlText();
          sqlText.setPostParseRequired(postParseRequired);
          sqlText.setText(data);
        } else {
        // 否则调用parseInlineParameterMap()解析sql文本 
        // 主要将其中的参数#param#替换为占位符?
        sqlText = PARAM_PARSER.parseInlineParameterMap(state.getConfig().getClient().getDelegate().getTypeHandlerFactory(), data, null);
          sqlText.setPostParseRequired(postParseRequired);
        }

        // 将当前节点加入语法树中的父节点
        dynamic.addChild(sqlText);
        // 将该节点数据拼接上原生sql串
        sqlBuffer.append(data);

        // 如果是include标签
      } else if ("include".equals(nodeName)) {
        // 遍历出该子节点所有的属性,取出refid的属性值
        Properties attributes = NodeletUtils.parseAttributes(child, state.getGlobalProps());
        String refid = (String) attributes.get("refid");
        // 找到refid引用的node节点
        Node includeNode = (Node) state.getSqlIncludes().get(refid);
        // 如果引用的node节点为空
        if (includeNode == null) {
          // 在refid加上当前的命名空间作为新refid
          // 并用新refid查找该节点
          String nsrefid = state.applyNamespace(refid);          
          includeNode = (Node) state.getSqlIncludes().get(nsrefid);
          if (includeNode == null) {
            throw new RuntimeException("Could not find SQL statement to include with refid '" + refid + "'");
          }
        }
        // 用递归的方式让引用的节点加入抽象语法树
        isDynamic = parseDynamicTags(includeNode, dynamic, sqlBuffer, isDynamic, false);
      } else {
        state.getConfig().getErrorContext().setMoreInfo("Check the dynamic tags.");

        // 从SqlTagHandlerFactory中获取动态标签对应的
        // SqlTagHandler(iterate的是IterateTagHandler)
        SqlTagHandler handler = SqlTagHandlerFactory.getSqlTagHandler(nodeName);
        if (handler != null) {
          isDynamic = true;

          // 将动态节点配置信息配置sqlTag
          SqlTag tag = new SqlTag();
          tag.setName(nodeName);
          tag.setHandler(handler);

          // 遍历该子节点的所有属性,再将所有属性传入sqlTag
          Properties attributes = NodeletUtils.parseAttributes(child, state.getGlobalProps());
                  tag.setPrependAttr(attributes.getProperty("prepend"));      tag.setPropertyAttr(attributes.getProperty("property"));          tag.setRemoveFirstPrepend(attributes.getProperty("removeFirstPrepend"));              
tag.setOpenAttr(attributes.getProperty("open"));         tag.setCloseAttr(attributes.getProperty("close"));      tag.setComparePropertyAttr(attributes.getProperty("compareProperty"));       tag.setCompareValueAttr(attributes.getProperty("compareValue"));
tag.setConjunctionAttr(attributes.getProperty("conjunction"));

          // an iterate ancestor requires a post parse         
          if (dynamic instanceof SqlTag) {
            // 如果是sqlTag节点,并且是iterate标签则标记需要解析
            SqlTag parentSqlTag = (SqlTag) dynamic;
            if (parentSqlTag.isPostParseRequired() ||
                tag.getHandler() instanceof IterateTagHandler) {
              tag.setPostParseRequired(true);
            }
            // 如果是动态sql,也标记需要解析
          } else if (dynamic instanceof DynamicSql) {
            if (tag.getHandler() instanceof IterateTagHandler) {
              tag.setPostParseRequired(true);
            }
          }
          // 将sqlTag加入抽象语法树
          dynamic.addChild(tag);
          // 递归处理子节点
          if (child.hasChildNodes()) {
            isDynamic = parseDynamicTags(child, tag, sqlBuffer, isDynamic, tag.isPostParseRequired());
          }
        }
      }
    }
    state.getConfig().getErrorContext().setMoreInfo(null);
    return isDynamic;
  }

这段代码比较长,核心逻辑如下:
依次处理当前节点的每个子节点,判断当前子节点类型,根据不同类型采用不同处理方式:
(1) 处理sql文本节点
对于sql文本节点,生成sqlText对象。这个过程需要解析sql语句,将其中的参数#param#替换为?,生成sql语句和parameterMapping对象。sql文本解析主要通过InlineParameterMapParser.parseInlineParameterMap()实现。
(2) 处理include标签
sqlMapParaser在处理sql标签时,会将处理结果放入xmlParserState.sqlIncludes这个map对象中。 这里主要通过当前include id在sqlIncludes中获取对应的包含节点信息,再递归处理包含节点。
(3) 处理动态标签
首先获取动态标签对应的sqlTagHandler,接着通过动态标签配置生成sqlTag对象,最后递归处理当前节点的每个子节点。

动态sql初始化的核心目标是通过递归方式构建DynamicSql对象,它是一个抽象语法树,由SqlText和SqlTag节点构成。而请求处理过程正是通过参数对象解释该抽象语法树,生成sql语句。

(2)初始化过程
a.MappedStatement.executeQueryWithCallback()
Ibatis 关于DynamicSql源码的解读_第1张图片
MappedStatement是单条SQL执行时的上下文环境信息,如SQL标识、SQL、参数信息、返回结果、操作行为等。MappedStatement.executeQueryWithCallback()方法包含了参数值映射、sql准备和sql执行等关键过程。MappedStatement获得当前请求的sql对象是通过Sql.getSql()方法。如图所示,dynamicSql的getSql()是生成当前sql请求语句的函数。
b.DynamicSql.getSql()

public String getSql(StatementScope statementScope, Object parameterObject) {
    String sql = statementScope.getDynamicSql();
    if (sql == null) {
      // process()方法生成sql语句  
      process(statementScope, parameterObject);
      sql = statementScope.getDynamicSql();
    }
    return sql;
  }

c.DynamicSql.process()

private void process(StatementScope statementScope, Object parameterObject) {
    SqlTagContext ctx = new SqlTagContext();
    List localChildren = children;
    // 通过参数对象解释抽象语法树
    processBodyChildren(statementScope, ctx, parameterObject, localChildren.iterator());

    // 构建入参映射的ParameterMap
    ParameterMap map = new ParameterMap(delegate);
    map.setId(statementScope.getStatement().getId() + "-InlineParameterMap");
    map.setParameterClass(((MappedStatement) statementScope.getStatement()).getParameterClass());
    map.setParameterMappingList(ctx.getParameterMappings());

    String dynSql = ctx.getBodyText();

    // Processes $substitutions$ after DynamicSql
    // 处理通过$substitutions$拼接的动态sql
    if (SimpleDynamicSql.isSimpleDynamicSql(dynSql)) {
      dynSql = new SimpleDynamicSql(delegate, dynSql).getSql(statementScope, parameterObject);
    }

    // 将动态sql和配置参数传入StatementScope
    statementScope.setDynamicSql(dynSql);
    statementScope.setDynamicParameterMap(map);
  }

上面这段代码目标是构建sql执行语句和ParameterMap,构建sql执行语句通过processBodyChildren()方法完成。
d.

private void processBodyChildren(StatementScope statementScope, SqlTagContext ctx, Object parameterObject, Iterator localChildren, PrintWriter out) {
    // 遍历抽象语法树子节点
    while (localChildren.hasNext()) {
      SqlChild child = (SqlChild) localChildren.next();
      // 处理静态的sqlText
      if (child instanceof SqlText) {
        SqlText sqlText = (SqlText) child;
        String sqlStatement = sqlText.getText();
        // 如果是空格直接输出文本sqlStatement
        if (sqlText.isWhiteSpace()) {
          out.print(sqlStatement);
        // 如果不需要解析
        } else if (!sqlText.isPostParseRequired()) {

          // BODY OUT
          // 接输出文本sqlStatement
          out.print(sqlStatement);

          // 将传入的参数加入到抽象语法树的上下文中
          ParameterMapping[] mappings = sqlText.getParameterMappings();
          if (mappings != null) {
            for (int i = 0, n = mappings.length; i < n; i++) {
              ctx.addParameterMapping(mappings[i]);
            }
          }
        } else {

          // 取出iterateTag的上下文
          IterateContext itCtx = ctx.peekIterateContext();

          // 获取该上下文的下一个节点
          if(null != itCtx && itCtx.isAllowNext()){
            itCtx.next();
            itCtx.setAllowNext(false);
            // 如果是最后一个,就标记final
            if(!itCtx.hasNext()) {
              itCtx.setFinal(true);
            }
          }

          if(itCtx!=null) {
            StringBuffer sqlStatementBuffer = new StringBuffer(sqlStatement);
            // 将iterate标签里#list[]#和$list[]$
            // 里的动态内容替换为正确的要迭代的表达式
            iteratePropertyReplace(sqlStatementBuffer, itCtx);
            sqlStatement = sqlStatementBuffer.toString();
          }

          // 将#params#和$params$里的替换成具体的值
          sqlText = PARAM_PARSER.parseInlineParameterMap(delegate.getTypeHandlerFactory(), sqlStatement);
          // 获得参数map
          ParameterMapping[] mappings = sqlText.getParameterMappings();
          out.print(sqlText.getText());
          if (mappings != null) {
             for (int i = 0, n = mappings.length; i < n; i++) {
               ctx.addParameterMapping(mappings[i]);
             }
          }
        }
      // 解释SqlTag节点
      } else if (child instanceof SqlTag) {
        SqlTag tag = (SqlTag) child;
        SqlTagHandler handler = tag.getHandler();
        int response = SqlTagHandler.INCLUDE_BODY;
        do {
          StringWriter sw = new StringWriter();
          PrintWriter pw = new PrintWriter(sw);
          // 调用节点的开始处理函数
          response = handler.doStartFragment(ctx, tag, parameterObject);
          // 如果不是无效节点
          if (response != SqlTagHandler.SKIP_BODY) {
            // 递归处理子节点
            processBodyChildren(statementScope, ctx, parameterObject, tag.getChildren(), pw);
            pw.flush();
            pw.close();
            StringBuffer body = sw.getBuffer();
            // 调用节点的结束处理函数
            response = handler.doEndFragment(ctx, tag, parameterObject, body);
            // 调用prepend处理
            handler.doPrepend(ctx, tag, parameterObject, body);
            // 不过不是空节点,打印出sql本身
            if (response != SqlTagHandler.SKIP_BODY) {
              if (body.length() > 0) {
                out.print(body.toString());
              }
            }

          }
        // 如果是需要重复处理的节点,则继续处理
        } while (response == SqlTagHandler.REPEAT_BODY);

        // 删除的可能多余的第一个prepend
        ctx.popRemoveFirstPrependMarker(tag);

        // 弹出已处理的第一个iterate
        if(ctx.peekIterateContext()!= null && ctx.peekIterateContext().getTag() == tag) {
          ctx.setAttribute(ctx.peekIterateContext().getTag(), null);
          ctx.popIterateContext();
        }

      }
    }
  }

这段代码是抽象语法树的解释过程,核心逻辑如下:
(1) 处理SqlText节点
SqlText节点主要包含sql语句和ParameterMapping信息,这些信息在初始化阶段已经处理完毕,解释时直接输出即可。
(2)处理SqlTag节点
SqlTag节点包含sql动态配置信息,通过调用节点对应的SqlTagHandler进行解释处理。解释的流程控制通过response返回值完成,该常量在SqlTagHandler中定义,有以下三种值: INCLUDE_BODY表示当前节点生效;SKIP_BODY表示当前节点无效;REPEAT_BODY表示节点需要重复处理。下面以is标签举例,说明SqlTag节点处理过程。

(3) iterate标签举个栗子
a.SqlTagHandlerFactory

public class SqlTagHandlerFactory {

  private static final Map HANDLER_MAP = new HashMap();

  static {
    ......
    HANDLER_MAP.put("iterate", new IterateTagHandler());
    ......
  }

  private SqlTagHandlerFactory() {
  }

  public static SqlTagHandler getSqlTagHandler(String name) {
    return (SqlTagHandler) HANDLER_MAP.get(name);
  }

}

SqlTagHandlerFactory注册了iterate标签对应的处理IterateTagHandler,可以通过SqlTagHandlerFactory.getSqlTagHandler(nodeName)获得。

b.IterateTagHandler.doStartFragment()

public int doStartFragment(SqlTagContext ctx, SqlTag tag, Object parameterObject) {
    // 获得当前iterate节点
    IterateContext iterate = (IterateContext) ctx.getAttribute(tag);
    if (iterate == null) {
      // 获得下一个iterate节点
      IterateContext parentIterate = ctx.peekIterateContext();
      // 删除第一个可能多余的prepend
      ctx.pushRemoveFirstPrependMarker(tag);

      Object collection;
      // 获得tag中的属性值
      String prop = tag.getPropertyAttr();
      if (prop != null && !prop.equals("")) {
          // 如果下个节点不为空,标记不允许下一个
          if(null != parentIterate && parentIterate.isAllowNext()){
            parentIterate.next();
            parentIterate.setAllowNext(false);
            // 如果是最后一个iterate节点,打上final的标
            if(!parentIterate.hasNext()) {            
              parentIterate.setFinal(true);
            }
          }
          // 如果parentIterate不为空,将iterate标签里[]数组的值替换成正确的值
          if (parentIterate != null) {
              prop = parentIterate.addIndexToTagProperty(prop);
          }
        // 获得该对象的属性
        collection = PROBE.getObject(parameterObject, prop);
      } else {
        collection = parameterObject;
      }
      // 初始化iterate上下文
      iterate = new IterateContext(collection,tag, parentIterate);

      iterate.setProperty( null == prop ? "" : prop );
      // 传参入抽象语法树上下文
      ctx.setAttribute(tag, iterate);
      ctx.pushIterateContext(iterate);
    } else if 
    // 如果iterate是第一个出现的是第一个prepend
    ("iterate".equals(tag.getRemoveFirstPrepend())) {
      ctx.reEnableRemoveFirstPrependMarker();
    }

    // 如果iterate不为空,且iterate有下一个标签,返回有效节点
    if (iterate != null && iterate.hasNext()) {
      return INCLUDE_BODY;
    // 否则是无效节点
    } else {
      return SKIP_BODY;
    }
  }

doStartFragment()作为节点开始处理函数,主要是将当前节点的下一个节点的iterate标签里[]数组的值替换成正确的值,然后不为空,则返回有效节点标记。

c.IterateTagHandler.doEndFragment()

 public int doEndFragment(SqlTagContext ctx, SqlTag tag, Object parameterObject, StringBuffer bodyContent) {
    // 获得当前要处理的iterate节点
    IterateContext iterate = (IterateContext) ctx.getAttribute(tag);

    if (iterate.hasNext() || iterate.isFinal()) {
      // 如果iterate允许下一个节点,则返回下一个节点
      if(iterate.isAllowNext()) {
        iterate.next();
      }

      if (bodyContent.toString().trim().length() > 0) {
        // the sub element produced a result.  If it is the first one
        // to produce a result, then we need to add the open
        // text.  If it is not the first to produce a result then
        // we need to add the conjunction text
        // 对conjunction的属性值进行插入处理
        if (iterate.someSubElementsHaveContent()) {
          if (tag.isConjunctionAvailable()) {
            bodyContent.insert(0, tag.getConjunctionAttr());
          }
        } else {
            // we need to specify that this is the first content
            // producing element so that the doPrepend method will
            // add the prepend
            // iterate打上允许加上prepend的标
            iterate.setPrependEnabled(true);
            // 对open的属性值进行插入处理
            if (tag.isOpenAvailable()) {
              bodyContent.insert(0, tag.getOpenAttr());
            }
        }
        iterate.setSomeSubElementsHaveContent(true);
      }

      // 对close的属性值进行插入处理
      if (iterate.isLast() && iterate.someSubElementsHaveContent()) {
        if (tag.isCloseAvailable()) {
          bodyContent.append(tag.getCloseAttr());
        }
      }

      iterate.setAllowNext(true);
      // 如果当前iterate节点是最后一个,调用父类函数,返回INCLUDE_BODY
      if(iterate.isFinal()) {
        return super.doEndFragment(ctx,tag,parameterObject,bodyContent);
      // 否则返回循环调用节点标记
      } else {
        return REPEAT_BODY;
      }
    // 如果当前iterate节点不允许下一个,调用父类函数,返回INCLUDE_BODY
    } else {
      return super.doEndFragment(ctx,tag,parameterObject,bodyContent);
    }
  }

doEndFragment()是节点结束处理的函数,对iterate标签里的conjunction,open,close的属性值进行插入处理。并且如果不是最后一个节点,返回REPEAT_BODY(递归节点),否则返回INCLUDE_BODY(有效节点)。

d.doStartFragment.doPrepend()

public void doPrepend(SqlTagContext ctx, SqlTag tag, Object parameterObject, StringBuffer bodyContent) {
    IterateContext iterate = (IterateContext) ctx.getAttribute(tag);
    // 如果当前节点可以插入prepend属性,对当前prepend属性进行处理,并且标记不可再处理prepend属性。
    if (iterate.isPrependEnabled()) {
      super.doPrepend(ctx, tag, parameterObject, bodyContent);
      iterate.setPrependEnabled(false);  // only do the prepend one time
    }
  }

该函数主要是对iterate标签里的prepend属性进行处理。

请求处理过程核心目标是通过参数对象解释抽象语法树,生成当前请求的sql语句。该过程重点是对SqlTag节点的解析,通过调用该节点对应的SqlTagHandler完成处理。SqlTagHandler中又包含了前置节点处理函数(doStartFragment),后置节点处理函数(doEndFragment),和prepend处理。


三. 心得总结

动态sql的作用十分显著,在db层就解决了各种复杂的数据罗盘需要的逻辑。在查看它源码过程中,感受到了ibatis源码的精简与代码层次的清晰,看到了关于设计模式的巧妙运用:解释器模式(请求处理时根据参数对象解释语法树)、工厂模式(为动态标签的处理方式创建SqlTagHandlerFactory,根据标签名称获取对应的处理方式)、策略模式(将动态标签处理方式抽象为接口,针对不同标签有相应的实现类)。对于代码的编写,查看源码更有助于我们对所使用框架的理解,做到不仅要知其然,还要知其所以然。

你可能感兴趣的:(ibattis)