Mybatis源码解析:sql参数处理(1)

当sql带有参数时,Mybatis是如何处理的。使用的是User类。

 

//省略get set方法
public class User {
    private int id;
    private String name;
    private String phone;
}

例1 带有全局变量的sql

//UserMapper中的dao接口
List<User> getByglobal();
<select id="getByglobal" resultType="com.entity.User">
      select * from user where id = ${globalId}
  </select>
<!--mybatis.xml中的部分配置-->
  <properties>
    <property name="globalId" value="1"/>
  </properties>

注意我是用的符号为$。在这个例子中globalId是在mybatis.xml文件中的property配置的。接口不传参数。

,我们知道每一个查询语句都会被包装成一个MappedStatement,这个对象用来存放我们的sql语句,返回类型,id等等。让我们回到之前的代码。

//XMLMapperBuilder.configurationElement
  private void configurationElement(XNode context) {
    try {
      //mapper的缓存信息,命名空间等会被临时保存到MapperBuilderAssistant中,最后把这些公用的信息在存到MappedStatement中
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      //该节点已经被废弃
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //在解析增删改查节点时,每个节点都会生成一个mapperStatement对象并保存到配置文件类中.
      //mapperStatement保存这这个节点的全部信息,如id,fetchSize,timeout
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

这部分代码应该不陌生,现在我们从context.evalNodes("select|insert|update|delete")开始。该方法开始解析增删改查的节点

//XNode.evalNodes
  public List<XNode> evalNodes(String expression) {
    return xpathParser.evalNodes(node, expression);
  }
public List<XNode> evalNodes(Object root, String expression) {
    List<XNode> xnodes = new ArrayList<>();
    NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
      //这里创建的新的节点并添加到结合中,解析也是在创建节点的时候开始的。
      xnodes.add(new XNode(this, nodes.item(i), variables));
    }
    return xnodes;
  }

XNode构造方法,在创建新节点的时候,会在构造器中先进性解析,也就是调用parseBody方法.

public XNode(XPathParser xpathParser, Node node, Properties variables) {
    this.xpathParser = xpathParser;
    this.node = node;
    this.name = node.getNodeName();
    this.variables = variables;
    //获取节点的属性
    //例如这个select就有id和resultType两个属性
    this.attributes = parseAttributes(node);
    //解析节点里面的内容,也就是sql了
    this.body = parseBody(node);
  }

variables传入配置中的全局变量

//XNode.parseBody
  private String parseBody(Node node) {
    //获取当前节点的信息
    //例子中这里返回空
    String data = getBodyData(node);
    if (data == null) {
      //获取孩子节点的信息
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        //获取当前节点的信息
        data = getBodyData(child);
        if (data != null) {
          break;
        }
      }
    }
    return data;
  }

为什么select节点getBodyData会返回空呢,从它的方法体中可以看出,首先它会判断节点的类型,select这个节点是ELEMENT_NODE类型,不属于它要求的文本类型或者部分节点类型。那么就直接返回空了。而当select的孩子节点,也就是sql语句select * from user where id = ${globalId}这个节点调用getBodyData方法时,sql语句是文本类型的,满足条件,才会使用解析器开始解析。

//XNode.getBodyData
  private String getBodyData(Node child) {
    //判断节点的类型
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNodeType() == Node.TEXT_NODE) {
      String data = ((CharacterData) child).getData();
      data = PropertyParser.parse(data, variables);
      return data;
    }
    return null;
  }
//PropertyParser.parse
  public static String parse(String string, Properties variables) {
    //先创建一个处理器
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //创建解析器
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    //进行解析
    return parser.parse(string);
  }

这里出现了很多陌生的类。首先是GenericTokenParser通用类型的解析器,他能根据传入的参数做出相应。如果参数满足条件,就会调用handler处理器来处理参数。每个handler都要实现handleToken方法,该方法就是用来处理参数的。

例如这里传入的是以${作为开头,}作为结尾。如果传入的字符串包含一个或者多个这样的格式,就会调用VariableTokenHandler.handleToken,该方法会试图从全局中找到该变量,并修改成具体的值。

VariableTokenHandler.handleToken 传入String变量globalId,将其替换成1并返回。

public String handleToken(String content) {
     //variables里面存放全局的变量,为空直接return
      if (variables != null) {
        String key = content;
        //是否存在默认值,默认是false
        if (enableDefaultValue) {
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        //variables是用来存放全局变量的容器。
        //这里会从全局变量中找到我们定义的globalId,然后将对应的值返回,这样我们的sql就拼接完成了
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }
  }

解析器代码,根据传入的标记开始解析,这里传入开始标记${和结束标记$}。在这之后还会用来解析#{}。代码比较长,最好打个断点进去看。

//GenericTokenParser.parse
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    //查找开始标记,如果不存在返回-1 ,存在返回偏移量
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    //这个变量用来存放中间的字符,如${id}中的id
    StringBuilder expression = null;
    //如果存在开始标志
    while (start > -1) {
      //这里将从offset开始,一直到start的字符先放入builder中
      //例如select * from user where id = 
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        //更新偏移量
        offset = start + openToken.length();
        //找到与开始标志对应的结束标志
        int end = text.indexOf(closeToken, offset);
        //取到中间字符globalId
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //这里根据不同的处理器会有不同的操作,刚才传入的是VariableTokenHandler
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

到这里全局变量就解析完成了,那么如果在全局变量中没有找到对应的值该怎么办呢?例如我这里使用的sql是select * from user where id = ${id},而不是${globalId},那么根据VariableTokenHandler处理器,它会原封不动的进行返回,等待后文的解析。

顺便一提,这一部分的解析实在解析我们的配置文件的时候就发生了,方法入口为context.evalNodes("select|insert|update|delete"),在解析配置的时候,其他节点也大量使用了context.evalNodes()方法去解,所以只要当配置mybatis.xml文件中的properties节点解析完成之后,里面的变量就是能全局使用了,这也是为什么properties节点要放在第一个解析。

又由于这个通用解析器只解析${XXX}格式的变量,所以全局的变量不能写成#{xxx}.

 

                                                                    需要更多教程,微信扫码即可

                                                                              

                                                                                         👆👆👆

                                                        别忘了扫码领资料哦【高清Java学习路线图】

                                                                     和【全套学习视频及配套资料】

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值