| J2EE 和 Web 服务
构建支持 XML 的 JavaServer Faces 应用程序 作者:Yuli Vasiliev
学习如何构建一个与 Oracle XML DB 交互的 JavaServer Faces 应用程序。
JavaServer Faces (JSF) 技术是一种新的服务器端用户界面 (UI) 组件框架,它正迅速成为 J2EE 应用程序的标准 Web 应用程序框架。JSF 技术的重要意义在于它使 Web 开发人员可以运用模型视图控制器 (MVC) 原则,从而将 Web 应用程序的模型层与表示层清晰分离。实际上,这样的分离使开发人员能够利用劳动力分工的好处 — Java 开发人员可以集中设计应用程序后端和集成代码,而不需要任何编程技能的页面制作人员可以集中设计应用程序的网页、在网页上布置 UI 组件以及将这些组件关联到应用程序后端对象。显而易见,该方法使开发更灵活、更具伸缩性和维护性的解决方案变得更简单。例如,为现有 JSF 应用程序添加 XML 支持基本上不需要对该应用程序的 JSF 页面进行任何更改,而仅需要修改应用程序后端。
由于了解新技术的最好方法就是用它来解决一个实际问题,因此我将在本文中介绍如何开发一个简单的 JSF 应用程序。该应用程序基于 XML 工作,并与 Oracle XML DB(Oracle 数据库的一个特性,它支持高性能的原生存储以及 XML 内容的检索)进行交互。在本文附带的示例中,您将首先开发一个简单的时事通讯应用程序,该应用程序基于 JSF 框架并且最初不提供任何 XML 支持。有了一个不支持 XML 的 JSF 应用程序后,我将着重介绍如何为其添加 XML 功能。此处的主要目标是演示为现有 JSF 应用程序添加 XML 功能的不同方法。
要求
由于 JSF 应用程序本质上是一个在必须支持 Servlet 2.3 和 JSP 1.2(或更高版本)的 Web 容器上运行的标准 Java Web 应用程序,因此本文假设您已经安装了一个 Web 容器,如 Oracle Application Server Containers for J2EE (OC4J) 10g 10.1.2 或 Tomcat 5.0。开发 JSF 应用程序所需的另一个软件包是 JSF 引用实现。幸运的是,Oracle JDeveloper 从 10.1.3 版开始就提供了对 JSF 的内置支持。在编写本文时,Oracle JDeveloper 10.1.3 开发人员预览版已可供下载。本预览版不但包含与 JDeveloper IDE 捆绑在一起的 JSF 引用实现,而且为开发人员提供了大量简化 JSF 应用程序开发的工具。这些工具包括一个支持 JSF 的 JSP 可视化编辑器(用于在可视化编辑器中呈现 JSF 组件)和一个 JSF 页面流制图器(使您可以可视化方式设计 JSF 导航模型)。
使用 JDeveloper 开发 JSF 应用程序
尽管您可以使用一个简单的文本编辑器(如记事本或 Vi)为该示例应用程序生成所有 JSF 页面、Java 源代码和配置文件,然后使用 javac 编译程序从命令行提示编译 Java 源代码,但本文假设您将使用 Oracle JDeveloper 10g 10.1.3 版或更高版本完成所有这些任务。通常,有很多原因可能导致您需要使用 JDeveloper 10g 作为开发工具来构建与数据库交互的 JSF 应用程序。首先,使用 JDeveloper 10g 将使您可以利用各种可以用于开发、调试、测试、调整和部署 JSF 应用程序的工具。此外,还对 JDeveloper 10g 进行了优化,以帮助开发人员生成一个多层的数据库应用程序体系结构。当您开发复杂的 JSF 应用程序时,后者尤其有用 - 回想一下,使用 JSF 技术的主要优点之一就是能够基于 MVC 体系结构组织 Web 应用程序,这使开发人员可以将应用程序的表示与业务逻辑分离开。
但请注意,本文的中心并非是介绍如何将 JSF 与 JDeveloper 结合使用。(许多其他资源可用于此目的。)显而易见,使用 JDeveloper 可以帮助您简化和加快构建本文附带的示例应用程序的开发过程。不过,即使不使用 JDeveloper,您仍然可以构建该示例。
构建一个不支持 XML 的简单 JSF 应用程序
本部分逐步介绍了构建一个基于 MVC 体系结构组织 JSF 应用程序的过程。完成本部分后,您将拥有一个最初不提供 XML 功能的简单时事通讯应用程序(实际上,它并不发送时事通讯,而是让用户进行订阅)。下面的各个部分将演示如何为该 JSF 应用程序添加 XML 功能。
开发模型 bean。本子部分着重介绍如何开发模型对象,这些对象将用于处理应用程序数据,其中包括将数据移入和移出数据库。尤其是,您将了解如何创建分别用于保存和操作登录信息和订阅者信息的模型 bean。但您在随后的各个子部分中将了解到,此应用程序并不直接使用这些模型对象。相反,它使用 LoginBean 和 SubscriberBean 辅助 bean。这两个 bean 分别扩展了 Login 和 Subscriber 模型 bean,从而允许模型层与表示层更大程度的分离。LoginBean 和 SubscriberBean 辅助 bean 将在本部分后面的“开发辅助 Bean”子部分中介绍。
既然我们知道了我们要做什么,那我们就开始吧。如果您使用 Oracle JDeveloper,则应先创建一个新应用程序。将该应用程序命名为 jsfsubscr,为 Application Package Prefix 指定同一名称,并保留 Directory Name 的默认设置。对于 Application Template,选择 Web Application [JSF, JSP, EJB](Oracle JDeveloper 从 10.1.3 版开始提供了此模板)。结果,JDeveloper 将创建 jsfsubscr 应用程序,它包含两个项目,即 Model 和 ViewController。将 Model 项目用于将要包含应用程序业务逻辑的模型对象。而 ViewController 项目用于 JSP 页面、配置文件、辅助 bean 以及其他构成表示层和定义应用程序行为的组件。
在继续构建 jsfsubscr 应用程序之前,确保指定 ViewController 项目依赖于 Model 项目。为此,请打开 ViewController 项目的 Project Properties\Dependencies 对话框,然后单击 Model.jpr 复选框。
首先从 Model 项目中的 Login 类开始。此模型 bean 用于保存用户验证信息:
package jsfsubscr.model;
public class Login {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
为保存订阅者提交的信息,应用程序将使用扩展 Login 的 Subscriber 类:
package jsfsubscr.model;
public class Subscriber extends Login {
private String name;
private String phone;
private boolean subscribed;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public boolean isSubscribed() {
return subscribed;
}
public void setSubscribed(boolean subscribed) {
this.subscribed = subscribed;
}
}
访问数据库。示例应用程序中使用的另一个重要的业务对象是 DBUtils 数据访问对象,它提供访问和操作数据库中存储的应用程序数据的方法。您还在 Model 项目中创建该类。当然,您可以将数据库代码直接添加到以上介绍的模型 bean。但是,对要与数据库通信的业务对象来说,拥有一个独立的层总是不错的。考虑清单 1(位于示例代码下载中)中的 DBUtils 类。
请注意,DBUtil 类的每个公共方法都使用 throws 子句声明该方法使用 Java throw 语句显式抛出的异常。如果出现异常,该方法只将该异常重新抛到调用代码中定义的下一个更高级别的异常处理程序。并使用附加的原始子句创建一个新异常,从而使您可以将异常重新抛到调用代码而不会失去导致该异常的原始子句。由于 DBUtil 的方法只重新抛出异常,而不采取进一步操作,因此您可能想知道在何处处理重新抛出的异常。当您开始开发“开发辅助 Bean”子部分中介绍的应用程序辅助 bean 时,便会对此有一个清楚地了解了。尤其是,您将了解到用户界面特定的代码需要知道失败的详细信息以便将相应的字符串返回给导航处理程序。
从头到尾看一遍清单 1 中显示的代码,您可能会注意到其中使用的所有 SQL 语句都只查询数据库表,即 subscriber。确保按如下所示在数据库中创建 subscriber 表:
CREATE TABLE subscriber (
email VARCHAR2(50) PRIMARY KEY,
password VARCHAR2(30),
name VARCHAR2(30),
phone VARCHAR2(15),
subscribed NUMBER(1)
)
开始使用 DBUtils 类之前,必须创建一个 getDBUtils 公共方法,该方法将该类的实例返回给使用这些类方法的代码。实际上,您可以在 DBUtils 类本身中创建 getDBUtils 方法,并在其中声明一个私有的静态属性来保存该类的实例。您也可以将 getDBUtils 方法放置在 Model 项目中单独的 Utils 类中,如下所示:
package jsfsubscr.model;
public class Utils {
private static DBUtils dbUtils;
public synchronized static DBUtils getDBUtils() throws Exception{
if (dbUtils == null)
try {
dbUtils = (DBUtils)Class.forName("jsfsubscr.model.DBUtils").newInstance();
}
catch (Exception e) {
throw new Exception (e.getMessage());
}
return dbUtils;
}
}
现在您已经创建了一个数据访问对象,该对象提供访问和操作数据库中存储的数据所必需的方法,并定义了一个将返回该数据访问对象的一个实例的方法。您可以返回到前面介绍的模型对象,并通过添加将操作该数据的方法增强它们的功能。首先将 doLogin 方法添加到 Login 模型 bean。考虑以下 doLogin 方法:
public void doLogin() throws Exception {
try {
subscriber = Utils.getDBUtils().select(this.email);
if(!password.equals(subscriber.getPassword().trim()))
throw new Exception("Wrong Password");
}
catch (Exception e) {
throw new Exception(e.toString());
}
}
此外,确保为 Login 类添加 subscriber 属性:
private Subscriber subscriber;
然后,创建 get 访问器以允许调用代码访问 subscriber 属性:
public Subscriber getSubscriber() {
return subscriber;
}
您可以看到,subscriber 属性用于保存从数据库中检索到的用户信息。请注意,您不必为 subscriber 属性定义 set 访问器,这是因为该属性仅在 doLogin 方法中写入,而从不会在将使用 Login 类的代码中写入。这就是最好只为 subscriber 属性提供读取访问权限的原因了。另一个应注意的有关 doLogin 方法的重要事项是,该方法完全专注于数据库,不包含任何用户界面特定的代码。
接下来,返回到 Subscriber 类,为该类添加以下两个公共方法:
public void save() throws Exception {
try {
Utils.getDBUtils().insert(this);
}
catch (Exception e) {
throw new Exception(e.toString());
}
}
public void remove() throws Exception {
try {
Utils.getDBUtils().delete(this);
}
catch (Exception e) {
throw new Exception(e.toString());
}
}
您可以看到,以上任一方法均不包含任何用户界面特定的代码。您可能想知道,我们为什么需要此示例应用程序来清晰地分离其各个层。实际上,该分离可以更容易地显示在您开始开发支持 XML 的应用程序时应用程序的哪些层确实需要修改而哪些层可以保持不变。
开发辅助 bean。本子部分着重介绍您将在示例应用程序中使用的辅助 bean。下个子部分演示了如何配置此应用程序来使用此处介绍的辅助 bean。
辅助 bean 最重要的特性是与页面上使用的用户界面组件关联的属性和方法。对于该示例应用程序,您不必从头创建辅助 bean,而是使用用户界面特定的代码来扩展前面介绍的模型 bean。现在切换到 ViewController 项目,创建用于扩展 Login 模型的 LoginBean 辅助 bean,如下所示:
package jsfsubscr.view;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import jsfsubscr.model.Login;
public class LoginBean extends Login
{
public LoginBean()
{
}
public String login()
{
FacesContext ctx = FacesContext.getCurrentInstance();
ValueBinding bnd = ctx.getApplication().createValueBinding("#{subscr}");
bnd.setValue(ctx, null);
try {
doLogin();
}
catch (Exception e) {
Logger.getLogger("jsfsubscr.view").log(Level.SEVERE, e.getMessage(), e);
ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(),""));
return "loginFailure";
}
return "loginSuccess";
}
}
如上所示,LoginBean bean 仅用一个方法(即 login)就扩展了 Login bean。稍后您将在“设计 JSF 页面”子部分中了解到,login 方法是一个动作方法,在用户单击 login.jsp 页面上的 Login 按钮时将调用它。login 方法所做的第一件事是将 subscr 受管理 bean 设为空,这是因为它可能保存有关先前用户的信息。(您将在“使用受管理 Bean 功能”子部分中了解到,subscr 受管理 bean 代表一个 SubscriberBean 对象,该对象用于在应用程序中保存和操作当前用户的信息。)接下来,login 方法调用 doLogin 方法,返回用于导航处理程序的逻辑输出字符串。通常,导航处理程序负责根据辅助 bean 中动作方法的逻辑输出或 UICommand 组件定义的动作属性的硬编码的输出选择要显示的下一个 JSF 页面。在该示例中,login 动作方法根据用户名和口令是否合法生成输出字符串。
然后,在 ViewController 项目中创建 SubscriberBean,以扩展前面介绍的 Subscriber 模型 bean:
package jsfsubscr.view;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import jsfsubscr.model.Subscriber;
public class SubscriberBean extends Subscriber
{
public SubscriberBean()
{
}
public String unsubscribe()
{
try {
remove();
}
catch (Exception e) {
Logger.getLogger("jsfsubscr.view").log(Level.SEVERE, e.getMessage(), e);
FacesContext ctx = FacesContext.getCurrentInstance();
ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(),""));
return "unsubscrFailure";
}
return "unsubscrSuccess";
}
public String subscribe()
{
try {
save();
}
catch (Exception e) {
Logger.getLogger("jsfsubscr.view").log(Level.SEVERE, e.getMessage(), e);
FacesContext ctx = FacesContext.getCurrentInstance();
ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(),""));
return "subscrFailure";
}
return "subscrSuccess";
}
}
此处介绍的辅助 bean 使用用户界面特定的代码扩展了模型 bean。当然,您也可以根本不使用模型 bean,而是将所有业务逻辑(包括数据库访问调用)封装在辅助 bean 中。但这将违反表示逻辑与业务逻辑的分离,从而降低代码的可维护性。
使用受管理 bean 功能。幸好,您不必编写负责创建辅助 bean 实例的代码 - JSF 提供了一个称作受管理 bean 功能的高效机制,它使得用户界面组件引用 bean 时将其提供给应用程序。您可以在应用程序配置文件(通常名为 faces-config.xml)中配置受管理 bean 功能。该文件还用于为要使用的应用程序定义转换程序、验证程序和导航规则(以及其他 JSF 应用程序元素)。在 Oracle JDeveloper 10.1.3 中,当您基于 Web Application [JSF, JSP, EJB] 模板创建新应用程序时,将在 ViewController 项目中自动创建 faces-config.xml 配置文件。此外,JDeveloper 为您提供了图形界面,用于在 faces-config.xml 文件中指定受管理 bean 和导航规则。
现在,将 SubscriberBean 和 LoginBean 的受管理 bean 声明添加到 faces-config.xml 文件中,如下所示:
<faces-config xmlns="http://java.sun.com/JSF/Configuration">
...
<managed-bean>
<managed-bean-name>subscr</managed-bean-name>
<managed-bean-class>jsfsubscr.view.SubscriberBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>email</property-name>
<value>#{login.subscriber.email}</value>
</managed-property>
<managed-property>
<property-name>password</property-name>
<value>#{login.subscriber.password}</value>
</managed-property>
<managed-property>
<property-name>name</property-name>
<value>#{login.subscriber.name}</value>
</managed-property>
<managed-property>
<property-name>phone</property-name>
<value>#{login.subscriber.phone}</value>
</managed-property>
</managed-bean>
<managed-bean>
<managed-bean-name>login</managed-bean-name>
<managed-bean-class>jsfsubscr.view.LoginBean</managed-bean-class>
<managed-bean-scope> session</managed-bean-scope>
</managed-bean>
...
</faces-config>
注意,subscr bean 包含四个 managed-property 元素。每个元素在其值元素的内部使用一个值绑定表达式将 subscr bean 的相应属性与 Subscriber 对象(它本身是另一个 bean(即 login)的属性)的相应属性关联起来。实际上,本示例演示了一个通常在 JSF 应用程序中用来将 bean 链接在一起的标准机制。
配置导航规则。正如我在前面指出的,导航处理程序负责根据辅助 bean 中的动作方法的逻辑输出或 UICommand 组件定义的动作属性的输出选择要显示的下一个 JSF 页面。但无论在哪种情况下,动作输出都必须与在应用程序配置文件中定义的导航规则中的输出相匹配。清单 2(示例代码下载)中显示了示例应用程序的导航规则。
查看清单 2 中显示的导航规则(实际上,Oracle JDeveloper 10.1.3 包含一个 JSF 导航模型制图器),您可能会注意到此示例应用程序的表示层只包含三个页面:login.jsp、welcome.jsp 和 subscribe.jsp。该示例应用程序的第一个页面是 login.jsp。在该页面上,用户可以输入该验证信息来登录,或单击订阅链接转到 subscribe.jsp 页面。成功登录后,用户将转到 welcome.jsp 页面,该页面显示一个欢迎消息、用户的订阅信息、注销链接以及引用 SubscriberBean 中的 unsubscribe 方法的取消订阅链接。
设计 JSF 页面。接下来,在 ViewController 项目中创建应用程序的 JSF 页面。在 Oracle JDeveloper 10.1.3 中,可以使用 Create JavaServer Faces (JSF) JSP 向导(通过选择 File->New->Web-Tier->JSF->JSF JSP 调用)构建支持 JSF 的 JSP 页面。该向导为生成的 JSF 页面自动创建辅助 bean,并将相应的受管理 bean 元素插入到 faces-config.xml 文件中。但由于您已经为示例应用程序创建了辅助 bean 并配置了 faces-config.xml 文件,因此可能要通过选择 File->New->Web-Tier->JSP->JSP 调用另一个 JDeveloper 的向导:Create JavaServer Pages (JSP)。通过该向导您还可以创建 JSF JSP 页面 -- 但与前者不同,它不为该页面自动创建辅助 bean。
由于 login.jsp 页面是应用程序启动时用户看到的第一个页面,因此可能需要从此页面开始为示例应用程序设计 JSF 页面。清单 3(示例代码下载)中显示了 login.jsp 的源代码。
可以在该清单中看到 login.jsp 包含两个表单。第一个表单包含一个 commandLink 标记,它的动作属性设置为 subscribe。用户单击此链接可以转到 subscribe.jsp 页面。第二个表单是登录表单,它包含代表用于输入验证信息的域的标记以及 commandButton 标记(代表用于提交表单的 Login 按钮)。具体而言,当用户单击 login.jsp 页面上的 Login 按钮时,将调用 LoginBean 实例的 login 方法。
正如前面指出的,用户通过单击 login.jsp 页面上的订阅链接转到 subscribe.jsp 页面。在 subscribe.jsp 页面上,用户必须填写所有域,然后单击 subscribe 按钮将信息保存到数据库中。清单 4 包含 subscribe.jsp 文件。
查看该清单,您可能会注意到 selectBooleanCheckbox 标记包含 validator 属性,该属性引用 SubscriberBean 辅助 bean 的 subscribedCheck 方法。该方法只检查用户是否选中了 subscribed 复选框。如果选中此复选框,则不采取进一步的操作。否则,subscribedCheck 方法将生成一个新的 FacesMessage,然后将该 FacesMessage 与 subscribed 组件关联,并最终设置一个指示该组件的值无效的 false 标记。
public void subscribedCheck(FacesContext context, UIComponent component, Object value)
{
if (value.toString()=="false") {
FacesMessage message = new FacesMessage("You must check this box to subscribe");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
String componentId = component.getClientId(context);
context.addMessage(componentId, message);
((EditableValueHolder) component).setValid(false);
}
}
此外,确保将以下导入语句置于 SubscriberBean.java 中现有导入语句的后面:
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
现在,您可以返回去开发 JSF 页面了。正如前面指出的,一旦成功登录,用户即转到 welcome.jsp 页面。该页面显示用户的订阅信息,并允许用户通过单击取消订阅链接取消他/她的订阅。清单 5(示例代码)提供了 welcome.jsp 页面的源代码。
Servlet 配置。最后,在您可以将示例应用程序部署到应用服务器内部之前,需要创建 Web 应用程序部署描述文件。实际上,JSF 应用程序与所有 J2EE Web 应用程序一样,都是通过 web.xml 部署描述文件中包含的元素进行配置的。对于 JSF 应用程序,以上的部署描述文件必须指定将处理 JSF 请求的 servlet 以及用于处理 servlet 的 servlet 映射。当您基于 Web Application [JSF, JSP, EJB] 应用程序模板创建应用程序时,Oracle JDeveloper 10.1.3 将自动生成该文件。大多数情况下,您不必更改 JDeveloper 生成的 web.xml 文件。示例应用程序的 web.xml 部署描述文件可能形如示例代码清单 6 所示。
要从 JDeveloper 中测试示例应用程序,只需在 Application Navigator 窗口中右键单击 login.jsp 页面,然后从上下文菜单中选择 Run。其结果是,JDeveloper 在它的嵌入式 OC4J 服务器中运行该应用程序,并将 login.jsp 页面加载到浏览器中,如图 1 所示。
图 1. 该应用程序的第一个页面
在 login.jsp 页面上,单击订阅链接打开 subscribe.jsp 页面。在该页面中应输入验证信息以及类似图 2 中所示的有关您自己的其他信息。
 图 2. 提供订阅信息
通过单击 Subscribe 按钮提交在 subscribe.jsp 页面上输入的信息后,您将返回到 login.jsp 页面,在该页面中您应输入验证信息,然后单击 Login 按钮登录。成功登录后,您将转到 welcome.jsp 页面,该页显示一个欢迎消息以及您在订阅时提供的信息。welcome.jsp 页面应如图 3 所示。
图 3. 查看用户的订阅信息
此时,您已经创建了一个与 Oracle 交互的简单 JSF 应用程序,它将关系数据移入和移出数据库。以下各部分介绍如何使该应用程序支持 XML,从而使它不仅可以与 Oracle XML DB 进行 XML 通信,而且它本身也可以处理 XML 数据。
为现有 JSF 应用程序提供 XML 功能
您已经拥有了一个 JSF 应用程序,接下来您可能需要为其添加 XML 功能。可能您提出的第一个问题就是“我怎样才能使 JSF 应用程序与 Oracle XML DB 进行 XML 通信?”首先,您可以简单地重写 DBUtils 数据访问对象中使用的 SQL 查询,以便可以使用它们访问和操作 XML 而非关系数据。由于 Oracle XML DB 提供了 SQL 的 XML 扩展,而这些扩展使您可以使用标 SQL 准命令(如 SELECT、UPDATE 和 INSERT)处理 XML 数据,因此您可以轻松地执行此操作。
设计 XML 数据库。在您继续开发支持 XML 的示例应用程序之前,您需要创建所有必需的数据库对象,以便以 XML 格式存储订阅者的有关信息。首先,您可能要创建一个 XMLType 类型的 subscriberXML 表来以 XML 格式存储订阅者记录。必须注意,XMLType 是 Oracle 数据库用于存储 XML 数据的原生数据类型。创建 XMLType 表的一种方法是在数据库中注册一个带注释的 XML 模式。假设您将使用带有信息注释的 XML 模式自动生成对象类型和对象表。要注册一个由 subscriber 注释的 XML 模式,您可能使用示例代码清单 7 中显示的 PL/SQL 代码。
以上 XML 模式定义订阅者 XML 文档的结构并在 URL http://localhost:8080/public/subscriber.xsd 下注册。(注意,使用默认端口 8080 的 Tomcat 用户可能在此处会遇到端口冲突。)在数据库中注册该模式时,Oracle 将自动创建 subscr_t 对象类型和 XMLType 类型的 subscriberXML 表。由于 subscriberXML 表被限制在上面的 XML 模式,因此只能将符合该 XML 模式的 subscriber XML 文档存储到该表中。当根据该模式验证 subscriber XML 文档时,Oracle XML DB 将分解该文档的内容并将它们作为 subscr_t 的实例存储到 subscriberXML 表中。但应注意,当您插入或更新 subscriberXML 表中存储的 XML 内容时,Oracle XML DB 将不执行完整的 XML 模式验证。相反,它将只检查该 XML 文档是否符合对象关系存储。例如,如果您试图插入一个 subscriber XML 文档(其口令节点包含一个大于 30 个字符的值),Oracle XML DB 将生成以下错误消息:
ERROR at line 1:
ORA-30951:Element or attribute at Xpath /SUBSCRIBER/PASSWORD exceeds maximum length
但如果您试图插入的 subscriber XML 文档的口令节点包含一个由单字符组成的值,则 Oracle XML DB 将不会拒绝,尽管定义一个单字符口令违反了口令节点的 subscriber XML 模式中定义的约束:
<xs:simpleType name="PasswordType">
<xs:restriction base="xs:string">
<xs:minLength value="2"/>
<xs:maxLength value="30"/>
</xs:restriction>
</xs:simpleType>
您在此处可以看到,根据以上约束,password 节点的值不能小于 2 个字符。
要避免插入无效数据,您需要显式调用 XMLType 类型的 schemaValidate 成员过程。执行此操作的方法之一是针对 XMLType 表定义一个 TRIGGER BEFORE INSERT,如下所示:
CREATE TRIGGER validSubscr BEFORE INSERT ON subscriberXML FOR EACH ROW
DECLARE
newdoc XMLType;
BEGIn
newdoc := :new.object_value;
xmltype.schemavalidate(newdoc);
END;
/
如果现在尝试插入一个 subscriber XML 文档(其口令节点包含一个由单字符组成的值),Oracle XML DB 将生成以下错误消息:
ERROR at line 1:
ORA-31154:invalid XML document
ORA-19202:Error occurred in XML processing
LSX-00221:"q" is too short (minimum length is 2)
ORA-06512:at "SYS.XMLTYPE", line 333
ORA-06512:at "SCOTT.VALIDSUBSCR", line 5
ORA-04088:error during execution of trigger 'SCOTT.VALIDSUBSCR'
正如此代码所示,将应用程序数据存储在基于 XML 模式的 XMLType 表中使您可以根据 XML 模式中指定的规则约束数据。尽管 JSF 为验证提供了广泛的支持,但在某些情况下使用 XML 模式约束仍很有用。实际上,XML 模式规范提供了比 JSF 更复杂的验证功能。例如,您可能需要创建一个验证程序,以确保用户在与相关组件对应的域中输入了合理的值。实际上,通过 JSF 解决此问题可能非常棘手,这是因为 JSF 使用的验证机制用于验证单个组件。相比之下,XML 模式定义语言不但使您可以为单个元素指定验证规则,而且还可以对多个元素的内容定义唯一性和引用约束。
返回到在您注册上面介绍的 subscriber XML 模式时 Oracle 自动创建的 subscriberXML 表,确保将 EMAIL 元素定义 PRIMARY KEY 约束,以确保 EMAIL 元素的值为 NOT NULL 并使存储在 subscriberXML 表中的 XML 文档唯一。为此,可以使用以下 SQL 命令:
ALTER TABLE subscriberXML
ADD constraint EMAIL_PRIMARYKEY
PRIMARY KEY (xmldata."EMAIL");
Table altered.
与 Oracle XML DB 交互。创建了数据库模式存储 subscriber XML 文档后,现在就可以返回到上个部分中介绍的 JSF 应用程序。正如前面指出的,要使应用程序与 Oracle XML DB 进行 XML 通信,只需重写 DBUtils 数据访问对象中使用的 SQL 查询。可以先更改 DBUtils 的 select 方法中的 sql 字符串,如下所示:
String sql = "SELECT EXTRACTVALUE(object_value, '//EMAIL')," +
"EXTRACTVALUE(object_value, '//PASSWORD')," +
"EXTRACTVALUE(object_value, '//NAME')," +
"EXTRACTVALUE(object_value, '//PHONE')," +
"EXTRACTVALUE(object_value, '//SUBSCRIBED')" +
"FROM subscriberXML WHERE EXTRACTVALUE(object_value, '//EMAIL')=" +
"'" + email + "'";
接下来,转到 insert 方法并按如下所示更改 SQL 字符串:
String sql = "INSERT INTO subscriberXML VALUES(XMLTYPE('<SUBSCRIBER>" +
"<EMAIL>" + email + "</EMAIL>" +
"<PASSWORD>" + password + "</PASSWORD>" +
"<NAME>" + name + "</NAME>" +
"<PHONE>" + phone + "</PHONE>" +
"<SUBSCRIBED>" + subscribed + "</SUBSCRIBED>" +
"</SUBSCRIBER>').createSchemaBasedXML('http://localhost:8080/public/subscriber.xsd'))";
最后,更改 DBUtils 的 delete 方法中的 SQL 字符串,如下所示:
String sql = "DELETE SubscriberXML WHERE EXTRACTVALUE(object_value, '//EMAIL')=" +
"'" + email + "'";
就这样了。请注意,您只需更改数据访问对象使用的 SQL 查询,以实现示例应用程序与 Oracle XML DB 的 XML 通信。如果现在对该示例应用程序进行测试,将发现它还是像以前那样运行,即从用户的角度来看这些更改并没有影响应用程序的行为。这是一个有关如何从使用 MVC 体系结构获得好处的示例 - 您可以只对一个应用程序层进行更改即可使用新特性来增强应用程序的功能。在以上示例中,要为此应用程序添加 XML 功能,您甚至不需要完全修改模型层。相反,您只需更改 DBUtils 数据访问对象使用的代表 SQL 查询的字符串。
进一步支持 XML
上个部分中介绍的应用程序的主要缺点是它仍然处理非 XML 数据。如前所述,该应用程序只使用 SQL/XML 查询将必要的信息移入和移出 XML 数据库。但您可以更进一步,使应用程序本身可以处理 XML 数据。再进一步,该应用程序不但可以将数据保存在 XML 中,而且还可以根据需要使用 XSLT 将应用程序中使用的 XML 结构转换为其他 XML 文档。
要实现以上提到的功能,您只需要修改示例应用程序的模型层 - 视图层(包括辅助 bean)将保持不变。首先修改 Login 模型 bean。示例代码清单 8 显示了经过必要校正后的 Login.java 中的代码。
对于清单 8 而言,需要注意的主要事项是,已将 Login 模型 bean 更改为使用 DOM 文档保存登录数据。其技巧是重写用于保存登录数据的属性的 set 和 get 方法,以使这些方法可以访问和操作 DOM 文档中存储的数据。第一步,在 Login 类的构造函数中创建一个 DOM 文档。然后,为该 DOM 文档添加一个元素。接下来,您重写所有用于访问和操作属性(用于保存登录数据)的访问器方法,以使它们可以操作 DOM 文档中存储的数据。您可以看到,在这个简单的示例中,您甚至不必导航 DOM 树的节点,这是因为它实际上仅包含一个节点,即 SUBSCRIBER。相反,您操作 SUBSCRIBER 节点的属性。最后请注意,Login.java 类使用 Oracle XML 开发人员工具包 (XDK) 10g 中的 Oracle XML Parser v2 库。如果使用 Oracle JDeveloper,则没有什么好担心的,这是因为它在默认情况下包含 Oracle XDK Java 库。此外,您随时都可以在 JDeveloper 中添加 XDK 库的新版本。要这样做,可以使用 Tools->Manage Libraries 对话框。在该对话框中您可以根据需要添加新库。
修改完 Login.java 文件后,您应转到 Subscriber.java 文件。示例代码清单 9 显示了 Subscriber.java 文件的代码,该文件已被修改以将订阅者的相关信息保存在 DOM 文档中。
最后,转到 DBUtils.java 文件,并按清单 10 所示对它进行修改。在查看其中显示的代码时,您可能会注意到仅修改了 insert 和 select 方法,而其他方法与清单 1 中所显示的 DBUtils 类代码相比并没有变化。注意有一点很重要,insert 和 select 方法负责在应用程序和数据库之间交换数据。
select 方法首先定义以下查询数据库的 SQL 查询字符串:
SELECT (SYS_NC_ROWINFO$).getStringVal() FROM subscriberXML
WHERE EXTRACTVALUE(object_value, '//EMAIL')=" + "'" + email + "'"
执行以上查询您就可以获取一个如下所示的字符串形式的行:
<SUBSCRIBER>
<EMAIL>josh@mail.com</EMAIL>
<PASSWORD>pswd</PASSWORD>
<NAME>Josh</NAME>
<PHONE>(650)475-3457>/PHONE>
<SUBSCRIBED>1</SUBSCRIBED>
</SUBSCRIBER>
接下来,使用 ResultSet 对象的 getBinaryStream 方法将从数据库检索的数据转换为 InputStream:
InputStream inStream=rsltSet.getBinaryStream(1);
然后,创建一个 XSLProcessor 对象,然后使用该对象将 ElemToAttr.xsl XSLT 样式表应用到从 InputStream 获取的 XML 数据源:
XSLProcessor processor = new XSLProcessor();
URL xslInput = new URL("http://localhost:8080/public/ElemToAttr.xsl");
XSLStylesheet stylesheet = processor.newXSLStylesheet(xslInput);
XMLDocumentFragment result = processor.processXSL(stylesheet, inStream, null);
结果,您将获得一个包含如下所示的 XML 数据的 XMLDocumentFragment:
<SUBSCRIBER EMAIL=" josh@mail.com" PASSWORD="pswd"
NAME="Josh" PHONE="(650)475-3457" SUBSCRIBED="1"/>
您可以看到,ElemToAttr.xsl 样式表将从数据库中获得的 SUBSCRIBER XML 节点转换为另一个 SUBSCRIBER XML 节点,然后将后用于应用程序中。以上示例假设您已经将 ElemToAttr.xsl 样式表文件存储在 Oracle XML DB 信息库的公共文件夹中。执行此操作的方法之一是使用 FTP 协议。第一步,创建了以下 ElemToAttr.xsl 文件:
<?xml version='1.0' encoding='utf-8' ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"><xsl:output method="xml"/>
<xsl:template match="/">
<xsl:for-each select="/SUBSCRIBER" >
<xsl:element name='{name()}'>
<xsl:for-each select="*" >
<xsl:attribute name='{name()}'>
<xsl:value-of select="."/>
</xsl:attribute>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
接下来,将 ElemToAttr.xsl 文件复制到用户主目录。(在 Windows 中,如果未指定主目录,则假设 %SystemDrive% 为主目录。)最后,使用标准的命令行 FTP 工具将该文件上载到 XML 信息库中,如下所示:
ftp> open localhost 2100
Connected to localhost.
220 localhost FTP Server (Oracle XML DB/Oracle Database 10g Enterprise Edition
Release 10.1.0.2.0) ready.
User (localhost:(none)):usr
331 pass required for USR
Password:
230 USR logged in
ftp> cd /public
250 CWD Command successful
ftp> put ElemToAttr.xsl
200 PORT Command successful
150 ASCII Data Connection
226 ASCII Transfer Complete
ftp:512 bytes sent in 0.00 Seconds 512000.00Kbytes/sec
ftp> quit
221 QUIT Goodbye.
返回 select 方法,使用 DOM 3.0 LS API 函数将 XSLProcessor 生成的 XMLDocumentFragment 转换为字符串:
DOMImplementation imp = subscriber.getSubscr_node().getOwnerDocument().getImplementation();
DOMImplementationLS impls=(DOMImplementationLS) imp;
LSSerializer domWriter = impls.createLSSerializer();
String subscrXML = domWriter.writeToString(result);
最后,从表示 SUBSCRIBER 节点(针对要使用的应用程序进行转换)的 subscrXML 字符串中获得一个 XMLElement:
DOMParser dp = new DOMParser();
dp.parse(new StringReader(subscrXML));
XMLDocument xd = dp.getDocument();
XMLElement elm = (XMLElement) xd.getDocumentElement();
subscriber.setSubscr_node((Element)elm);
与 select 方法(从数据库中获取 XML 数据,然后将该数据转换为 XML 以便应用程序使用)不同,insert 方法执行相反操作,即通过应用程序使用的 XML 数据生成要存储在数据库中的 XML。为执行此转换,insert 方法使用 AttrToElem.xsl 样式表,如下所示:
<?xml version='1.0' encoding='utf-8' ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml"/>
<xsl:template match="/">
<xsl:for-each select="/SUBSCRIBER">
<xsl:element name="{name()}">
<xsl:for-each select="@*">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
要将该文件上载到 Oracle XML DB 中的公共文件夹,可以使用前面介绍的 FTP 协议。
修改完 Login.java、Subscriber.java 和 DBUtils.java 文件后,可以对此示例应用程序进行测试:尽管在用户看来它仍像以前那样运行,但实际上您已经创建了一个支持 XML JSF 的应用程序(不仅与 Oracle XML DB 进行 XML 通信而且本身还使用 DOM 3.0 和 XSLT 的功能操作 XML 内容)。
结论
本文已经演示了几种在现有 JSF 应用程序实现 XML 支持的几种方法。如果 JSF 应用程序将 Oracle 数据库用作应用程序数据库的信息库,则使 JSF 应用程序与数据库进行 XML 通信的最简单方法是先创建必要的数据库对象(以存储 XML 格式的应用程序数据),然后使用 SQL/XML 函数操作具有 SQL 语句的 XML。该方法的缺点是应用程序本身仍操作非 XML 数据。因此,您可能希望继续开发支持 XML 的应用程序,以便它不仅可以与 Oracle XML DB 进行 XML 通信,而且它本身可以根据需要操作 XML。
Yuli Vasiliev 是一位主要致力于 Oracle 对象和 Oracle XML 技术的软件开发人员、自由作者和顾问。
将您的意见发送给我们
|