parsing,从字面上理解就是编译解析的意思,那么这个包中的内容就应该和mybatis配置文件的编译解析有关系。本文首先会按照引用层次来分别介绍这个包中各个类的作用,而后再用实际的例子解释它们是如何组合到一起去解决了什么样的问题。
public interface TokenHandler { String handleToken(String content); }
这个接口中只有一个函数,就是对字符串进行处理。
从这个类的名字看到,这个类是对常用Token进行parser的类,我们首先了解这个类的属性和构造函数:
private final String openToken;//开始标识 private final String closeToken;//结束标识 private final TokenHandler handler;//token处理器 //利用带参数的构造函数初始化各项属性 public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; }
在了解完这个类的属性及构造函数后,我们来看下这个的主要的也是唯一的函数到底做了那些事情:
public String parse(String text) { StringBuilder builder = new StringBuilder(); if (text != null && text.length() > 0) {//如果传入的字符串有值 //将字符串转为字符数组 char[] src = text.toCharArray(); int offset = 0; //判断openToken在text中的位置,注意indexOf函数的返回值-1表示不存在,0表示在在开头的位置 int start = text.indexOf(openToken, offset); while (start > -1) { if (start > 0 && src[start - 1] == '\\') { //如果text中在openToken前存在转义符就将转义符去掉。如果openToken前存在转义符,start的值必然大于0,最小也为1 //因为此时openToken是不需要进行处理的,所以也不需要处理endToken。接着查找下一个openToken builder.append(src, offset, start - 1).append(openToken); offset = start + openToken.length();//重设offset } else { int end = text.indexOf(closeToken, start); if (end == -1) {//如果不存在openToken,则直接将offset位置后的字符添加到builder中 builder.append(src, offset, src.length - offset); offset = src.length;//重设offset } else { builder.append(src, offset, start - offset);//添加openToken前offset后位置的字符到bulider中 offset = start + openToken.length();//重设offset String content = new String(src, offset, end - offset);//获取openToken和endToken位置间的字符串 builder.append(handler.handleToken(content));//调用handler进行处理 offset = end + closeToken.length();//重设offset } } start = text.indexOf(openToken, offset);//开始下一个循环 } //只有当text中不存在openToken且text.length大于0时才会执行下面的语句 if (offset < src.length) { builder.append(src, offset, src.length - offset); } } return builder.toString(); }
简单的说,这个函数的作用就是将openToken和endToken间的字符串取出来用handler处理下,然后再拼接到一块。我们接下来看一个具体的handler,了解下它对传入的字符串做了怎样的处理。
PropertyParser这个类中包含一个内部私有的静态类VariableTokenHandler。VariableTokenHandler实现了TokenHandler接口,包含了一个Properties类型的属性,在初始化这个类时需指定该属性的值。VariableTokenHandler类对handleToken函数的具体实现如下:
public String handleToken(String content) { //如果variables不为空且存在key为content的property,就从variables中返回具体的值,否则在content两端添加上${和} if (variables != null && variables.containsKey(content)) { return variables.getProperty(content); } return "${" + content + "}"; }
在了解完PropertyParser的内部类VariableTokenHandler后,我们在来了解下PropertyParser类的parser静态方法:
public static String parse(String string, Properties variables) { //先初始化一个handler VariableTokenHandler handler = new VariableTokenHandler(variables); //在初始化GenericTokenParser对象,设置openToken为${,endToken为} //有没有对${}比较熟悉,这个符号就是mybatis配置文件中的占位符,例如定义datasource时用到的 <property name="driverClassName" value="${driver}" /> //同时也可以解释在VariableTokenHandler中的handleToken时,如果content在properties中不存在时,返回的内容要加上${}了。 GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); }
XPathParser类parsing包中的核心类之一,既然这个类是XPath的Parser,就需要对xpath的语法有所了解,如果对此不熟悉的读者最好能先了解xpath的语法(http://www.w3school.com.cn/xpath/index.asp)。
打开这个类的outline会发现这个类包含的函数真的是“蔚为壮观”,虽然数量众多,基本上可以分为两类:初始化(构造函数)、evalXXX。
XPathParser类的构造函数数量众多,是由于这个类的属性比较多,这些构造函数内部都会调用到如下函数:commonConstructor和createDocument。接下来我们来看看这两个函数具体做了那些事情:
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { //初始化这个类的基本属性 this.validation = validation; this.entityResolver = entityResolver; this.variables = variables; //利用XPathFactory创建一个新的xpath对象 XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); }
private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor // mybatis源代码基本上没有什么注释,但是上面这行注释是源代码中自带的。 // 那为什么必须在调用commonConstructor函数后才能调用这个函数呢?因为这个函数里面用到了两个属性:validation和entityResolver // 如果在这两个属性没有设置前就调用这个函数,就可能会导致这个类内部属性冲突 try { //创建document时用到了两个类:DocumentBuilderFactory和DocumentBuilder。 //为什么设置这两个类的这些属性,这些属性有什么作用。要完全介绍清楚需要不少篇幅,在这里就不做介绍了, DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { throw exception; } public void fatalError(SAXParseException exception) throws SAXException { throw exception; } public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(inputSource); } catch (Exception e) { throw new BuilderException("Error creating document instance. Cause: " + e, e); } }
这个类中的evalXXX函数有两种多态形式:一种是只有一个expression参数;另一种则有两个函数,除了expression参数外还包含一个root参数。像我们经常见到的那样,带一个参数的evalXXX函数会在设置一个默认值后调用带有两个参数的函数,我们看一个具体的例子evalString:
public String evalString(String expression) { //设置类中的document属性作为root, return evalString(document, expression); } public String evalString(Object root, String expression) { String result = (String) evaluate(expression, root, XPathConstants.STRING); result = PropertyParser.parse(result, variables); return result; }
而在带有两个参数的evalString中调用了evaluate函数,这个函数才是真正开始了对xpath表达式的解析:
private Object evaluate(String expression, Object root, QName returnType) { try { //调用xpath类进行相应的解析。 //注意returnType参数,虽然evaluate返回的数据类型是Object的,但是如果指定了错误的returnType,那么在进行类型转换时将会报类型转换异常 return xpath.evaluate(expression, root, returnType); } catch (Exception e) { throw new BuilderException("Error evaluating XPath. Cause: " + e, e); } }
其他的evalXXX和evalString大同小异,主要的不同在类型转换和returnType参数设置上。
接下来我们来了解parsing包中的最后一个类XNode。该类是对org.w3c.dom.Node类的一个封装,在Node类的基础上添加了一些新功能。
我们首先来看XNode类的构造函数:
public XNode(XPathParser xpathParser, Node node, Properties variables) { this.xpathParser = xpathParser; this.node = node; this.name = node.getNodeName(); this.variables = variables; //获取当前节点的所有属性 this.attributes = parseAttributes(node); //获取当前节点的文本节点内容,当然获取到的数据是已经经过TokenHandler处理过的 this.body = parseBody(node); }
构造函数调用了两个函数:parseAttributes和parseBody。parseAttributes函数相对简单些,就是利用Node类的函数去获取该节点的所有属性名和值,只是在获取属性值后会调用PropertyParser.parse()去处理下,在次就不贴源代码了。我们重点看下parseBody函数:
private String parseBody(Node node) { String data = getBodyData(node); //如果该节点不是文本节点或者CDATA节点,取其子节点值 if (data == null) { NodeList children = node.getChildNodes(); //尽管这个for循环不是一个好的实现方式,因为 children.getLength()被执行了多次,但在mybatis的源代码经常出现 for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); data = getBodyData(child); //只要一个节点为文本节点或者CDATA节点,就结束循环。因而此时的body值只是node的第一个文本节点的内容 if (data != null) break; } } return data; } private String getBodyData(Node child) { //如果这个节点是文本节点或者CDATA节点,就取节点的内容,然后用PropertyParser.parse()处理下 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; }
这个类中的evalXXX函数是通过XPathParser中的evalXXX来实现,以evalString为例
public String evalString(String expression) { //传入的object为XNode类的node属性 return xpathParser.evalString(node, expression); }
前面介绍了parseBody函数,通过这个函数设置了XNode类的body属性,现在就要通过getXXXBody函数获取body属性并将其转换为对应的数据类型。我们以getBooleanBody函数为例:
public Boolean getBooleanBody() { //设置默认值为null return getBooleanBody(null); } //两个函数的不同在于这个函数具有一个默认值,而上面的没有 public Boolean getBooleanBody(Boolean def) { if (body == null) { return def; } else { return Boolean.valueOf(body); } }
在介绍完getXXXBody后,我们再来看看getXXXAttribute。XNode类中的attributes属性是通过parseAttributes函数设置的,前面我们也做过简单的介绍。现在我们来看看getXXXAttribute的运行机制,以getBooleanAttribute为例,它的整体设计和getXXXBody很相似。
public Boolean getBooleanAttribute(String name) { return getBooleanAttribute(name, null); } public Boolean getBooleanAttribute(String name, Boolean def) { //从attributes获取key,如果存在则进行类型转换,否则就返回默认值 String value = attributes.getProperty(name); if (value == null) { return def; } else { return Boolean.valueOf(value); } }
这是我们最后要介绍的两个函数。我们先来看看getChildren,从字面上看这个函数的功能是获取node的所有子节点,但实际上是不是如此呢?我们看看它的实现:
public List<XNode> getChildren() { List<XNode> children = new ArrayList<XNode>(); //获取所有子节点 NodeList nodeList = node.getChildNodes(); if (nodeList != null) { for (int i = 0, n = nodeList.getLength(); i < n; i++) { Node node = nodeList.item(i); //如果子节点类型是元素节点,就添加到list中 if (node.getNodeType() == Node.ELEMENT_NODE) { children.add(new XNode(xpathParser, node, variables)); } } } return children; }
从代码中可以看到该函数并不是获取node所有的节点,它只是获取node的子元素节点。接下来我们看getChildrenAsProperties函数:
public Properties getChildrenAsProperties() { Properties properties = new Properties(); for (XNode child : getChildren()) { String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); //只有当节点同时具有name和value属性才会添加到properties中 if (name != null && value != null) { properties.setProperty(name, value); } } return properties; }
我们写一个示例看看这个包中的类是如何运行的。从上面的类和接口的介绍中可以发现,XPathParser类是比较上层的类(这里的上层,不是说这个类是各个类的超类,而是说它依赖的类较多)。用XPathParser类解析一段数据源定义的配置文件片段,首先设定properties文件的内容,文件名为jdbc.properties:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
username=root
password=1q2w3e
代码如下:
Properties properties = Resources.getResourceAsProperties("jdbc.properties"); //定义数据源的xml片段 String xml ="<?xml version='1.0' encoding='utf-8'?>"+ "<bean id='dataSource' class='org.apache.commons.dbcp.BasicDataSource' destroy-method='close' > " + " <property name='driverClassName' value='${driver}' />" + " <property name='url' value='${url}' /> " + " <property name='username' value='${username}' /> " + " <property name='password' value='${password}' /> " + "</bean>"; //初始化XPathParser XPathParser xPathParser = new XPathParser(xml,false,properties); //解析表达式,获取XNode对象 XNode xnode = xPathParser.evalNode("//bean"); //下面调用对应的函数 System.out.println(xnode); System.out.println(xnode.getValueBasedIdentifier()); System.out.println(xnode.getStringAttribute("id")); System.out.println(xnode.getStringAttribute("class"));
这段代码的执行结果如下:
<bean destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource" id="dataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root"/> <property name="password" value="1q2w3e"/> </bean> bean[dataSource] dataSource org.apache.commons.dbcp.BasicDataSource
从代码的执行结果可以看到${}中的内容已经被properties文件中对应的值所替换。
这是mybatis中进行${}转换的过程,下次再和spring中的替换过程进行下对比,看看两者在实现上有何不同。