XML 和 JSP 是当今最流行的话题。 这篇文章引导你如何运用这两种技术去创建动态Web站点。用XML文件去储存数据,用JSP文件去显示它。同时还可以了解DOM, XPath, XSL等其它 Java-XML技术。。
作者 Alex Chaffee
我先假设读者与其他大多数Java 程序员一样,对JSP(JavaServer Pages )和XML(Extensible Markup Language)有一定的了解,但是不清楚如何运用它们。在这篇文章中,您将学习如何用XML设计一个系统。许多网站都有大量的格式各异的数据,它们的表现方式或多或少的都没有遵循一定的标准。而我在此推荐用这样一种方法来设计网站,即用XML文件保存数据,用JSP文件显示这些数据。
想必大家都已经知道了HTML的局限性了把。随着站点的发展,需要一种方法来共享和交换数据。不管是内容出售,订单处理,还是报表生成,都需要对数据进行定义。XML正好可以发挥作用,其他的应用只能阅读和翻译信息,而XML可以赋予数据以意义。也许你会觉得奇怪,为什么要用XML来储存数据,而不用数据库呢?答案是在很多应用方面,数据库显的太“猛”了一点。为了要使用数据库,你必须要安装和支持一个独立的服务器处理,安装DBMS,建立DBA,并且还要去学习SQL语言,学会如何写查询语句并把结果返回。但是如果你使用XML文件,你不必去为准备额外的数据库Server而花费,且好处是可以很简单的编辑处理数据,就象是用一个文本编辑器一样,而不是复杂的数据库管理工具。XML文件也十分容易备份,共享和下载,以及通过FTP上传新数据至网站。
XML的另一个稍稍抽象的优点是采用了层次结构而不是关系结构来定义数据,可以根据需要直接了当的设计应用的数据结构,也不必使用实体关系设计器去进行模式的规范化操作。如果您有一个成员包含另一个成员,您可以通过层次结构直接表示出来,而不用使用连接表。从这种意义上说XML有利于信息的表达和结构化组织,可以准确定义数据,从而使数据搜索更有效。
对于许多应用来说,文件系统是没有足够的能力满足需要。当有大量的数据需要更新时,它的弱点就暴露了。并发写操作的冲突等是大问题。数据库有良好的事务处理机能,丰富的索引与复杂查询功能,完全可以为数据库提供一个包装器,以便将创建查询并将它们转化成XML流。这样XML就变成了一个强大的且编程友好的数据库前端部件(Oracles XSQL servlet是一个实现的例子)。
用XML来定义数据 :在线相册的例子
每个人都喜欢照片。互联网是个工具可以展示自己,朋友,充物及各种活动。这个例子以定义一个简单照片对象为主(源代码可以从参考资料处获得)。展示了定义一个照片对象所需要的属性有:标题,日期,简短说明以及图像的位置。
定义一幅图像需要的属性:
图像文件(GIF or JPEG)的位置
图像的高度(像素)
图像的宽度(像素)
在这里把文件系统当作数据库来存储信息有一个简洁的优点,即图像文件和数据描叙文件可以保存在同一位置下。
最后用一个元素来定义小照片图像(thumbnail images)集来扩充相片记录的定义,以便在任何地方使用。
XML来定义照片的例子如下:
<picture>
<title>Alex On The Beach</title>
<date>1999-08-08</date>
<caption>Trying in vain to get a tan</caption>
<image>
<src>alex-beach.jpg</src>
<width>340</width>
<height>200</height>
</image>
<thumbnails>
<image>
<src>alex-beach-sm.jpg</src>
<width>72</width>
<height>72</height>
</image>
<image>
<src>alex-beach-med.jpg</src>
<width>150</width>
<height>99</height>
</image>
</thumbnails>
</picture>
使用XML,可以把所有的关于一幅照片的信息放入一个简单的文件,而不放在分散的表里。这个文件称为pix文件。这样文件系统就如下面的样子所示。
summer99/alex-beach.pix
summer99/alex-beach.jpg
summer99/alex-beach-sm.jpg
summer99/alex-beach-med.jpg
summer99/alex-snorkeling.pix
etc.
有好几种办法可以把XML数据与JSP页面结合起来。下面列出了几种办法:
DOM: 用类实现DOM接口来分析与检视XML文件
XMLEntryList: 把XML加载进 java.util.List 的name-value对
XPath: 用 XPath 处理器 (就象Resin一样) 通过路径名来定位XML中的元素
XSL: 用 XPath 处理器来转化XML为HTML
Cocoon: 使用开放代码的Cocoon框架
Roll your own bean: 可以写一个包装类加载数据进入一个的定制JavaBean
这些方法能很好地等同运用在客户端或应用程序服务器端接受XML流时。
Java Server Pages
JSP有很多实现版本, 不同的JSP版本实现了不同的且互不兼容的规格.作者在这里选用的是Tomcat, 基于如下的几点理由:
它支持最新的JSP 和 Servlet 规格
Sun和Apache认可它
你可以单独运行它而不用单独配置一个Web服务器
它的源码开放
(关于Tomcat的更多信息, 请看参考资料.)
你可以随便选用你喜欢的JSP引擎,但需要你自己配置它。一定要确认所选用的引擎支持JSP1.0规格。从0.91到1.0的版本有很多地方不一样。只要JSWDK (Java Server Web Development Kit) 能正常工作就可以了。
JSP 构造
当构造一个基于JSP驱动的WEB站点时,建议将公用函数,导入模块,常量和变量定义放在一个独立的名为init.jsp的文件里。然后用<%@include file="init.jsp"%>的方式把它添进每个JSP文件中。<%@include%>指示的作用类似于C语言的#include定义符,将被包含文件(例如init.jsp)作为包含文件(例如picture.jsp)的内容一起进行编译。注意,作为比较, <jsp:include>标志指示编译器将它作为一个独立的JSP文件编译,并在被编译好的JSP中嵌入对它的调用。
查找文件
当启动JSP时,在初始化工作结束之后的第一件事是查找所需要的XML文件。怎么知道需要哪些文件呢?答案是以CGI程序方式来传递所需要的参数。用户可以通过URL picture.jsp?file=summer99/alex-beach.pix的方式或用HTML的form对象来传递文件参数。
当JSP收到参数之后,你还得知道 filesystem的根目录位于什么地方。在UNIX系统之下,文件位置是以/home/alex/public_html/pictures/summer99/alex-beach.pix方式来指定的。JSP在执行时是没有相对路径的概念的,所以必须在java.io包内提供绝对路径。
根据所采用JSP 或 Servlet的版本,Servlet APT函数提供了转换URL路径为绝对路径的方法。ServletContext.getRealPath(String)是实现的窍门。每个JSP版本都有个 ServletContext对象(称为application)。因此代码可以象下面的样子:
String picturefile =
application.getRealPath("/" + request.getParameter("file"));
or
String picturefile =
getServletContext().getRealPath("/" + request.getParameter("file"));
它们在servlet中也能工作。注意必须增加"/",因为request.getPathInfo()的结果将被传递给这个方法。
另外重要的是,当访问本地资源时,必须特别小心所传过来的数据,黑客或粗心的用户可能发送伪造的数据来黑了你的站点。例如用file=../../../../etc/passwd时用户是可以看服务器的口令的。
文档对象模型
DOM代表Document Object Model,它是一组用来浏览XML文档的标准API。由World Wide Web Consortium (W3C)所制定。其接口位于包org.w3c.dom内,其文档位于W3C站点(参考资料)。
有许多DOM解析器的实现。因为DOM是一个接口集合而不是类的定义,每一种解析器都必须返回忠实地实现这些接口的对象。我选用了IBM所实现的XML4J。
尽管接口集是标准的,但是DOM有两个主要的缺陷:
1. API函数,尽管是面向对象的实现,但还是相当烦琐
2. 没有为DOM解析器设计标准的API,虽然每一种解析器都返回org.w3c.dom.Document对象,但是解析器的初始化以及文件的载入一直都是各种解析器所独有的。
上面所描述的相片文件,如果用DOM来表达,则可用树结构中的几个对象来描述:
Document Node
--> Element Node "picture"
--> Text Node " " (whitespace)
--> Element Node "title"
--> Text Node "Alex On The Beach"
--> Element Node "date"
--> ... etc.
为了获得值“Alex On The Beach”,必须调用几个方法,遍历DOM树。进一步的说,解析器可以选择为去访问任意数量的空白结点。解析器也可以包括区别XML实体(例如 &),CDATA结点,或其他元素结点(例如:<b>big<b> bear可以至少被转化成3个结点。其中之一是元素b,包含一个 文本结点,文本值为big)。在DOM中是没有办法简单的说一句:"get me the text value of the title element."就可以了的。简言之,遍历DOM树不怎么愉快。
从另外一个较高的视角来看,DOM问题就是访问XML对象不能象访问JAVA对象那样直接。对它们的访问必须通过DOM API来分解。
请参照我在 Java-XML 数据绑定技术讨论中所下的结论,在那里采用了直接访问Java对象的方法来访问XML数据。
我写过一个小小的工具类DOMUtils,包含一些静态方法来实现普通DOM任务。例如获得根结点 (picture)的子结点(title)的内容,可以写如下代码:
Document doc = DOMUtils.xml4jParse(picturefile);
Element nodeRoot = doc.getDocumentElement();
Node nodeTitle = DOMUtils.getChild(nodeRoot, "title");
String title = (nodeTitle == null) ? null : DOMUtils.getTextValue(nodeTitle);
获得image子元素的值的办法,也很简洁:
Document doc = DOMUtils.xml4jParse(picturefile);
Element nodeRoot = doc.getDocumentElement();
Node nodeTitle = DOMUtils.getChild(nodeRoot, "title");
String title = (nodeTitle == null) ? null : DOMUtils.getTextValue(nodeTitle);
等等!
一旦为相关元素建立了Java变量,就必须用JSP标志将它们嵌入HTML中。
<table bgcolor="#FFFFFF" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="center" valign="center">
<img src="<%=src%>" width="<%=width%>" height="<%=height%>" border="0" alt="<%=src%>"></td>
</tr>
</table>
完整的代码如下。HTML的输出使用的是JSP文件。
<%
// invoke this file like: picture-dom.jsp?file=foo.pix
%>
<%@include file="init.jsp"%>
<%
String picturefile =
application.getRealPath("/" + request.getParameter("file"));
System.out.println(picturefile);
Document doc = DOMUtils.xml4jParse(picturefile);
Element nodeRoot = doc.getDocumentElement();
Node nodeTitle = DOMUtils.getChild(nodeRoot, "title");
String title = (nodeTitle == null) ? null : DOMUtils.getTextValue(nodeTitle);
Node nodeCaption = DOMUtils.getChild(nodeRoot, "caption");
String caption = (nodeCaption == null) ? null : DOMUtils.getTextValue(nodeCaption);
Node nodeDate = DOMUtils.getChild(nodeRoot, "date");
String date = (nodeDate == null) ? null : DOMUtils.getTextValue(nodeDate);
Node nodeImage = DOMUtils.getChild(nodeRoot, "image");
Node nodeSrc = DOMUtils.getChild(nodeImage, "src");
String src = DOMUtils.getTextValue(nodeSrc);
Node nodeWidth = DOMUtils.getChild(nodeImage, "width");
String width = DOMUtils.getTextValue(nodeWidth);
Node nodeHeight = DOMUtils.getChild(nodeImage, "height");
String height = DOMUtils.getTextValue(nodeHeight);
%>
<html>
<head>
<title><%=title%></title>
</head>
<body>
<center>
<table bgcolor="#CCCCCC" border="0" cellspacing="0" cellpadding="8">
<tr>
<td align="left" bgcolor="#6666AA" width="100%">
<b><%=title%></b>
</td></tr>
<tr>
<td align="center" valign="top">
<table bgcolor="#FFFFFF" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="center" valign="center">
<a href="picture-sm-dom.jsp"><img src="<%=src%>" width="<%=width%>" height="<%=height%>" border="0" alt="<%=src%>"></a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" valign="center">
<% if (caption != null) { %>
<b><%=caption%></b><br/>
<% } %>
<% if (date != null) { %>
<font size="-1"><i><%=date%></i></font><br/>
<% } %>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
使用JSP beans 来分开模型和视图
上面的代码picture-dom.jsp并不吸引人。当你要在JSP内放置大量的Java代码时,有一个简洁的办法:可以使用JSP JavaBeans来存储大量的Java代码。好处是在JSP页面内可节省JSP 脚本 tags (<% and %>) ,以便于控制程序流与减少变量的操作。为了原型开发的目的,把所有的Java代码放入JSP内可能开始觉得轻松愉快。也可以在有了好主意后,回头将必要代码抽出放入JavaBeans。这样做的代价有点高,但是会在今后得到回报,如果你的应用需要模块化的话。可以在不同的页面内使用同一Beans,而不用做讨厌的copy-and-paste,以支持代码重用。
在以上的例子中,从XML文件中抽出字串的值可以作成一个JSP JavaBean。定义 Picture,Image,Thumbnails类来表达XML中的主要元素。这些Beans将有构造函数或setter方法,调用DOM结点或文件名来从中抽出需要的值。从参考资料或picture-beans.jsp中可以找到 picturebeans包的源码。
阅读源码时请注意:
在实现类以外单独定义了接口,因此今后可以自由地选择改变实现。可以在列表中,在DOM里甚至在database里存储数据。
beans被定义在一个定制包 picturebeans内。所有 JSP beans 都必须放在一个包内;大部分JSP引擎不能找到缺省包内的类。
除get方法之外还提供了set方法。现在,还只能读而不能改变图片的属性。将来可以用来改变这些属性以便用户编辑图片。
自从需要的值被存储在beans中而不是在局部变量中后,就必须用<%=picture.getCaption()%>而不是<%=caption%>了。当然,也可定义局部变量比如String caption = picture.getCaption();.这样使代码较容易理解。
用thumbnails来进行缩放
在第一个JSP的输出中,显示了全尺寸的图像文件。可以稍微修改代码,让它显示由一些稍小一点的图象组成的列表。这要使用到存储在XML数据文件中的图像列表。
首先定义一个参数 zoom,它可以决定显示那一幅Thumbnails图像。在小图像上点击鼠标,可以看到全尺寸的原来图像。点击Zoom In 和 Zoom Out按钮可以选择下一幅或前一幅Thumbnails图像。
Thumbnails对象返回 java.util.List图像对象,找到正确的Thumbnails对象却不怎么容易,仅仅说(Image)picture.getThumbnails().get(i)是不够的。
做 Zoom In 和 Zoom Out 链接时,必须用不同的参数生成对同一个页面的递归调用。这样可以使用request.getRequestURI()方法。仅仅是将路径传给servlet,不需要带其它参数,因此可以附加自己需要的参数。
<%
if (zoom < (thumbnails.size() -1)) {
out.print("<a href=" +
request.getRequestURI() +
"?file=" + request.getParameter("file") +
"&zoom=" + (zoom+1) +
">");
out.print("Zoom In</a>");
}
%>
Here is an HTML screenshot of the working JSP page.
这里有一个关于JSP页面工作的例子。
使用JSP bean标记
JSP规范中定义了<jsp:useBean>标记,以在JSP页面中自动实例化和使用JavaBeans。useBean标记通常都可被内嵌的Java代码所替换。在这里我已经替换了。基于此,很多人都在询问useBean 和 setProperty标记何时需要使用的问题。关于这些标记有以下几点:
标记语法不会对 HTML 设计者构成干扰
useBean 有一个scope 参数,可以自动决定Beans是否应该存储在局部变量或 session变量中或作为一个application的属性。
如果变量是长期有效的(session或application),在必要时,useBean会初始化它,但是只有当变量已经存在时才能使用。
标记对于将来的JSP版本规范来说,潜在地具有很多优点,可以灵活地改变它的实现。(例如,一个假定的JSP引擎可以储存变量于 database或通过server进程来共享)
对于这个应用来说等价useBean语法如下:
<jsp:useBean id="picture" scope="request" class="picturebeans.DOMPicture">
<%
Document doc = DOMUtils.xml4jParse(picturefile);
Element nodeRoot = doc.getDocumentElement();
nodeRoot.normalize();
picture.setNode(nodeRoot);
%>
</jsp:useBean>
或者,如果你在 DOMBean中定义了一个setFile(String) 方法的话:
<jsp:useBean id="picture" scope="request" class="picturebeans.DOMPicture">
<jsp:setProperty name="picture" property="file" value="<%=picturefile%>"/>
</jsp:useBean>
使用XMLEntryList
为了克服使用DOM API的一些困难,做了一个XMLEntryList类。它实现了Java集合接口java.util.List并包含java.util.Map的get,put方法,提供了直接的方法集合去遍历简单XML树结构。可以用集合API的标准抽出方法来做诸如查询,迭代和子视图。实体列表中的每个实体都有键和值,如同一个Map对象。键是子结点的名字,值或是字串或子XMLEntryLists.
XMLEntryList 并不是DOM的完全替代。有好几个DOM功能它不能实现。可是,它是一个便利的包装器,用来做基本的getting, setting,list-oriented功能来处理 XML数据结构。例如为了得到picture 结点的标题属性的值,可以这样写:
String caption = (String)picturelist.get("caption");
标题属性的值被解析出并被存入一个字串中。
Caching缓存
不论XML文件有什么优点,解析它会花很多时间。为了提高基于XML的应用程序的性能,必须使用一些缓存技术。缓存必须基于XML文件名在内存中存储XML对象。如果上次加载以来,XML文件被更新了,则必须重新加载。我做了关于实现这个数据结构的一个简单工具CachedFS.java。可以使用内部类给把CachedFS作为一个回调函数来使用,就能实现XML解析,转换文件为一个对象。cache就可以将对象存储在内存。
下面是产生cache的代码。这个对象具有应用程序的范围,可以被反复使用。这段代码被放入了init.jsp。
<jsp:useBean id="cache" class="com.purpletech.io.CachedFS" scope="application">
<% cache.setRoot(application.getRealPath("/"));
cache.setLoader( new CachedFS.Loader() {
// load in a single Picture file
public Object process(String path, InputStream in) throws IOException
{
try {
Document doc = DOMUtils.xml4jParse
(new BufferedReader(new InputStreamReader(in)));
Element nodeRoot = doc.getDocumentElement();
nodeRoot.normalize();
Picture picture = new DOMPicture(nodeRoot);
return picture;
}
catch (XMLException e) {
e.printStackTrace();
throw new IOException(e.getMessage());
}
}
});
%>
</jsp:useBean>
XPath
XPath 是一个查找定位XML树结点的简单句法单位。它比DOM易于使用。当你想从一个结点进入下一个结点时,不用调用making方法。可以把全部路径嵌入一个字串,如/picture/thumbnails/image[2]。在Resin中,包括了XPath对象,可直接在应用中调用。
Node verse = XPath.find("chapter/verse", node);
XSL
这篇文章讨论了如何将Java代码嵌入JSP,用来提取 XML结点的数据。另外还有一个流行的模型来完成这个工作。就是XSL (Extensible Stylesheet Language)。它完全不同于已讨论过的JSP模型。在JSP中,主文档模型是HTML,包含有Java代码;在XSL中文档模型是XSL,包含有HTML。对于XSL和Java/JSP的关系,有许多值得讨论的地方。这里限于篇幅,就不深入了。将来在JavaWorld 中要讨论XSL 和JSP一起用的问题。
结论与展望未来之路
读完这篇文章,读者应该对于 JSP-XML应用的结构与强大之处以及缺陷有教好的理解了吧。
在开发JSP-XML应用中,最单调乏味的事是为每个 XML模式的元素构造JavaBeans。 XML Data Binding group正在开发能从给定的模式自动生成Java类的技术。我也开发了一个公开源代码的Java-XML数据绑定的原型。IBM alphaWorks最近也发布了XML Master 或(XMas),这是另外一个Java-XML数据绑定系统。
另外一个可能就是拓展文件系统的功能,构造一些更强大的特征,比如查询或事物处理。自然,我也在考虑开发源代码公开的工程来实现这种功能。有谁想写 XML搜寻引擎呢?
(c) 版权所有 2000 ITworld.com, Inc., an IDG Communications company
Resources
文章中的源代码可以从下面找到:
http://www.javaworld.com/jw-03-2000/jspxml/jspxml-webapp.zip
For future updates to that source code:
http://www.purpletech.com/jspxml/
XML:
http://www.xml.org/
http://www.xml.com/
http://www.jguru.com/faq/XML
SML, a controversial simplification of XML syntax:
http://www.xml.com/pub/1999/11/sml/index.html
http://www.xml.com/pub/1999/12/sml/responses.html
JSP:
http://java.sun.com/products/jsp
http://www.jguru.com/faq/JSP
DOM specification:
http://www.w3.org/TR/1998/REC-DOM-Level-1-19981001/java-language-binding.html
http://www.w3.org/TR/REC-DOM-Level-1/
Java-XML data binding: