在JAXP中透明的缓存XSL转换器
使用内置在Transformer Factory中的缓存功能在保持易用性的同时提高性能
摘要
当在具有众多并发进程重复使用XSLT(Extensible Stylesheet Language Transformations)的Web环境中时,实现一个样式表缓存可以从根本上提高Web应用程序的性能。可是对于纯粹的JAXP用户来说,在大多数JAXP (Java API for XML Parsing) 程序中使用样式表缓存通常并不方便。本文章将介绍如何实现增加缓存功能至transformer factory,并可以完全透明的使用缓存。(2003年5月2日)
作者:Alexey Valikov
不要怀疑,XSLT的确是一个强大的技术,人们使用它构建了非常多的XML应用。具体来说,众多的Web开发者们可以利用XSLT在呈现层(Presentation Layer)的优势增加Web应用的易用性和伸缩性。然而这个优势的代价是更多的内存和更高的CPU负载,这个问题使开发者们在使用XSLT时更关注优化和缓存技术。当在一个有众多并发进程共享样式表的Web环境中时缓存尤为重要。
在这些情况中,恰当的transformation缓存被证实对于提高性能有极大的帮助。最常见的建议是在使用JAXP时的将transformations加载入一个Templates对象,并且使用这个对象来创建Transformer对象,这比直接从工厂中实例化Transformer对象具有更好的效果。那样一个Templates对象可以在稍后被重用于创建更多的transformers,这样就节约了每一次用在样式表解析和编译的时间。在“Top Ten Java and XSLT Tips”中,Eric Burke在Tip1中列出了下列代码
Source xsltSource = new StreamSource(xsltFile);
TransformerFactory transFact = TransformerFactory.newInstance();
Templates cachedXSLT = transFact.newTemplates(xsltSource);
Transformer trans = cachedXSLT.newTransformer();
在这个示例中,xsltFile的transformation首先被载入cachedXSLT Templates对象。然后这个对象被用于创建新的transformer对象trans。这样使用的优势在于当我们需要另一个transformer对象时,解析和编译过程可能会被跳过。
Transformer anotherTrans = cachedXSLT.newTransformer();
尽管这个技巧肯定会提高性能(特别是当重复使用相同的样式表时,比如在Web应用中),坦白的讲,它对于开发者来说并不方便。原因是除基于Templates的transformer实例外,你还必须注意样式表的最后更新时间,重新载入过期(outdated)的transformations,提供安全的并且在多线程访问样式表时仍具有高效率的缓存,还很多其他的细节。甚至将需要的功能封装进一个单独的transformer缓存实现也无法帮助使用第三方模块的开发者。一个很好的例子就是JSTL x:transform标记:当前在org.apache.taglibs.standard.tag.common.xml.TransformSupport和org.apache.taglibs.standard.tag.el.xml.TransformTag类中直接用TransformerFactory的newTransformer(…)方法。很明显,x:transform无法借助外部的缓存实现来提高性能
不过还是有一个简单的、不错的方法来解决这个问题的。JAXP始终允许我们替代其Transformer Factory,那我们为什么不写一个具有caching能力的factory来解决这个问题呢?
这个主意并不难实现。我们可以扩展任何一个合适的TransformerFactory(我使用Michale Key的Saxon 7.3)并且重载父类的newTransformer(…)方法使从基于文件的StreamSources中载入的transmations可以被缓存并从缓存中读取。当然,前提是transmations在载入后没有被修改。下面是一个新版本的newTransformer(…)方法:
public Transformer newTransformer(final Source source)
throws TransformerConfigurationException
{
// Check that source in a StreamSource
if (source instanceof StreamSource)
try
{
// Create URI of the source
final URI uri = new URI(source.getSystemId());
// If URI points to a file, load transformer from the file
// (or from the cache)
if ("file".equalsIgnoreCase(uri.getScheme()))
return newTransformer(new File(uri));
}
catch (URISyntaxException urise)
{
throw new TransformerConfigurationException(urise);
}
return super.newTransformer(source);
}
像你所看到的,如果transformer的source不是一个StreamSource或者未被指向一个文件,父类的newTransformer(…)会返回transformer。如果source是一个基于文件的StreamSource,这样我们就可以使用缓存来实现更智能的transformation载入过程。
基于文件的样式表缓存算法非常简单:根据一个给定的文件,我们首先检查缓存中已经有绝对文件名相同的Templates对象。如果不存在,我们为这个文件创建并缓存一个新的Templates对象。如果已经存在于缓存,我们检查在Template载入后文件是否曾修改过,比较文件的最后修改时间和缓存中的时间。如果文件被更新过,Templates必须被再次载入,否则就从缓存中取出。最后,用Template对象(根据情况而定,可能从缓存或磁盘中载入)生成一个新的transformer。下面有一个根据这个算法实现的方法:
protected Transformer newTransformer(final File file)
throws TransformerConfigurationException
{
// Search the cache for the templates entry
TemplatesCacheEntry templatesCacheEntry = read(file.getAbsolutePath());
// If entry is found
if (templatesCacheEntry != null)
{
// Check timestamp of modification
if (templatesCacheEntry.lastModified
< templatesCacheEntry.templatesFile.lastModified())
// Clear entry, if it is obsolete
templatesCacheEntry = null;
}
// If no templatesEntry is found or this entry was obsolete
if (templatesCacheEntry == null)
{
logger.debug("Loading transformation [" + file.getAbsolutePath() + "].");
// If this file does not exists, throw the exception
if (!file.exists())
{
throw new TransformerConfigurationException(
"Requested transformation ["
+ file.getAbsolutePath()
+ "] does not exist.");
}
// Create new cache entry
templatesCacheEntry =
new TemplatesCacheEntry(newTemplates(new StreamSource(file)), file);
// Save this entry to the cache
write(file.getAbsolutePath(), templatesCacheEntry);
}
else
{
logger.debug("Using cached transformation [" + file.getAbsolutePath() + "].");
}
return templatesCacheEntry.templates.newTransformer();
}
无论如何,我们仍然必须要考虑另外一个问题:线程安全。缓存始终被很多并发进程所共享,我们必须有一个可靠的方法来使读(从缓存中获得所需内容)和写(保存一个新载入的样式表到缓存)操作安全。
虽然Java提供了先进的同步功能,但这里的问题并不是同步,而是如何平衡同步和性能。最简单的方法是全部同步(full synchronization):我们将整个newTransformer(…)方法声明为synchronized,但这种方法效率十分低下。通常我们只有有限的几个样式表,并且并不经常被修改,缓存读的次数要比写的次数要多的多。全部同步(full synchronization)会阻挡其他并发的读取者。首先,这并不是任何时候都需要。第二,这会导致瓶颈问题。
另一方面,使用未同步的容器(比如HashMap)来存储缓存内容非常危险。如果我们没有采取任何措施,那么同时发生的读取和写操作(必然会出现的问题)会导致系统不稳定。
我们主要会遇到一个典型的读/写问题:一个给定的资源,可能有一个写操作和若干个读操作在某一时间同时发生。这个典型的问题也有一个典型的解决方法,来自于Doug Lea的Concurrent Programming in Java。这个方法是,跟踪执行状态(根据计算活动中和等待中的读/写线程的数量确定),只允许在没有任何活动中的写操作线程时才允许读操作。同样,只有在没有活动中的读操作线程时才允许写操作。
为实现以上内容,我们将访问缓存的内容写在两个方法中,read()和write():
两对berfore/after,read/write方法执行需要线程同步的步骤,确保安全,同时保证了访问缓存的效率。
protected synchronized void beforeRead()
{
while (activeWriters > 0)
try
{
wait();
}
catch (InterruptedException iex)
{
}
++activeReaders;
}
protected synchronized void afterRead()
{
--activeReaders;
notifyAll();
}
protected synchronized void beforeWrite()
{
while (activeReaders > 0 || activeWriters > 0)
try
{
wait();
}
catch (InterruptedException iex)
{
}
++activeWriters;
}
protected synchronized void afterWrite()
{
--activeWriters;
notifyAll();
}
了解了以上的代码后,我们最终获得了一个可以透明的实现对基于文件的样式表的缓存功能的transformer factory(你可以在资源处下载全部源代码)。这里只展示了使factory完全适用于标准JAXP程序的部分。
有一些方法可以使TransformerFactory.newInstance()方法返回一个定制的transformer factory实现的实例。其中最简单易懂的方法就是在system property中的javax.xml.transform.TransformerFactory中指定factory的类名。
这个办法的优点就是具有最高的优先级,缺点就是必须手工操作。
另一个方法是是使用一个JRE(Java Runtime Environment)级的配置文件${JRE_HOME}/lib/jaxp.properties来指定你自己的类名
...
# Specifies transformer factory implementation
javax.xml.transform.TransformerFactory=de.fzi.dbs.transform.CachingTransformerFactory
...
最后一个方法是通过Services API在原信息(meta-information)中提供transformer factory名。只需要在META-INF/services目录中创建一个叫做javax.xml.transform.TransformerFactory的文件。这个文件的内容应该是一个指定了定制的transformer factory类名的单行字符串。这种方法有一个问题:另一个JAR可能也尝试通过Services API设置factory类。例如,如果你把你的JAR和Saxon’s JAR放入你的Web应用的WEB-INF/lib目录,实际上JAXP使用的factory将会根据JAR文件的载入顺序确定。避免这种问题的方法是简单的将factory设置在WEB-INF/classes/META-INF/services/javax.xml.transform.TransformerFactory文件中。在本文情况中,该文件需要包含一个单行字符串de.fzi.dbs.transform.CachingTransformerFactory。
使缓存可以透明的使用
现在,当一切都完成后。你没那么头痛了。你不再需要担心载入、缓存和重新载入样式表。你可以确保使用标准JAXP的第三方库可以很好的使用缓存功能。你可以肯定不会再有并发冲突,缓存也不会是瓶颈。
可是,使用这个实现仍然有一些缺点。首先,这个factory只缓存基于文件的样式表。原因是我们可以很容易的查看文件的最后修改的日期,但其他资源并不都能实现这个功能。另一个问题是样式表import和include其他样式表。修改了被import和include的样式表并不会使主样式表重新载入。最后,扩展一个现有的factory实现将你绑在了一个特定的XSLT Processor(除非你为每个一个你可能使用的factory都写一个缓存扩展)。不过,值得高兴的一点是,大部分情况下,这些问题比起我们从基于factory的缓存中获得的:使用透明、方便、高性能来说无关紧要。
关于作者
Alexey Valikov是一个具有广泛编程背景的电脑科学家,特别是Java和XML技术。他现在在FZI(电脑科学研究中心,Karlsruhe/Germany)研究“Web应用中的效率问题”。他工作于FZI的XML Competence Center,他同时担任XML技术顾问,并且参与了European Commission research projects。Alexey是一本很受欢迎的XSLT应用指南“The Technology of XSLT”的作者,该书出版于俄罗斯。
资源
下载本文的源代码:
http://www.javaworld.com/javaworld/jw-05-2003/xsl/jw-0502-xsl.zip
"Top Ten Java and XSLT Tips," Eric M. Burke (java.oreilly.com, August 2001):
http://java.oreilly.com/news/javaxslt_0801.html
Concurrent Programming in Java, Second Edtion: Design Principles and Patterns, Doug Lea (Addison-Wesley Pub Co., 1999; ISBN: 0201310090):
http://www.amazon.com/exec/obidos/ASIN/0201310090/javaworld
Concurrent Programming in Java online supplement:
http://gee.cs.oswego.edu/dl/cpj/
Saxon, an XSLT processor written by Michael Kay:
JAXP documentation:
http://java.sun.com/xml/jaxp/index.html
Jar file specification, service provider documentation:
http://java.sun.com/j2se/1.4/docs/guide/jar/jar.html#Service%20Provider
log4J, Java logging package (used in proposed factory implementation):
http://jakarta.apache.org/log4j
Apache Ant, Java-based build tool (you may use it to build the proposed factory implementation):
More JavaWorld stories on XSLT:
"Generate JavaBean Classes Dynamically with XSLT," Victor Okunev (February 2002)
"Boost Struts with XSLT and XML," Julien Mercay and Gilbert Bouzeid (February 2002)
"XSLT Blooms with Java," Taylor Cowan (December 2001)
Browse the Java and XML section of JavaWorld's Topical Index:
http://www.javaworld.com/channel_content/jw-xml-index.shtml
Talk more about XSLT in our XML & Java discussion:
http://forums.devworld.com/webx?50@@.ee6b78f
Sign up for JavaWorld's free weekly email Enterprise Java newsletter:
http://www.javaworld.com/subscribe