MyBatis源码简读——2.2 SQL的解析

SQL初始化被分为主要几个内容

SQL初始化以LanguageDriver接口为主要内容,以获得SqlSource为结束。主要涉及到的几个内容为

  • LanguageDriver 语言驱动
  • XMLScriptBuilder SqlSource的构造器
  • LanguageDriverRegistry 注册表
  • NodeHandler 节点处理
  • DynamicContext 动态环境上下文
  • SqlNode SQL节点
  • SqlSource 的实现类
  • BoundSql 可执行的SQL封装
  • ParameterHandler 参数处理
  • LanguageDriver 语言驱动

LanguageDriver

语言驱动目前主要有两种语言驱动,

  • XMLLanguageDriver
  • RawLanguageDriver

其接口代码是

/**
 * 语言驱动接口
 */
public interface LanguageDriver {

  /**
   * 创建 ParameterHandler 对象。
   */
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

  /**
   * 创建 SqlSource 对象,从 Mapper XML 配置的 Statement 标签中,即 <select /> 等。
   */
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

  /**
   * 创建 SqlSource 对象,从方法注解配置,即 @Select 等。
   */
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}

XMLLanguageDriver

XML语言驱动实现类

RawLanguageDriver

RawSqlSource 语言驱动器实现类,确保创建的 SqlSource 是 RawSqlSource 类

createSqlSource 创建sql源


    @Override
      public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        // 创建 XMLScriptBuilder 对象,执行解析
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
      }
    
      @Override
      public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        // issue #3
        // 如果是 <script> 开头,使用 XML 配置的方式,使用动态 SQL
        if (script.startsWith("<script>")) {
          // 创建 XPathParser 对象,解析出 <script /> 节点
          XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
          // 调用上面的 #createSqlSource(...) 方法,创建 SqlSource 对象
          return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
        } else {
          // issue #127
          // 变量替换
          script = PropertyParser.parse(script, configuration.getVariables());
          // 创建 TextSqlNode 对象
          TextSqlNode textSqlNode = new TextSqlNode(script);
          // 如果是动态 SQL ,则创建 DynamicSqlSource 对象
          if (textSqlNode.isDynamic()) {
            return new DynamicSqlSource(configuration, textSqlNode);
            // 如果非动态 SQL ,则创建 RawSqlSource 对象
          } else {
            return new RawSqlSource(configuration, script, parameterType);
          }
        }
      }

其内部逻辑是使用XMLScriptBuilder对配置,XNode节点对象或者字符串对象和参数类型进行解析,如果是

XMLScriptBuilder

它是解析配置成SQL源的主要对象,后续对于这个类,我们还有很多东西需要学习,其构造方法和主要方法为

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    // 初始化 nodeHandlerMap 属性
    initNodeHandlerMap();
  }
  
  
  // 初始化 nodeHandlerMap 属性
  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 解析成 SqlSource 对象
  public SqlSource parseScriptNode() {
    // 解析SQL
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    // 创建 SqlSource 对象
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
  
  • 首先在构造方法中可以看到context就是XNode对象,同时mybatis直接初始化了一些节点处理对象的映射(initNodeHandlerMap中)。
  • parseScriptNode 是其主要的解析方法,其关键的几个方法是:
  • parseDynamicTags 将XNode解析成MixedSqlNode,根据配置和MixedSqlNode 解析成DynamicSqlSource或者RawSqlSource
parseDynamicTags
// 解析 SQL 成 MixedSqlNode 对象
  protected MixedSqlNode parseDynamicTags(XNode node) {
    // 创建 SqlNode 数组
    List<SqlNode> contents = new ArrayList<>();
    // 遍历 SQL 节点的所有子节点
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      // 当前子节点
      XNode child = node.newXNode(children.item(i));
      // 如果类型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 时
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        // 获得内容
        String data = child.getStringBody("");
        // 创建TextSql对象
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 验证是否为动态SQL,主要是验证"${"  "}",
        if (textSqlNode.isDynamic()) {
          // 添加到 contents 中
          contents.add(textSqlNode);
          // 标记为动态 SQ
          isDynamic = true;
          // 如果是非动态的 TextSqlNode 对象
        } else {
          // 创建 StaticTextSqlNode 添加到 contents 中
          contents.add(new StaticTextSqlNode(data));
        }
        // 如果类型是 Node.ELEMENT_NODE
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        // 根据子节点的标签,获得对应的 NodeHandler 对象
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        // 获得不到,说明是未知的标签,抛出 BuilderException 异常
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        // 执行 NodeHandler 处理
        handler.handleNode(child, contents);
        // 标记为动态 SQ
        isDynamic = true;
      }
    }
    // 创建 MixedSqlNode 对象
    return new MixedSqlNode(contents);
  }

其核心逻辑是,筛选出符合要去的节点然后通过MixedSqlNode对此节点进行一次报装,同时校验${}来判断是否动态参数

RawSqlSource指的是原始的SQL源,DynamicSqlSource指的是动态的数据源,其区别可以在getBoundSql看出来。

其继承了XMLLanguageDriver,主要是对SqlSource进行校验,保证为RawSqlSource类,其他逻辑都是使用父类方法

@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // 调用父类,创建 SqlSource 对象
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    // 校验创建的是 RawSqlSource 对象
    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");
    }
}

LanguageDriverRegistry 驱动注册表

其内部维护了一个语言驱动类和语言驱动的映射表,里面逻辑都比较简单

  private final Map<Class<? extends LanguageDriver>, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<>();

NodeHandler 节点处理接口

在上面的XMLScriptBuilder.initNodeHandlerMap中我们看到,他初始化了很多节点处理器,现在看下其主要内容

private interface NodeHandler { 
      // nodeToHandle 要处理的 XNode 节点
      // targetContents 目标的 SqlNode 数组。
      // 实际上,被处理的 XNode 节点会创建成对应的 SqlNode 对象,
      // 添加到 targetContents 中
      // 其实现类XXXHandler格式,其中XXX 为具体标签
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }
  

他是XMLScriptBuilder的一个内部接口,用来对各种节点进行处理。

这里需要添加一个图片,显示其实现类

简单的查看其中一个实现

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

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析内部SQL节点
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      // 获得 prefix、prefixOverrides、"suffix"、suffixOverrides 属性
      String prefix = nodeToHandle.getStringAttribute("prefix");
      String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
      String suffix = nodeToHandle.getStringAttribute("suffix");
      String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
      // 创建 TrimSqlNode 对象 并 添加到targetContents中
      TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
      targetContents.add(trim);
    }
  }

其逻辑是将XNode使用parseDynamicTags解析成MixedSqlNode,然后从MixedSqlNode中读取需要的参数,将这些获取到的参数封装到处理方法对应的SqlNode中,然后保存到整体的SqlNode列表中。
具体实现类比较多,对应的SqlNode也比较多,但是每个handler相对独立,有兴趣的可以单独看一下,不算太麻烦。

DynamicContext 动态环境上下文

一个动态环境的上下文

而其构造函数可以看出

// 构造函数, 对传入的parameterObject对象进行“map”化处理; 
  public DynamicContext(Configuration configuration, Object parameterObject) {
      // 当传入的参数对象不是Map类型时,Mybatis会将传入的POJO对象用MetaObject对象来封装
      // 当动态计算sql过程需要获取数据时,用Map接口的get方法包装 MetaObject对象的取值过程。
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
    // 添加 bindings 的默认值
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

ContextMap

ContextMap是其内部类,继承hashmap,从map中或者从其内部的MetaObject对象中取值

  static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;

    private MetaObject parameterMetaObject;

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

    @Override
    public Object get(Object 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;
    }
  }

ContextAccessor

是 DynamicContext 的内部静态类,实现 ognl.PropertyAccessor 接口,上下文访问器

  static {
      // Mybatis中采用了Ognl来计算动态sql语句,DynamicContext类中的这个静态初始块,很好的说明了这一点
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }

根据DynamicContext的静态代码块中可以制定,ContextAccessor是其处理上下文访问的工具
其取值逻辑可以看到

// ContextAccessor.java

   @Override
    public Object getProperty(Map context, Object target, Object name) {
      Map map = (Map) target;
      // 优先从 ContextMap 中,获得属性
      Object result = map.get(name);
      if (map.containsKey(name) || result != null) {
        return result;
      }
      // 如果没有,则从 PARAMETER_OBJECT_KEY 对应的 Map 中,获得属性
      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
        return ((Map)parameterObject).get(name);
      }
      return null;
    }

SqlNode SQL节点

每个 XML Node 会解析成对应的 SQL Node 对象
其主要作用就是讲SQLNode绑定到上下文中

/**
 * SQL Node 接口,每个 XML Node 会解析成对应的 SQL Node 对象
 * @author Clinton Begin
 */
public interface SqlNode {
    // context 上下文
  boolean apply(DynamicContext context);
}

写到这里要吐槽下有道笔记,竟然没有自动保存功能,昨天写完后直接合上电脑走了,第二天发现笔记都不见了,本来后面的内容我用2个小时写完了,现在全没了,自闭了自闭了

关于SqlNode.apply具体作用,我们可以根据调试过程来看一下,首先我们知道方法进来的才是是动态参数的上下文,那么最终实现的内容是什么呢?

// org/apache/ibatis/scripting/xmltags/IfSqlNode.java
@Override
  public boolean apply(DynamicContext context) {
    // 判断是否符合条件
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      // 符合执行contents的应用
      contents.apply(context);
      return true;
    }
    return false;
  }

比如这个IfSqlNode可以看到最后使用的contents.apply(context),这个contents是什么?其实是另外一个SqlNode,那它最终会调用到哪个SqlNode呢?
根据调试的结果可以看出来

在这里插入图片描述
最终调用的是org/apache/ibatis/scripting/xmltags/TextSqlNode.java
而其中的逻辑是

  @Override
  public boolean apply(DynamicContext context) {
    // 创建BindingTokenParser 和 GenericTokenParser对象
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 进行解析,并且将结果放入context中
    context.appendSql(parser.parse(text));
    return true;
  }

可以理解,最终是将文本内容拼接到context,最终拼接成有变量占位符的SQL字符串。
而可以里面出contents.apply里面的逻辑主要是根据对应的表达式判断是否符合,假如符合就将内容拼接到SQL中。

SqlNode的代码嵌套的比较复杂,第一时间自己也没看出个端倪,只好调试一步一步的操作查看程序逻辑,所以我建议想去进一步了解mybatis对if,where,set等等标签是如何解析的,在这一步一定要通过调试,结合起来自学。

SqlSource 的实现类

根据XMLScriptBuilder.parseScriptNode的后续逻辑是开始获得SqlSource。

public interface SqlSource {

  // 根据传入的参数对象,返回 BoundSql 对象
  BoundSql getBoundSql(Object parameterObject);

}

SqlSource的方法只有一个就是获取BoundSql

SqlSource主要有四个实现类

  • DynamicSqlSource
  • ProviderSqlSource
  • RawSqlSource
  • StaticSqlSource
    而根据逻辑这里主要学习DynamicSqlSource和RawSqlSource
if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }

DynamicSqlSource

/**
 * 实现 SqlSource 接口,动态的 SqlSource 实现类
 * @author Clinton Begin
 */
public class DynamicSqlSource implements SqlSource {

  private final Configuration configuration;
  // 根 SqlNode 对象
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // 应用 rootSqlNode
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    // 创建 SqlSourceBuilder 对象
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    // 解析出 SqlSource 对象
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // 获得 BoundSql 对象
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 添加附加参数到 BoundSql 对象中
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    // 返回 BoundSql 对象
    return boundSql;
  }

}

根据上面的逻辑可以看到,在传入参数数据对象的时候,

  1. mybatis通过配置和参数封装成DynamicContext。
  2. 然后获得Sql源的构建器,通过和参数的数据配合构建出SqlSource
  3. 最后通过参数替换获得最终的执行SQL

RawSqlSource

public class RawSqlSource implements SqlSource {

  // SqlSource 对象
  private final SqlSource sqlSource;

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

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    // 获得SQLsourceBuilder对象
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 获得sqlSource
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    // 创建 DynamicContext 对象
    DynamicContext context = new DynamicContext(configuration, null);
    // 绑定rootSqlNode
    rootSqlNode.apply(context);
    // 获得SQL
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // 根据参数对象获得BoundSql
    return sqlSource.getBoundSql(parameterObject);
  }

}

可以看到和DynamicSqlSource相比,因为并不存在SQL拼接,所以逻辑要简单很多,在getBoundSql的时候只需要进行简单的参数替换就可以返回结果

BoundSql 可执行的SQL封装

public class BoundSql {
  // SQL语句
  private final String sql;
  // 参数映射数组
  private final List<ParameterMapping> parameterMappings;
  // 参数对象
  private final Object parameterObject;
  // 附加的参数集合
  private final Map<String, Object> additionalParameters;
  // MetaObject 对象
  private final MetaObject metaParameters;

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

  public String getSql() {
    return sql;
  }

  public List<ParameterMapping> getParameterMappings() {
    return parameterMappings;
  }

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

一次可执行的SQL的封装,主要记录SQL,入参和结果的一些属性,都不算太复杂

ParameterHandler 参数处理

ParameterHandler主要进行参数的替换

public interface ParameterHandler {

  // 获得参数对象
  Object getParameterObject();

  // 设置PreparedStatement的占位符
  void setParameters(PreparedStatement ps)
      throws SQLException;

}

其主要的实现类就是DefaultParameterHandler。
而其设置参数的主要逻辑是

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 遍历 ParameterMapping 数组
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        // 获得 ParameterMapping 对象
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          // 查询 ParameterMapping 值
          Object value;
          // 获得属性名称
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          // 获得 typeHandler、jdbcType 属性
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 设置 ? 占位符的参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

整个逻辑大致可以分为几部分:

  • 从boundSql中获得参数映射。
  • 然后从boundSql或者传递来的参数,或者从configuration中获得和映射中符合条件的参数值。
  • 然后通过值和类型一次替换掉PreparedStatement中的占位符。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值