MyBatis源码解析(二) 解析器模块

mybatis源码解析系列源码基于 3.5.2-SNAPSHOT 版本,截至笔者开始写第一篇源码解析时这是官方的最新master分支。

写文章的过程中参考了很多大佬的文章和思路,以及一些为了解释清楚所必须的图。如有侵权,请联系删除。

参考内容:芋道源码博客 《MyBatis技术内幕》以及其他很多文章及书籍,以及开源项目,不一一列举。

引言:在之前的项目结构概览里提到了初始化项目的时候解析 mybatis-config.xml 文件,其中解析操作就是解析器模块来实现的。解析器模块还可以为映射配置文件提供支持,以及处理动态SQL语句中的占位符。

先看一下 pasring 包下的文件:

MyBatis源码解析(二) 解析器模块_第1张图片

前面说了mybatis解析xml 是基于 XPath 封装的,所以很显然XPathParser就是解析 xml 文件的了,先拿这个开始入手。

1.XPathParser

贴一下XPathParser的属性:

  // XML 文档对象,例如 mybatis-config.xml *Mapper.xml 等
  private final Document document;
  //是否校验 XML ,一般为 true
  private boolean validation;
  // XML 实体解析器
  private EntityResolver entityResolver;
  //变量 properties 对象
  private Properties variables;
  //JDK XPath 对象
  private XPath xpath;

这里面不太好理解的就是 EntityResolver 了,不是说XPathParser就是解析 xml 的吗?怎么这里又来了一个什么鬼实体解析器。XML是有一套约束用来校验文件的,而校验XML文件时会基于开始位置指定的 DTD / XSD 文件来校验。而这里指定的都是网络位置。当服务部署在网络环境不好或或者内网环境下时,就会无法下载约束文件,导致校验失败。所以 MyBatis 自定义了 EntityResovler 实现,用于加载本地的DTD文件

veriables 主要是用于记录 mybtais-config.xml 中的变量的键值对,比如在 xml 文件中定义了 ${username} 等变量,就由此字段来保存具体值。

1.1构造方法

xml的构造方法有16个,不过除了参数不太一样之外,其他基本上是差不多的,这里拿下面的构造方法做一个示例讲解:

/**
   * XPathParser 构造方法
   * @param xml xml文件地址
   * @param validation 是否校验xml
   * @param variables 变量properties对象
   * @param entityResolver xml实体解析器
   */
  public XPathParser(String xml, boolean validation, Properties variables, EntityResolver entityResolver) {
    //公用构造方法
    commonConstructor(validation, variables, entityResolver);
    //解析XML文件到 document 对象
    this.document = createDocument(new InputSource(new StringReader(xml)));
  }

公用构造方法 org.apache.ibatis.parsing.XPathParser#commonConstructor 如下:

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    //赋值
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    //创建XPathFactory实例
    XPathFactory factory = XPathFactory.newInstance();
    //实例化xpath对象
    this.xpath = factory.newXPath();
  }

创建 document 对象方法 org.apache.ibatis.parsing.XPathParser#createDocument 如下:

private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
      //创建 DocumentBuilderFactory
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      //设置是否校验xml
      factory.setValidating(validation);

      factory.setNamespaceAware(false);
      factory.setIgnoringComments(true);
      factory.setIgnoringElementContentWhitespace(false);
      factory.setCoalescing(false);
      factory.setExpandEntityReferences(true);

      //创建 DocumentBuilder
      DocumentBuilder builder = factory.newDocumentBuilder();
      //设置 xml 实体解析器
      builder.setEntityResolver(entityResolver);
      //设置异常处理 实现空方法
      builder.setErrorHandler(new ErrorHandler() {
        @Override
        public void error(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
        }
      });
      //解析 xml 文件
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }

以上就是 XPathParser 的构造方法了,虽然有贴代码的嫌疑,但其实底层都是java解析xml的代码,和mybatis的关系不大,所以就不详解了,后续有可能的话去写一篇博客,其实很多框架都涉及xml解析校验这些,比如 Spring。


1.2eval* 方法

eval* 方法有很多个,用于解析 Boolean , String,Integer,Node等类型的元素或节点值的值。

eval* 方法最后调用的都是 org.apache.ibatis.parsing.XPathParser#evaluate 。下面对方法做一个简单解析:

/**
   * 获取指定元素或值
   * @param expression 表达式
   * @param root 指定节点
   * @param returnType 返回类型
   * @return
   */
  private Object evaluate(String expression, Object root, QName returnType) {
    try {
      //xpath api获取指定元素节点或值
      //javax.xml.xpath.XPath.evaluate(java.lang.String, java.lang.Object, javax.xml.namespace.QName)
      return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
      throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
  }

1.2.1evalString()

这个方法和其他方法不太相同的是在调用完 evaluate() 方法之后会调用 org.apache.ibatis.parsing.PropertyParser#parse 处理节点中相应的默认值。

public String evalString(Object root, String expression) {
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    //如果result是动态值,则基于 variables 替换动态值,也就是mybatis如何替换动态值的实现方式
    result = PropertyParser.parse(result, variables);
    return result;
  }

调用 evaluate方法获取到值,然后调用 org.apache.ibatis.parsing.PropertyParser#parse 基于 variables 替换动态值。在 parse 方法中会创建 GenericTokenParser 解析器,将默认值委托给 org.apache.ibatis.parsing.GenericTokenParser#parse 方法来处理,方法会顺序查找openToken和closeToken,解析得到其占位符的字面值,并将其交给TokenHadler处理,然后将解析结果拼装成字符串并返回。

因为代码略长,就不贴代码了。

org.apache.ibatis.parsing.PropertyParser#parse

public static String parse(String string, Properties variables) {
    //创建 VariableTokenHandler
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //创建 GenericTokenParser,并指定其处理的占位符格式为 ${}
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    //执行解析
    return parser.parse(string);
  }

1.3EntityResolver

XPathParser还有一个属性就是 EntityResolver了,前面说到这个是用来加载本地的DTD文件的。但是这是一个接口,里面只有一个 resolveEntity() 。按照惯例,需要找其默认实现来看看里面到底有什么套路。默认实现是XMLMapperEntityResolver 。类上面的注释说这个是 MyBatis DTD的离线实体解析器,嗯,应该就是它了。

属性值如下,制定了配置文件的 DTD 和 mapper.xml文件的 DTD 位置。文件就在builer包下,感兴趣的可以去看一下。

private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
  private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
  private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";

  //mybatis-3-config.dtd 文件本地位置
  private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
  //mybatis-3-mapper.dtd 文件本地位置
  private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

解析的代码相对比较简单,可以使用 org.apache.ibatis.autoconstructor.AutoConstructorTest#fullyPopulatedSubject debug看一下怎么解析的,其实前面提到的createDocument()方法里 builder.parse() 方法就有调用这个方法。


2.token以及GenericTokenParser 和TokenHandler

前面的代码里提到了token这个东西,这里解释一下token是个啥。在《MyBatis技术内幕》里,给的解释是占位符。debug

org.apache.ibatis.parsing.GenericTokenParser#parse 时,openToken和closeToken的值分别是 ${ 和 } 。是不是很熟悉?其实就是占位符而已。

GenericTokenParser 这个看名字是通用Token解析器,那不通用的呢?不通用的由TokenHandler来处理。GenericTokenParser主要做的事情就是将openToken和closeToken之前的字符串取出来,然后交由handler处理,再拼接到一块。这个处理的handler就是由 org.apache.ibatis.parsing.PropertyParser.VariableTokenHandler#handleToken 来完成的。

org.apache.ibatis.parsing.PropertyParser.VariableTokenHandler#handleToken 前面说到这个方法可以替换动态值,实现逻辑是什么样的呢?

 public String handleToken(String content) {
      //variables 不为空
      if (variables != null) {
        //key为content
        String key = content;
        //检测是否支持占位符中使用默认值的功能
        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());
          }
          //有默认值,在variables中查找指定的占位符
          if (defaultValue != null) {
            //在variables中查找指定的占位符
            return variables.getProperty(key, defaultValue);
          }
        }
        //未开启默认值,直接替换
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      //variables 为空,在content的前后加上${和}
      return "${" + content + "}";
    }
  }

TokenHandler与 parsing 包相关的实现只有 org.apache.ibatis.parsing.PropertyParser.VariableTokenHandler。这个主要是用来替换动态值的,是 PropertiesParser 的内部类,其中的handleToken方法逻辑已经在上面写了详细的注释,这里不再重复。


parsing包的解析差不多就到这里了,其实还有部分代码和方法没有去写解析,但是主线和比较重要的部分这里已经大致说了一些。比如动态值替换是怎么实现的。源码解析也不一定是每一部分都要实现,如果每一步都去解析一遍,就可以考虑自己造个轮子了。


MyBatis源码解析(二) 解析器模块_第2张图片

你可能感兴趣的:(Mybatis,源码解析)