程序人生

一头犁牛半块田,收也凭天,荒也凭天;粗茶淡饭饱三餐,早也香甜,晚也香甜;布衣得暖胜丝棉,长也可穿,短也可穿;草舍茅屋有几间,行也安然,住也安然;雨过天晴驾一船,鱼在一边,酒在一边;夜归儿女话灯前,今也有言,古也有言;日上三竿我独眠,谁是神仙,我是神仙

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
http://www-900.ibm.com/developerWorks/cn/xml/x-dochan.shtml

有时候 SAX 文档处理程序的代码可能变得非常麻烦、结构性差而且难以维护,尤其是对于那些包含多种不同元素的复杂 XML 结构。本文介绍了应对这种问题的设计策略,从而有助于改进代码的质量和可维护性。

要将 SAX 接口用于 XML 文件,需要编写一个文档处理程序类,在处理程序类的方法中为不同的 XML 标签指定处理方式:

  • startElement
  • endElement
  • characters

这意味着每当 XML 文件中增加了新的元素类型,都必须修改这些方法增加新元素的处理方式。如果原有的元素需要某种修改或者调整,也必须修改这些方法。文档处理程序类的大小和复杂性随着不同 XML 标签数量的增加而增加,直到某个时候可读性和维护性都变得非常差。

本文说明如何通过把关于每种 XML 标签的所有代码隔离在单独的类中来改进设计结构。通过使用 Java 反射机制来泛化文档处理程序类,并使用一个抽象类来实现元素类。

注意: 本篇技巧使用的是 Xerces-Java 2,但这些概念适用于任何 SAX 兼容的解析器。

设计策略
设计策略的第一步是编写一个通用的文档处理程序。只要在解析的 XML 文档中遇到一个标签,该处理程序就为相应的 XML 元素调用特定的类。按照这种策略,该元素类是一个外部文件,实现了和所处理元素有关的方法(比如,startElementendElementcharacters)。为了实现 SAX 解析器对外部类的动态调用,可以使用 Java 反射机制,即 Class.forName 方法,如清单 1 中所示。

清单 1. SAX XML 通用文档处理程序

import org.xml.sax.*;
import org.xml.sax.helpers.*;
  
public class SaxParseSample extends DefaultHandler {

String lastName;
  
 static XMLReader parser;  

 public static void main (String args[]) throws Exception
  {

  /**
    * Create a parser.
    */
	try 
	{   
	   parser = XMLReaderFactory.createXMLReader();
	}
	catch (Exception e) 
	{   
	   parser = null;   
	   System.err.println("error: Unable to instantiate parser("+parser+")");
	}
	SaxParseSample handler = new SaxParseSample();
	parser.setContentHandler(handler);
	parser.setErrorHandler(handler);
	parser.parse(new InputSource(args[0]));
  }



  /**
    * Handle the start of an element.
    */
  public void startElement (String uri, String name,String qName, Attributes atts)
  {
    lastName = new String(name); 
   	try 
  	{
	   Class classToRun = Class.forName(name, true, 
	           ClassLoader.getSystemClassLoader()); 
	   XmlElementsInterface etlElement =  
	           (XmlElementsInterface) classToRun.newInstance(); 
	   etlElement.startXmlElement(uri, name, qName, atts);
  	}
  	catch (Exception e) 
  	{  
	   System.out.println( e );
  	};
 
  }

 
  /**
    * Handle the end of an element.
    */
  public void endElement (String uri, String name, String qName)
  {
  	try 
  	{
	   Class classToRun = Class.forName(name, true, 
	           ClassLoader.getSystemClassLoader());
	   XmlElementsInterface etlElement =  
	           (XmlElementsInterface) classToRun.newInstance();
	   etlElement.endXmlElement(uri, name, qName);
  	}
  	catch (Exception e) 
  	{
	   System.out.println( e );
  	};

  }


  /**
    * Handle character data.
    */

  public void characters (char ch[], int start, int length)
  {
   	try 
  	{
	   Class classToRun = Class.forName(lastName, true, 
	           ClassLoader.getSystemClassLoader());
	   XmlElementsInterface etlElement =  
	           (XmlElementsInterface) classToRun.newInstance();
	   etlElement.XmlCharacters(lastName, ch , start, length);
  	}
  	catch (Exception e) 
  	{
	   System.out.println( e );
  	}
  }
}

从清单 1 中可以看出,这里没有明确引用任何 XML 标签。而是通过加载和运行一个类来实现元素的处理,这个类的名称包含在从 SAX 解析器接收到的 name 参数中。程序员负责保证这个类的存在,以免出现 ClassLoadingException 异常。

这样就可以实现要加载的类名的动态解释。但是获得这种灵活性也在时间上付出了小小的代价(参见本文后面关于性能的讨论)。

此外,本例中使用元素的本地名标识类名。这意味着如果使用多个存在冲突元素的名称空间可能无法正常工作。这种情况下必须使用某种扩展命名机制。比如,类名可以定义成 namespace_localname 或者等价的表示。

如果分析运行期间执行的动作序列——比方说一个 XML 元素的开始——就需要:

  1. 解析器读入元素开始的 XML 标签并调用文档处理程序的 startElement 方法。
  2. startElement 方法加载并运行本地名对应所找到的标签的类。
  3. 这个外部类负责每当新的 XML 元素开始时所要执行的动作。

同样的逻辑也适用于 XML 元素的结束和元素内容的处理。

为了保证这种机制能够工作,外部类必须实现 XmlElementsInterface 接口,该接口也用于强制转换所加载的类。清单 2 说明了 XmlElementsInterface 类:

清单 2. XmlElementsInterface 接口

public interface XmlElementsInterface {
	
	public void startXmlElement (String uri, String name, String qName, 
	        org.xml.sax.Attributes atts) ;
	public void endXmlElement (String uri, String name, String qName) ; 
	public void XmlCharacters (String lastName , char ch[], int start, int length);  
	
}

在清单 2 中只包含了 startXmlElementendXmlElementXMLCharacters 方法,不过也可以增加其他的方法。

有了清单 2 所示的接口类,现在就可以对所有的 XML 元素实现外部类。比如,对于清单 3 中的 XML 片段,需要创建三个外部类,Commands.classComment.classSyscommand.class,以便对应其中的三个 XML 元素:

清单 3. XML 片段

<Commands>
<Comment>The dir command displays a list of the files in a directory</Comment>
<Syscommand>DIR >> c:\\directory.log</Syscommand>
<Comment>The write command is used to edit the content of a file</Comment>
<Syscommand>write.exe c:\\directory.log</Syscommand>
</Commands>
	

每个类的最小实现可以是:

清单 4. 外部 XML 元素类的最小实现

public class Syscommand implements XmlElementsInterface {

	public void startXmlElement (String uri, String name, String qName,
	         org.xml.sax.Attributes atts) 
   		{
   		  	System.out.println(  "Start element " + qName); 		
   		};
     
   	public void endXmlElement (String uri, String name, String qName)  
   		{
   		  	System.out.println(  "End element " + qName);
   		};
   		
   		
   	public void XmlCharacters (String lastName , char ch[], int start, int length)  
   		{
   		  	System.out.println(  "Content of element " + lastName);
   		};

  		
}
	

其他元素类 Commands.classComment.class 的定义与此类似。

按照这些定义,XML 片段解析在控制台上的输出如下:

清单 5. 默认方法创建的控制台日志

Start element Commands
Content of element Commands

Start element Comment
Content of element Comment
End element Comment

Start element Syscommand
Content of element Syscommand
End element Syscommand

Start element Comment
Content of element Comment
End element Comment

Start element Syscommand
Content of element Syscommand
End element Syscommand

End element Commands
	

现在可以改变一个元素的行为,而不必修改文档处理程序代码或者其他元素的代码。所有的修改都局限在单个类中,与其他类互不影响。比如,您可以修改 Syscommand 让它执行所传递的命令。这样,Syscommand 类就应该变成:

清单 6. 增强 Syscommand.java

public class Syscommand implements XmlElementsInterface {

   public void XmlCharacters (String lastName , char ch[], int start, int length) 	
   {
	String COMMAND   = new String(ch,start,length);
	System.out.println(  "Executing command: " + COMMAND);
	try
	{  
	   Runtime rt = Runtime.getRuntime();
	   String[] callAndArgs = { "cmd.exe" ,"/C", COMMAND };
	   try 
	   {
                Process child = rt.exec(callAndArgs);
                int rc = child.waitFor();
                System.out.println("Process exit code is: " + rc);
	   }
	   catch(Exception e) 
	   { 
                System.err.println("Exception " + e ); 
	   }
	}
	catch(Exception e)
	{ 
	   System.err.println("Exception " + e );
	}
   } 		
// . . .
// The other methods remain unchanged
// . . . 
}

Syscommand 类中的代码更新时,这个系统的行为就发生了变化。SAX 解析器、文档处理程序以及用于其他元素的外部类都不需要作任何修改。

这种方法可以极大提高代码的质量,特别是如果 XML 文件很复杂,或者要处理许多其他的 XML 元素。

改进这种机制
为了充分利用上述基本方法,需要解决两方面的局限性。首先,如果增加了新的 XML 元素或者拼写 XML 标签时出现错误怎么办?在这种基本方法中,将引发一个 ClassNotFound 异常。为了避免这种情况,或者至少提供意义明确的调试信息,与 XML Schema 检查(或者 DTD 检查)结合使用是一种不错的方法。使用的元素必须首先在模式中定义,解析器检查该模式就可以发现与解析文件不一致的地方。模式检查的语法可能随着不同的 SAX 解析器略有不同。比如,在 Xerces-Java 2 实现中,就需要在 SaxParseSample 类(如清单 1 所示)的 main 方法中加上下面这一行:

清单 7. 在 main 方法中增加 XML Schema 检查

	...
      parser.setFeature("http://xml.org/sax/features/validation",true);
      parser.setFeature("http://apache.org/xml/features/validation/schema",true);
	...
	

第二方面的局限是只有在不需要将数据从一个元素传递到另一个元素时才有效,因为每次解析一个元素时,都会实例化类的一个新副本,不会保留在内存中。该例中的 XML 片段包含一系列要执行的命令,每一个命令都独立于前一个。但是,如果命令的执行以前一个命令的成功退出为条件时该怎么办呢?按照上述的基本方法,这是不可能的,因为当一个元素被处理时它完全不知道上一个元素。

因此,对于更复杂的情况必须修改上面的模型,以便把参数从一个元素传递到另一个元素。

程序之间传递信息有多种方法。比如,可以在程序中增加对 Java Naming and Directory Interface(命名和目录接口,JNDI)目录服务的应用。

使用 JNDI 目录服务
目录服务提供了在分布式环境中存储和检索信息的一种方法,比如序列化对象。JNDI 是多种目录服务实现的标准接口,定义在 javax.naming.directory 包中,要使用 JNDI 必须将该包导入程序。

清单 8 引用了 Lightweight Directory Access Protocol(轻型目录访问协议,LDAP)实现。并假设该实现所需要的环境信息已经保存在 env 散列表中。

按照上述假设,您必须在本文所定义的元素类中增加以下代码,以便在它们之间互相交换 commandResult 字符串:

清单 8. 共享对象所需的修改

try { 	    
    DirContext dirCtx = new InitialDirContext(env);

    String commandResult = new String("Successful");
    dirCtx.bind("cn=result" , commandResult);
	
} catch (NamingException e) {
    System.out.println("Operation failed: " + e);
}
 	    

下面的代码可以从另一个元素类中读取共享的对象:

清单 9. 读取共享对象所需要的修改

try { 	    
    DirContext dirCtx = new InitialDirContext(env);

    String previousResult = (String) dirCtx.get("cn=result" , commandResult);
	
} catch (NamingException e) {
    System.out.println("Operation failed: " + e);
} 	    

性能问题
有时候设计策略中所用的 Java 反射机制可能对应用程序的性能产生负面影响,因此一定要考虑到这样是否会降低性能。对于简单的应用程序,上述机制没有很大的影响。比如根据测试,在 600 MHz 的处理器上,包含 10,000 个元素的 XML 文件只多花了不到半秒钟的时间。事实上,多花的执行时间是和 XML 文件中的元素数成比例的。因此,只有在非常复杂的大量使用循环和递归调用的应用程序中,才需要考虑这种潜在的影响。

结束语
本文展示了在需要处理已解析 XML 文件中不同元素的文档处理程序代码中,如何通过为每种元素创建单独的类来简化编码。通过这种方式可以创建更小的类和方法,使代码更容易测试、调试和修改,从而改进整体质量和生产率。

可以使用 Java 反射方法 Class.forNameclassToRun.newInstance 创建通用的解析代码,作为调用所定义元素类的接口。XML 文件中定义的所有元素的类都必须创建,并且实现接口 XmlElementsInterface

此外,在这个例子中,我还说明了如何在通用解析代码 SaxParseSample 中增加 XML Schema 检查,以捕获新的或者错误的 XML 元素,避免产生 ClassNotFound 异常。另外还介绍了如何使用 JNDI 目录服务在不同的代码片中共享变量或对象。

posted on 2004-09-06 09:30  啸天犬  阅读(451)  评论(0编辑  收藏  举报