与大多数ORM框架一样,iBatis2也是用Xml描述ORM映射信息(在annotations出现之前),那么这些XML配置信息是怎么解析呢?呵呵,大部分人看到这儿可能会说:这有啥难的,用DOM或者SAX解析xml都是很容易的事!确实iBatis解析xml的方法也无外乎这二者之一,不过仔细读过iBatis解析XML的源码,我发现iBatis解析xml的代码很值得我们学习……
iBatis中最重要的一个接口是SqlMapClient,首先看看在程序中是怎么样同过配置文件得到SqlMapClient对象的:
static { try { String resource = "com/ppsoft/ibatis/test/config/SqlMapConfig.xml"; Reader reader = Resources.getResourceAsReader (resource); sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException ("Error initializing MyAppSqlConfig class. Cause:"+e); } }
类SqlMapClientBuilder提供了几个静态方法,用于读取iBatis配置文件并创建 SqlMapClient对象,这一章主要是分析iBatis是如何读取配置文件,所以也只看解析xml文件的部分,那再看看buildSqlMapClient方法中都做了些什么事:
public static SqlMapClient buildSqlMapClient(Reader reader) { return new SqlMapConfigParser().parse(reader); }
首先创建一个SqlMapConfigParser, 调用新创建的 SqlMapConfigParser对象的parser方法,将解析xml和创建
SqlMapClient的工作委托给 SqlMapConfigParser对象。那么接下来看看 SqlMapConfigParser的parse方法都干了些
啥:
public SqlMapClient parse(Reader reader) { try { usingStreams = false; parser.parse(reader); return state.getConfig().getClient(); } catch (Exception e) { throw new RuntimeException("Error occurred. Cause: " + e, e); } }
呵呵,看到这里你会发现,其实SqlMapConfigParser的parse方法也没干啥,只是将解析工作委托给 SqlMapConfigParser的一个parser属性,看看 SqlMapConfigParser的parser属性是啥东西:
protected final NodeletParser parser = new NodeletParser(); //state用于存储所有解析出来的信息 private XmlParserState state = new XmlParserState();
原来parser属性是一个NodeletParser对象,xml就是由NodeletParser这个类解析的,这个类时下面分析的重点。那我们再来看看NodeletParser这个类的parse方法是如何解析xml的:
public void parse(Reader reader) throws NodeletException { try { Document doc = createDocument(reader); parse(doc.getLastChild()); } catch (Exception e) { throw new NodeletException("Error parsing XML. Cause: " + e, e); } }
首先创建document对象(调用JAXP创建的,并且根据DTD文件检验了xml的格式是否正确),然后调用NodeletParser中的另外一个重载的parse方法:
public void parse(Node node) { Path path = new Path(); processNodelet(node, "/"); process(node, path); }
先创建一个Path对象(Path是NodeletParser中定义的一个内部类),然后调用processNodelet方法,最后调用了process(node,path);先看看processNodelet方法干嘛啦?
private void processNodelet(Node node, String pathString) { Nodelet nodelet = (Nodelet) letMap.get(pathString); if (nodelet != null) { try { nodelet.process(node); } catch (Exception e) { throw new RuntimeException("Error parsing XPath '" + pathString + "'. Cause: " + e, e); } } }
参数pathString实际上是个xpath字符串,从这段代码可以看出NodeletParser有个letMap的属性,是一个Map,以xpath为key,Nodelet对象为value。这段代码逻辑是:根据传入的xpath查找letMap有没有对应的Nodelet对象,如果有就调用对应Nodelet对象的process方法,参数为要处理的Node。那么这个Nodelet到底是什么东西呢?看看代码就知道啦:
public interface Nodelet { void process (Node node) throws Exception; }
原来只是个接口而以,将对节点的处理抽象出来,这个设计很高明:将节点处理方法抽象成Nodelet接口,sqlMap中存储处理每个Node的Nodelet对象(我们可以称之为Node处理器),key为Node的xpath,如果我们指定好每个Node的处理器对象,那么只需要遍历所有的节点,并到sqlMap查找对应Nodelet对象调用其process方法即可完成对xml的解析处理。
下面我们来分析下process(node,path)方法做了些什么事情。看这个方法的代码前先得看看Path这个类时干嘛:
private static class Path { private List nodeList = new ArrayList(); public Path() { } public Path(String xpath) { StringTokenizer parser = new StringTokenizer(path, "/", false); while (parser.hasMoreTokens()) { nodeList.add(parser.nextToken()); } } public void add(String node) { nodeList.add(node); } //删除xpath路径中的最后一个节点 public void remove() { nodeList.remove(nodeList.size() - 1); } public String toString() { StringBuffer buffer = new StringBuffer("/"); for (int i = 0; i < nodeList.size(); i++) { buffer.append(nodeList.get(i)); if (i < nodeList.size() - 1) { buffer.append("/"); } } return buffer.toString(); } }
看看源码就知道,这个类实际上只是用来描述xpath的,xpath中的所有节点都顺序存放,在一个List中,并复写了toString方法,将List转换为Xpath字符串,另外,Path类提供了两个重要的方法add和remove,add用于添加子节点,如果原来的xpath是/root,调用add("element1")后,path就成为/root/element1;remove方法用于删除path中的最后一个节点,与add相反。
再分析下process(node,path)代码,代码如下:
private void process(Node node, Path path) { if (node instanceof Element) { // Element String elementName = node.getNodeName(); path.add(elementName); processNodelet(node, path.toString()); processNodelet(node, new StringBuffer("//").append(elementName).toString()); // 处理节点的所有Attribute NamedNodeMap attributes = node.getAttributes(); int n = attributes.getLength(); for (int i = 0; i < n; i++) { Node att = attributes.item(i); String attrName = att.getNodeName(); path.add("@" + attrName); processNodelet(att, path.toString()); processNodelet(node, new StringBuffer("//@").append(attrName).toString()); path.remove(); } // 递归遍历处理所有node的Children NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { process(children.item(i), path); } //node以及其子节点处理结束,调用node的end()处理器 path.add("end()"); processNodelet(node, path.toString()); path.remove(); path.remove(); } else if (node instanceof Text) { // 如果是Text path.add("text()"); processNodelet(node, path.toString()); processNodelet(node, "//text()"); path.remove(); } }
代码中加了些简单注释,仔细看看就明白,process(Node node,Path path)方法递归遍历了node、node的所有属性和node的所有子节点,并调用processNodelet,这也印证了前面的推测的正确性(请看前面对processNodelet方法的分析)。
根据上面的分析,我们知道知道xml中每个Node的信息的处理方法(前面提到过Node信息处理抽象为接口Nodelet)都以Node的xpath为key存放在NodeletParser类的letMap中,那么我们如何为每个Node注册处理器(Nodelet对象)呢?
让我们回到SqlMapConfigParser的代码看看,首先看看SqlMapConfigParser的构造方法:
public SqlMapConfigParser() { parser.setValidation(true); //设置DTD文件的classpath映射 parser.setEntityResolver(new SqlMapClasspathEntityResolver()); //注册Node处理器 addSqlMapConfigNodelets(); addGlobalPropNodelets(); addSettingsNodelets(); addTypeAliasNodelets(); addTypeHandlerNodelets(); addTransactionManagerNodelets(); addSqlMapNodelets(); addResultObjectFactoryNodelets(); }
可以看到上面的构造方法中一大半的代码是addXXX形式,这个就是给xml文档的Node注册处理器(Nodelet对象),
我们随便看一个addXXX方法,看里面是怎么注册Node处理器的,就看SqlMapConfigParser的addTypeAliasNodelets()方法吧:
/** * 注册typeAlias处理器 */ private void addTypeAliasNodelets() { parser.addNodelet("/sqlMapConfig/typeAlias", new Nodelet() { public void process(Node node) throws Exception { Properties prop = NodeletUtils.parseAttributes(node, state.getGlobalProps()); String alias = prop.getProperty("alias"); String type = prop.getProperty("type"); state.getConfig().getTypeHandlerFactory().putTypeAlias(alias, type); } }); }
这里调用了类SqlMapConfigParser的parser属性(这里的parser属性就是前面说的NodeletParser的一个实例)的addNodelet方法,原来是调用这个方法给xml的节点注册处理器的。NodeletParser的方法addNodelet的第一个参数是一个xpath,用于表示xml中的Node;第二个参数是节点处理器(Nodelet对象),用于处理xpath指定的xml节点,这里的Nodelet是使用匿名内部类实现的;我们看看NodeletParser的addNodelet方法的代码:
public void addNodelet(String xpath, Nodelet nodelet) { letMap.put(xpath, nodelet); }
Nodelet对象就是在letMap中映射的!
再看看这里对/sqlMapConfig/typeAlias是怎么处理的:
- 首先通过NodeletUtils的parseAttribute方法计算出/sqlMapConfig/typeAlias节点的所有属性值,以Properties对象返回,属性名为key,属性值为value,这里会把属性值中带有${}这样的表达式值计算出来。
- 从返回的属性值的Properties对象中获取/sqlMapConfig/typeAlias节点的alias和type属性。
- 将提取的typeAlias信息存入TypeHanderFactory的别名映射表中。
其他Node的处理器以同样的方式注册到NodeletParser对象中。往NodeletParser对象注册了所有需要的 Nodelet处理器之后,调用NodeletParser的parser方法就可以将xml解析出来,具体每个节点是怎么处理的是有我们自己指定的,NodeletParser只是定义了如何遍历xml中所有节点的方法。
NodeletParser实际上应用了模板方法模式的思想,在NodeletParser中定义了如何遍历xml中所有节点的方法,但是没有定义节点是如何处理的,而是通过使用指定Node的Nodelet处理器,当遍历节点时就去调用对应的Nodelet的process方法,从而达到代码的复用,做到与具体xml文件无关。
com.ibatis.sqlmap.engine.builder.xml包下还有个类SqlMapParser,用于对SqlMap文件的解析,解析的方式和SqlMapConfigParser一样,也是通过NodeletParser,向NodeletParser对象注册Nodelet处理器实现对SqlMap文件的解析。
下面的类图是iBatis解析SqlMapConfig文件和SqlMap文件的几个最核心的几个类之间的关系:
iBatis的xml解析模块基本已经很明了,认真分析完这些代码后,我第一次感受到看别人设计优良的代码的乐趣,后期计划继续读完iBatis的源代码,当然也会写下我的所感所悟~