在JSP页面交换XML和数据库内容的定制标记库
Mark A. Kolb
软件工程师
2003 年 8 月 11 日
Web应用程序的标志是多个子系统的集成。SQL和XML是在这类子系统之间交换数据的两种最通用的机制。在本文中,Mark Kolb介绍访问JSP页面数据库和XML内容的sql和xml库并对JSTL进行了总结。
Web应用程序的模板式(stereotypical)架构分为三层:处理请求的Web服务器、实施业务逻辑的应用程序服务器以及管理永久性数据的数据库。应用程序和数据库层之间的联接通常采用关系数据库中的SQL调用格式。当业务逻辑被写入到Java语言中时,JDBC用于实现这些调用。
如果应用程序调用与其它服务器(本地或远程)的集成,我们将需要用于在不同子系统之间交换数据的更深层次的机制。在Web应用程序内部和应用程序之间传送数据采用的越来越普遍的一种方法是XML文件的交换。
迄今为止,在我们的JSTL之旅中,我们讨论了JSTL 表达式语言(expression language,EL)和 core和 fmt标记库。在最后一部分,我们将考虑sql和xml库--正如它们的名字表示的一样 -- 提供定制标记来接入和管理从SQL数据库和XML文件检索到的数据。
xml库
根据设计,XML提供灵活的方式来表示结构化数据,这些数据同时准备进行验证,因此它尤其适应于在松散联合的系统之间交换数据。这反过来使其成为Web应用程序极具吸引力的集成技术。
与使用XML表示的数据进行交互的第一步是把数据作为一个XML文件,对其进行检索并进行分解,以创建数据结构来接入该文件中的内容。在分解文件后,您可以有选择的对其进行转换以创建新的XML文件,您可以对新的XML文件进行相同的操作。最终,文件中的数据可以被提取,然后显示或使用作为输入数据来运行其它操作。
这些步骤都在用于控制XML的JSTL标记中反映出。根据我们在第2部分 探讨核心中所讨论的,我们使用core库中的<c:import>标记来检索XML文件。然后使用<x:parse>标记来分解该文件,支持标准的XML分解技术,如文件对象模式(Document Object Model,DOM)和简单XML API(Simple API for XML,SAX)。<x:transform>标记可用于转换XML文件并依赖标准技术来转换XML数据:扩展样式表语言( Extensible Stylesheet Language,XSL)。最后,我们提供多个标记来接入和控制分解后的XML数据,但是所有这一切都依赖于另一种标准- XML路径语言(XML Path Language,XPath),以引用分解后的XML文件中的内容。
分解XML
<x:parse>标记有多种格式,取决于用户希望的分解类型。这一项操作最基本的格式使用以下语法:
<x:parse xml="
expression" var="
name" scope="
scope"
filter="
expression" systemId="
expression"/>
|
在这五种属性中,只有xml属性是需要的,其值应该是包含要分解的XML文件的字符串,或者是java.io.Reader实例,通过它可以读取要被分解的文件。此外,您可以使用以下语法,根据<x:parse>标记的主体内容来规定要被分解的文件:
<x:parse var="
name" scope="
scope"
filter="
expression" systemId="
expression">
body content
</x:parse>
|
var和scope属性规定存储分解后的文件的scoped变量。然后xml库中的其它标记可以使用这一变量来运行其它操作。注意,当var和 scope 属性存在时,JSTL用于表示分解后的文件的数据结构类型以实施为导向,从而厂商可以对其进行优化。
如果应用程序需要对JSTL提供的分解后的文件进行处理,它可以使用另一种格式的<x:parse>,它要求分解后的文件坚持使用一个标准接口。在这种情况下,该标记的语法如下:
<x:parse xml="
expression" varDom="
name" scopeDom="
scope"
filter="
expression" systemId="
expression"/>
|
当您使用<x:parse>的这一版本时,表示分解后的XML文件的对象必须使用org.w3c.dom.Document接口。当根据<x:parse>中的主体内容来规定XML文件时,您还可以使用varDom和scopeDom属性来代替var 和 scope属性,语法如下:
<x:parse varDom="
name" scopeDom="
scope"
filter="
expression" systemId="
expression">
body content
</x:parse>
|
其它两个属性filter 和 systemId 可以实现对分解流程的精确控制。filter 属性规定org.xml.sax.XMLFilter类的一个实例,以在分解之前对文件进行过滤。如果要被分解的文件非常大,但目前的工作只需要处理一小部分内容时这一属性尤其有用。systemId属性表示要被分解的文件的URI并解析文件中出现的任何相关的路径。当被分解的XML文件使用相关的URL来引用分解流程中需要接入的其它文件或资源时需要这种属性
清单1展示了<x:parse> 标记的使用,包括与 <c:import>的交互。此处<c:import> 标记用于检索众所周知的Slashdot Web 网站的RDF Site Summary (RSS)反馈,然后使用<x:parse>分解表示RSS 反馈的XML文件,表示分解后的文件的以实施为导向的数据结构被保存到名为rss的变量(带有page 范围)中。
清单1:<x:parse>与<c:import>的交互
<c:import var="rssFeed" url="http://slashdot.org/slashdot.rdf"/>
<x:parse var="rss" xml="${rssFeed}"/>
|
转换XML
XML通过XSL样式表来转换。JSTL使用<x:transform>标记来支持这一操作。与<x:parse>的情况一样,<x:transform> 标记支持多种不同的格式。<x:transform> 最基本的格式的语法是:
<x:transform xml="
expression" xslt="
expression"
var="
name" scope="
scope"
xmlSystemId="
expression" xsltSystemId="
expression">
<x:param name="
expression" value="
expression"/>
...
</x:transform>
|
关于 RSS RDF Site Summary (RSS) 是许多以新闻为导向的网站公布的XML文件格式,它列出它们当前的标题,提供链接到相关文章的URL。同样,它提供在Web上联合新闻的简单机制。关于RSS的更详细信息,请参阅 参考资料。 |
此处,xml 属性规定要被转换的文件,xslt 属性规定定义这次转换的样式表。这两种属性是必要的,其它属性为可选。
与<x:parse>的xml属性一样,<x:transform>的xml 属性值可以是包含XML文件的字符串,或者是接入这类文件的Reader。此外,它还可以采用 org.w3c.dom.Document 类或javax.xml.transform.Source 类的实例格式。最后,它还可以是使用<x:parse> 操作的var或varDom属性分配的变量值。
而且,您可以根据<x:transform> 操作的主体内容来包含要被转换的XML文件。在这种情况下,<x:transform> 的语法是:
<x:transform xslt="
expression"
var="
name" scope="
scope"
xmlSystemId="
expression" xsltSystemId="
expression">
body content
<x:param name="
expression" value="
expression"/>
...
</x:transform>
|
在这两种情况下,规定XSL 样式表的xslt 属性应是字符串、Reader或javax.xml.transform.Source实例。
如果var 属性存在,转换后的XML文件将分配给相应的scoped变量,作为org.w3c.dom.Document 类的一个实例。通常,scope属性规定这类变量分配的范围。
<x:transform> 标记还支持将转换结果存储到javax.xml.transform.Result 类的一个实例中,而不是作为org.w3c.dom.Document的一个实例。如果var 和 scope 属性被省略,result对象规定作为result属性的值,<x:transform>标记将使用该对象来保存应用该样式表的结果。清单2中介绍了使用<x:transform> 的result属性的这两种语法的变化:
清单2:使用result属性来提供javax.xml.transform.Result实例时,<x:transform>操作的语法变化
<x:transform xml="
expression" xslt="
expression"
result="
expression"
xmlSystemId="
expression" xsltSystemId="
expression">
<x:param name="
expression" value="
expression"/>
...
</x:transform>
<x:transform xslt="
expression"
result="
expression"
xmlSystemId="
expression" xsltSystemId="
expression">
body content
<x:param name="
expression" value="
expression"/>
...
</x:transform>
|
无论您采用这两种<x:transform>格式中的那一种,您都必须从定制标记单独创建javax.xml.transform.Result对象。该对象自身作为result属性的值提供。
如果既不存在var 属性,也不存在result属性,转换的结果将简单地插入到JSP页面,作为处理<x:transform> 操作的结果。当样式表用于将数据从XML转换成HTML时尤其有用,如清单3所示:
清单3:在JSP页面直接显示转换的XML数据
<c:import var="rssFeed" url="http://slashdot.org/slashdot.rdf"/>
<c:import var="rssToHtml" url="/WEB-INF/xslt/rss2html.xsl"/>
<x:transform xml="${rssFeed}" xslt="${rssToHtml}"/>
|
在本例中,使用 <c:import> 标记来读取RSS反馈和适当的样式表。样式表的输出结果是HTML,通过忽略<x:transform>的var和result 属性来直接显示。图1显示了实例结果:
图1:清单3的输出结果
与<x:parse>的systemId 属性一样,<x:transform>的xmlSystemId 和 xsltSystemId 属性用于解析XML文件中相关的路径。在这种情况下,xmlSystemId 属性应用于根据标记的 xml属性值提供的文件,而xsltSystemId 属性用于解析根据标记的xslt属性规定的样式表中的相关路径。
如果正在推动文件转换的样式表使用了参数,我们使用<x:param> 标记来规定这些参数。如果参数存在,那么这些标记必须在<x:transform> 标记主体内显示。如果根据主体内容规定了要被转换的XML文件,那么它必须先于任何 <x:param> 标记。
<x:param> 标记有两种必要的属性 -- name 和 value -- 就象本系列 第2部分 和 第3部分中讨论的<c:param> 和 <fmt:param> 标记一样。
处理XML内容
XML文件的分解和转换操作都是基于整个文件来进行。但是,在您将文件转换成一种可用的格式之后,一项应用程序通常只对文件中包含的一部分数据感兴趣。鉴于这一原因,xml 库包括多个标记来接入和控制XML文件内容的各个部分。
如果您已经阅读了本系列第2部分( 探讨核心),您将对这些xml 标记的名字非常熟悉。它们都基于JSTL core 库相应的标记。但是,这些core 库标记使用EL表达式,通过它们的value属性来接入JSP容器中的数据,而它们在xml 库中的副本使用XPath表达式,通过select属性接入XML文件中的数据。
XPath是引用XML文件中元素及它们的属性值和主体内容的标准化符号。正如其名字代表的一样,这种符号与文件系统路径的表示方法类似,使用短斜线来分开XPath语句的组分。这些组分对映于XML文件的节点,连续的组分匹配嵌套的Element。此外,星号可以用于作为通配符来匹配多个节点,括号内的表达式可以用于匹配属性值和规定索引。有多种在线参考资料介绍XPath和它的使用(见 参考资料)。
要显示XML文件的数据的一个Element,使用<x:out> 操作,它与core 库的<c:out> 标记类似。 但是,<c:out> 使用名为value 和escapeXml的属性,<x:out> 的属性为select 和escapeXml:
<x:out select="
XPathExpression" escapeXml="
boolean"/>
|
当然,两者的区别在于<x:out> 的select 属性值必须是XPath表达式,而<c:out> 的value 属性必须是EL表达式。两种标记的escapeXml 属性的意义是相同的。
清单4显示了<x:out> 操作的使用。注意,规定用于select 属性的XPath表达式由一个EL表达式规定为scoped变量,尤其是$rss。这一EL表达式根据将被求值的XPath语句来识别分解后的XML文件。该语句在此处查找名为title且父节点名为channel的Element,从而选择它找到的第一个Element(根据表达式尾部[1]索引规定)。这一<x:out> 操作的结果是显示这一Element的主体内容,关闭正在转义(Escaping)的XML字符。
清单4:使用<x:out>操作来显示XML Element的主体内容
<c:import var="rssFeed" url="http://slashdot.org/slashdot.rdf"/>
<x:parse var="rss" xml="${rssFeed}"/>
<x:out select="$rss//*[name()='channel']/*[name()='title'][1]"
escapeXml="false"/>
|
除了<x:out>之外,JSTL xml 库包括以下控制XML数据的标记:
- <x:set> ,向JSTL scoped 变量分配XPath表达式的值
- <x:if> ,根据XPath表达式的布尔值来条件化内容
- <x:choose>、<x:when>和<x:otherwise>,根据XPath表达式来实施互斥的条件化
- <x:forEach> ,迭代根据XPath表达式匹配的多个Elements
每个这些标记的操作与core库中相应的标记类似。例如,清单5中显示的<x:forEach>的使用, <x:forEach> 操作用于迭代XML文件中表示RSS反馈的所有名为item 的Element。注意,<x:forEach>主体内容中嵌套的两个<x:out> 操作中的XPath表达式与<x:forEach>标记正在迭代的节点相关。它们用于检索每个item element的子节点link 和 title。
清单5:使用<x:out> 和<x:forEach>操作来选择和显示XML数据
<c:import var="rssFeed" url="http://slashdot.org/slashdot.rdf"/>
<x:parse var="rss" xml="${rssFeed}"/>
<a href="<x:out select="$rss//*[name()='channel']/*[name()='link'][1]"/>"
><x:out select="$rss//*[name()='channel']/*[name()='title'][1]"
escapeXml="false"/></a>
<x:forEach select="$rss//*[name()='item']">
<li> <a href="<x:out select="./*[name()='link']"/>"
><x:out select="./*[name()='title']" escapeXml="false"/></a>
</x:forEach>
|
清单5中JSP程序代码的输出结果与 清单3类似,它在 图1中显示。xml 库以XPath为导向的标记提供备选的样式表来转换XML内容,尤其是当最后的输出结果是HTML的情况。
sql库
JSTL第4个也是最后一个操作是sql定制标记库。正如其名字代表的一样,该库提供与关系数据库交互的标记。尤其是sql 库定义规定数据源、发布查询和更新以及将查询和更新编组到事务处理中的标记。
Datasource
Datasource是获得数据库连接的工厂。它们经常实施某些格式的连接库来最大限度地降低与创建和初始化连接相关的开销。Java 2 Enterprise Edition (J2EE)应用程序服务器通常内置了Datasource支持,通过 Java命名和目录接口( Java Naming and Directory Interface,JNDI)它可用于J2EE应用程序。
JSTL的sql 标记依赖于Datasource来获得连接。实际上包括可选的dataSource 属性以明确规定它们的连接工厂作为 javax.sql.DataSource 接口实例,或作为JNDI名。
您可以使用<sql:setDataSource> 标记来获得javax.sql.DataSource 实例,它采用以下两种格式:
<sql:setDataSource dataSource="
expression"
var="
name" scope="
scope"/>
<sql:setDataSource url="
expression" driver="
expression"
user="
expression" password="
expression"
var="
name" scope="
scope"/>
|
第一种格式只需要dataSource 属性,而第二种格式只需要url 属性。
通过提供JNDI名作为dataSource属性值,您可以使用第一种格式来接入与JNDI名相关的datasource。第二种格式将创建新的datasource,使用作为url属性值提供的JDBC URL。可选的driver 属性规定实施数据库driver的类的名称,同时需要时user 和 password 属性提供接入数据库的登录证书。
对于<sql:setDataSource>的任何一种格式而言,可选的var 和 scope 属性向scoped变量分配特定的datasource。如果var属性不存在,那么 <sql:setDataSource> 操作设置供sql 标记使用的缺省 datasource,它们没有规定明确的datasource。
您还可以使用javax.servlet.jsp.jstl.sql.dataSource 参数来配置sql 库的缺省datasource。在实践中,在应用程序的Web.xml文件中添加与清单6中显示的类似的程序代码是规定缺省datasource最方便的方式。使用<sql:setDataSource> 来完成这一操作要求使用JSP页面来初始化该应用程序,因此可以以某种方式自动运行这一页面。
清单6:使用JNDI名来设置JSTL在web.xml部署描述符中的缺省datasource
<context-param>
<param-name>javax.servlet.jsp.jstl.sql.dataSource</param-name>
<param-value>jdbc/blog</param-value>
</context-param>
|
提交查询和更新
在建立了datasource接入之后,您可以使用<sql:query> 操作来执行查询,同时使用<sql:update> 操作来执行数据库更新。查询和更新使用SQL语句来规定,它可以是使用基于JDBC的java.sql.PreparedStatement 接口的方法来实现参数化。参数值使用嵌套的<sql:param> 和 <sql:dateParam> 标记来规定。
支持以下三种<sql:query> 操作:
<sql:query sql="
expression" dataSource="
expression"
var="
name" scope="
scope"
maxRows="
expression" startRow="
expression"/>
<sql:query sql="
expression" dataSource="
expression"
var="
name" scope="
scope"
maxRows="
expression" startRow="
expression">
<sql:param value="
expression"/>
...
</sql:query>
<sql:query dataSource="
expression"
var="
name" scope="
scope"
maxRows="
expression" startRow="
expression">
SQL statement
<sql:param value="
expression"/>
...
</sql:query>
|
前两种格式只要求sql 和 var 属性,第三种格式只要求var 属性。
var 和 scope 属性规定存储查询结果的scoped 变量。maxRows 属性可以用于限制查询返回的行数,startRow 属性允许忽略一些最开始的行数(如当结果集(Result set)由数据库来构建时忽略)。
在执行了查询之后,结果集被分配给scoped变量,作为javax.servlet.jsp.jstl.sql.Result 接口的一个实例。这一对象提供接入行、列名称和查询的结果集大小的属性,如表1所示:
表1:javax.servlet.jsp.jstl.sql.Result 接口定义的属性
属性 |
说明 |
rows |
一排SortedMap 对象,每个对象对映列名和结果集中的单行 |
rowsByIndex |
一排数组,每个对应于结果集中的单行 |
columnNames |
一排对结果集中的列命名的字符串,采用与rowsByIndex属性相同的顺序 |
rowCount |
查询结果中总行数 |
limitedByMaxRows |
如果查询受限于maxRows 属性值为真 |
在这些属性中,rows 尤其方便,因为您可以使用它来在整个结果集中进行迭代和根据名字访问列数据。我们在 清单7中阐述了这一操作,查询的结果被分配到名为queryResults的scoped变量中,然后使用core 库中的<c:forEach>标记来迭代那些行。嵌套的<c:out> 标记利用EL内置的Map 收集支持来查找与列名称相对应的行数据。(记得在 第1部分 ,${row.title}和${row["title"]} 是相等的表达式。)
清单7还展示了使用<sql:setDataSource> 来关联datasource 和scoped变量,它由<sql:query> 操作通过其dataSource 属性随后接入。
清单7:使用<sql:query>来查询数据库,使用<c:forEach>来迭代整个结果集
<sql:setDataSource var="dataSrc"
url="jdbc:mysql:///taglib" driver="org.gjt.mm.mysql.Driver"
user="admin" password="secret"/>
<sql:query var="queryResults" dataSource="${dataSrc}">
select * from blog group by created desc limit ?
<sql:param value="${6}"/></sql:query>
<table border="1">
<tr>
<th>ID</th>
<th>Created</th>
<th>Title</th>
<th>Author</th>
</tr>
<c:forEach var="row" items="${queryResults.rows}">
<tr>
<td><c:out value="${row.id}"/></td>
<td><c:out value="${row.created}"/></td>
<td><c:out value="${row.title}"/></td>
<td><c:out value="${row.author}"/></td>
</tr>
</c:forEach>
</table>
|
图2显示了清单7中JSTL程序代码的实例页面输出结果。注意:清单7中<sql:query>操作主体中出现的SQL语句为参数化语句。
图2:清单7的输出
在<sql:query> 操作中,SQL语句根据主体内容来规定,或者使用?字符,通过sql 属性实现参数化。对于SQL语句中每个这样的参数来说,应有相应的<sql:param> 或 <sql:dateParam> 操作嵌套到<sql:query> 标记的主体中。<sql:param> 标记只采用一种属性 -- value --来规定参数值。此外,当参数值为字符串时,您可以忽略value 属性并根据<sql:param> 标记的主体内容来提供参数值。
表示日期、时间或时间戳的参数值使用<sql:dateParam> 标记来规定,使用以下语法:
<sql:dateParam value="
expression" type="
type"/>
|
对于<sql:dateParam>来说,value 属性的表达式必须求 java.util.Date 类实例的值,同时type 属性值必须是date、time或timestamp,由SQL语句需要那类与时间相关的值来决定。
与<sql:query> 一样,<sql:update> 操作支持三种格式:
<sql:update sql="
expression" dataSource="
expression"
var="
name" scope="
scope"/>
<sql:update sql="
expression" dataSource="
expression"
var="
name" scope="
scope">
<sql:param value="
expression"/>
...
</sql:update>
<sql:update dataSource="
expression"
var="
name" scope="
scope">
SQL statement
<sql:param value="
expression"/>
...
</sql:update>
|
sql 和dataSource 属性有与<sql:query>相同的<sql:update> 语义。同样,var 和 scope 属性可以用于规定scoped变量,但在这种情况下,分配给scoped变量的值将是java.lang.Integer 的一个实例,显示作为数据库更新结果而更改的行数。
管理事务处理
事务处理用于保护作为一个组必须成功或失败的一系列数据库操作。事务处理支持已经嵌入到JSTL的sql 库中,通过将相应的<sql:query>和<sql:update>操作嵌套到<sql:transaction>标记的主体内容中,从而将一系列查询和更新操作打包到一个事务处理中也就显得微不足道了。
<sql:transaction> 语法如下:
<sql:transaction dataSource="
expression" isolation="
isolationLevel">
<sql:query .../>
or <sql:update .../>
...
|
<sql:transaction> 操作没有必需的属性。如果您忽略了dataSource 属性,那么使用JSTL的缺省datasource。isolation 属性用于规定事务处理的隔离级别,它可以是read_committed、read_uncommitted、repeatable_read或serializable。如果您未规定这一属性,事务处理将使用datasource的缺省隔离级别。
您可能希望所有嵌套的查询和更新必须使用与事务处理相同的datasource。实际上,嵌套到<sql:transaction>操作中的<sql:query> 或 <sql:update> 不能用于规定dataSource 属性。它将自动使用与周围的<sql:transaction>标记相关的datasource (显性或隐性)。
清单8显示了如何使用<sql:transaction> 的一个实例:
清单:使用<sql:transaction>来将数据库更新联合到事务处理中
<sql:transaction>
<sql:update sql="update blog set title = ? where id = ?">
<sql:param value="New Title"/>
<sql:param value="${23}"/>
</sql:update>
<sql:update sql="update blog set last_modified = now() where id = ?">
<sql:param value="${23}"/>
</sql:update>
</sql:transaction>
|
注意事项
JSTL的xml 和 sql 库使用定制标记,从而能够在JSP页面上实施复杂的功能,但是在您的表示层实施这类功能可能不是最好的方法。
对于多位开发人员长期编写的大型应用程序来说,实践证明,用户接口、基本的业务逻辑和数据仓库之间的严格分离能够长期简化软件维护。广泛流行的模式/视图/控制器( Model-View-Controller,MVC)设计模板是这一“最佳实践”的公式化。在J2EE Web应用程序领域中,模式是应用程序的业务逻辑,视图是包含表示层的JSP页面。(控制器是form handlers和其它服务器端机制,使浏览器操作能够开始更改模式并随后更新视图。) MVC规定应用程序的三个主要elements--模式、视图和控制器 --相互之间有最小的相关性,从而限制相互之间的交互到统一、精心定义的接口。
应用程序依赖XML文件来进行数据交换以及关系数据库来提供数据永久性都是应用程序业务逻辑(也就是其模式)的特征。因此,使用MVC设计模板建议无需在应用程序的表示层(也就是其视图)反映这些实施细节。如果JSP用于实施表示层,那么使用 xml 和 sql 库将违反MVC,因为它们的使用将意味着在表示层内暴露基本业务逻辑的elements。
鉴于这一原因,xml 和 sql 库最适用于小型项目和原型工作。应用程序服务器对JSP页面的动态编译也使得这些库中的定制标记可以用于作为调试工具。
结束语
在本系列中,我们讨论了4个JSTL定制标记库的功能及它们的使用。在 第1部分 和 第2部分,我们讨论通过El和core 库标记的使用,您如何在许多常见情况下避免JSP脚本程序。 第3部分 关注使用fmt 库来本地化Web内容。
在最后一部分,我们讨论了xml 和 sql 库的功能。如果您愿意接受将业务逻辑包含到表示层的结果,这两个库中的标记都使其能够非常轻松地将XML文件和关系数据库中的内容结合到JSP页面。这两个库还展示了当集成<sql:query> 和<c:forEach>时,JSTL库如何构建和集成,以及xml 库利用<c:import> 操作的能力。
参考资料
posted on 2005-06-06 15:30
小米 阅读(349)
评论(0) 编辑 收藏 所属分类:
Java