mybatis解析模块

mybatis读取配置文件时都是通过xml来解析对应的配置。这里mybatis内部使用的是sax的解析方式,采用xpath的方式来查询xml中的数据。

xpath解析方式

关于xpath的解析方式这里自己手动来测试一个xml文件,对应的使用方法在mybatis源码中也有对应的实现

 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();

        //开启验证
        documentBuilderFactory.setValidating(true);
        documentBuilderFactory.setNamespaceAware(false);
        documentBuilderFactory.setIgnoringComments(true);
        documentBuilderFactory.setIgnoringElementContentWhitespace(false);
        documentBuilderFactory.setCoalescing(false);
        documentBuilderFactory.setExpandEntityReferences(true);


        //创建DocumentBuilder
        DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();

        //设置异常处理对象
        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 {
            }
        });

        //将文档加载到Document对象中
        Document doc = builder.parse("src/main/resources/mybatis-config.xml");
        XPathFactory xPathFactory = XPathFactory.newInstance();
        XPath xpath = xPathFactory.newXPath();

        NodeList result = (NodeList)xpath.evaluate("//property",doc, XPathConstants.NODESET);
        System.out.println(result.item(1).getAttributes().getNamedItem("value"));

这是一个main方法中的代码片段,主要作用是读取mybatis-config.xml中的对应的数据库地址

<property name="url" value="jdbc:mysql://119.23.25.22:3306/fzan?characterEncoding=UTF-8"/>

mybatis中的解析模块

这些类后面会说到。

先来看下XPathParser类,改类中的各个字段如下

  private final Document document; //Document 对象

  private boolean validation; //是否开启验证
  private EntityResolver entityResolver;  //用于加载本地DTD文件
  private Properties variables;//mybatis-config.xml中<propteries>边间键值对集合
  private XPath xpath;//xpath对象

document是xml解析后原始的一个Document对象,所以是一个final类型的。
然后是validation是一个bool类型的属性表示是否开启验证,默认情况下mybatis读取xml会联网加载dtd文件来校验mybatis的配置文件是否正确。但也不排除没有网络的情况,这样可以通过entityResolver来加载mybatis包中的离线dtd文档来校验。
EntityResolver是一个接口,其中XMLMApperEntityResolver就继承自该接口。
EntityResolver接口具体方法如下

    public abstract InputSource resolveEntity (String publicId,
                                               String systemId)
        throws SAXException, IOException;

该方法主要用户验证dtd并返回InputSource
XMLMapperEntityResolver类主要用于离线的dtd验证
该类字段如下

  //指定dtd的systemid
  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";

  //指定dtd具体路径
  private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

该类继承接口实现如下

  @Override
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
    try {
      if (systemId != null) {
        String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);//小写的systemid
        //查找systemid指定的DTD文档,并调用getInputSource()读取
        if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
          return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
        } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
          return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
        }
      }
      return null;
    } catch (Exception e) {
      throw new SAXException(e.toString());
    }
  }

  private InputSource getInputSource(String path, String publicId, String systemId) {
    InputSource source = null;
    if (path != null) {
      try {
        InputStream in = Resources.getResourceAsStream(path);//读取文件
        source = new InputSource(in);
        source.setPublicId(publicId);
        source.setSystemId(systemId);        
      } catch (IOException e) {
        // ignore, null is ok
      }
    }
    return source;
  }

InputSource这个类是通过XML实例产生的输入源
说完XMLMapperEntityResolver接着来看XPathParser

 private Document createDocument(InputSource inputSource)

这个方法主要用来创建Document的对象,代码实现可以参考前面的读取xml案例,几乎没什么区别,只不过在调用该方法前必须调用commonConstructor方法来给字段赋值。

 //通用构造器
  private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
  }

然后就是XPathParser中的各种eval方法了,用于解析boolean,long等类型的信息,具体实现都是通过一个通用的解析方法来完成的,通过returnType来指定需要返回的类型

 //通用解析方法  expression root   returnType
  private Object evaluate(String expression, Object root, QName returnType) {
    try {
      return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
      throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
  }

重点来介绍下string类型的解析情况,按道理string类型的解析应该是最简单的,但这里涉及到一个默认值的问题
举个例子

<!-- 允许占位符 -->
   *   <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
   *   <!-- 设置占位符分隔符 -->
   *   <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value=":?"/>
   *     <property name="driver" value="${driver:?com.mysql.jdbc.Driver}"/>

property中的name为driver的value可以通过占位符来指定,但如果这个driver值不存在就必须使用占位符了,但前提是必须开启占位符才能使用,enable-default-value值为true即可。mybatis中默认的分隔符是:,当然也可以改成别的
在解析string是用到了这样一个方法

  public static String parse(String string, Properties variables) {//mybatis-config.xml中<propteries>边间键值对集合
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

这个方法是PropertyParser中的一个静态方法,PropertyParser类中的具体字段如下

 private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
 //mybatis-config中是否开启默认值的配置
  public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
 //配置占位符与默认分隔符之间的配置
  public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";
  //默认关闭默认值功能
  private static final String ENABLE_DEFAULT_VALUE = "false";
  //默认分隔符为:号
  private static final String DEFAULT_VALUE_SEPARATOR = ":";

看到这些就应该知道mybatis-config.xml中配置文件为什么样这样写了
可以看到,这个方法是通过GenericTokenParser这个通用的字占位符解析器来完成解析并返回的
该类字段如下

private final String openToken; //${
  private final String closeToken;//}
  private final TokenHandler handler;//处理实例

它会按顺序查找openToken和closeToken解析得到的占位符字面值,并交给handler处理,将解析结果返回,解析主要代码如下

// search open token
    int start = text.indexOf(openToken, 0);//查找开始位置
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      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. //找到了open token 继续找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);//获取closetoken位置
        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();//从offset后面开始查找
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found. //如果没有closetoken
          builder.append(src, start, src.length - start);//没有占位符,相当于普通字符串
          offset = src.length;
        } else {
          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();
  }

可以看到这两个while循环的主要作用都是为了避开opentoken和closetoken被转义的情况,直到查找完成后会去调用 builder.append(handler.handleToken(expression.toString()));来得到最后的字符串
现在回到PropertyParser这个类,这个类中包含了一个静态的内部类

//校验处理的方法  一个处理用的静态类
  private static class VariableTokenHandler implements TokenHandler {
    private final Properties variables;
    private final boolean enableDefaultValue;
    private final String defaultValueSeparator;

    private VariableTokenHandler(Properties variables) { //构建对象
      this.variables = variables;
      this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
      this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
    }

    private String getPropertyValue(String key, String defaultValue) {
      return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
    }

    @Override
    public String handleToken(String content) {//返回内容
      if (variables != null) {
        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());
          }
          if (defaultValue != null) {//没有分隔符,直接返回值
            return variables.getProperty(key, defaultValue);
          }
        }
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }
  }

最终又handleToken方法来返回最后的字符串,这个方法是来自于TokenHandler接口的,该接口就这一个方法
GenericTokenParser不仅仅用于这里的默认解析,后面来可以用于sql语句的解析,GenericTokenParser只是查找指定的占位符,而具体的解析由持有的TokenHandler的实现不同而有所不同,这里有点策略模式的意思

接下来回到XPathParser,除了evalString方法外还有一个evalNode也需要注意下,它返回的类型是XNode,对org.w3c.dom.Node对象做了封装和解析

 private final Node node;
  private final String name;
  private final String body;
  private final Properties attributes;
  private final Properties variables;
  private final XPathParser xpathParser;
posted @ 2020-03-02 21:42  rw4y  阅读(182)  评论(0)    收藏  举报