与大多数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的源代码,当然也会写下我的所感所悟~