iBatis2源码分析(一)——xml解析模块

       与大多数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是怎么处理的:

  1. 首先通过NodeletUtils的parseAttribute方法计算出/sqlMapConfig/typeAlias节点的所有属性值,以Properties对象返回,属性名为key,属性值为value,这里会把属性值中带有${}这样的表达式值计算出来。
  2. 从返回的属性值的Properties对象中获取/sqlMapConfig/typeAlias节点的alias和type属性。
  3. 将提取的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文件的几个最核心的几个类之间的关系:


iBatis2源码分析(一)——xml解析模块

 

      iBatis的xml解析模块基本已经很明了,认真分析完这些代码后,我第一次感受到看别人设计优良的代码的乐趣,后期计划继续读完iBatis的源代码,当然也会写下我的所感所悟~

 

 

 

 

你可能感兴趣的:(设计模式,框架,xml,ibatis,orm)