parsing,从字面上理解就是编译解析的意思,那么这个包中的内容就应该和mybatis配置文件的编译解析有关系。本文首先会按照引用层次来分别介绍这个包中各个类的做用,然后再用实际的例子解释它们是如何组合到一块儿去解决了什么样的问题。node
public interface TokenHandler { String handleToken(String content); }
这个接口中只有一个函数,就是对字符串进行处理。mysql
从这个类的名字看到,这个类是对经常使用Token进行parser的类,咱们首先了解这个类的属性和构造函数:spring
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; }
在了解完这个类的属性及构造函数后,咱们来看下这个的主要的也是惟一的函数到底作了那些事情:sql
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,了解下它对传入的字符串作了怎样的处理。express
PropertyParser这个类中包含一个内部私有的静态类VariableTokenHandler。VariableTokenHandler实现了TokenHandler接口,包含了一个Properties类型的属性,在初始化这个类时需指定该属性的值。VariableTokenHandler类对handleToken函数的具体实现以下:apache
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)。mybatis
打开这个类的outline会发现这个类包含的函数真的是“蔚为壮观”,虽然数量众多,基本上能够分为两类:初始化(构造函数)、evalXXX。app
XPathParser类的构造函数数量众多,是因为这个类的属性比较多,这些构造函数内部都会调用到以下函数:commonConstructor和createDocument。接下来咱们来看看这两个函数具体作了那些事情:dom
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中的替换过程进行下对比,看看二者在实现上有何不一样。