在“Creating Web Applications with the Eclipse WTP”(http://jdj.sys-con.com/read/152270.htm)一文中,我们使用Eclipse Web Tools Project、Tomcat应用服务器和MySQL数据库服务器,创建了一个Web应用程序。尽管该应用程序(DBTest)不错,但是也存在一些局限性:
幸运的是,这些问题可以通过两种有趣的解决方案得以解决。第一个问题可通过使用开源Struts框架解决,该框架通过将模型动作映射到一个简单配置文件中的视图组件(比如JSP),从而分离应用程序的模型、视图和控制器。
第二个问题可使用提供Java和关系数据库持久性的框架来解决。Hibernate框架在对象和数据库表之间提供了一个强大的高性能映射引擎。本文将使用下列技术:
我们再扼要重述一下上次我们做了些什么。该基本Web应用程序实现了下列用例:
该系统使用通用servlet/jsp编程模型、MySQL数据库以及Tomcat应用服务器实现。系统域模型由Customer(顾客)和Order(订单)两个类表示(参见图1)。
创建了两个对应的数据库表CUSTOMERS和ORDERS来表示这些对象所持有的数据。还创建了4个负责执行上述用例的数据库命令类,以及四个作为控制器的Servlet,来收集用户输入信息,调用这些命令,并将响应转发给适当的JSP。CommandExecutor类负责使用Tomcat连接池处理数据库连接。
使用File-Import选项并选择要导入的WAR文件,将DBTestWAR文件(http://java.sys-con.com/read/152270.htm)导入Eclipse工作空间。如果工作空间中没有DBTest项目,上述操作就可以了。如果工作空间中已经有了DBTest项目,在Navigator视图的已有项目上右击,然后选择复制和粘贴,保存现有项目。当提示输入新项目名称时,选择DBTestStruts作为新项目名,以便不会覆盖现有项目。现在,为添加Struts支持,必须将下列文件复制到WEB-INF\lib文件夹:struts.jar、commons-lang.jar、commons-collections.jar、commons-beanutils.jar、commons-validator.jar、commons-logging.jar、commons-digester.jar、commons-fileupload.jar。
上述所有文件均可从Struts Web站点下载获得,这些文件包含Struts framework以及相应的Apache Commons包,这些包是处理诸如国际化、集合操作、实用工具、验证、日志记录、digester以及文件上传操作等特性所必需的。上述均为Struts支持的组件。本文不会用到上述所有功能,但是Struts依赖于其中的许多功能,例如,在解析Struts配置文件时就会大量用到digester功能。当需要使用日志记录、文件上传等服务时,这些功能就会派上用处。
因此,要将下列文件添加到WEB-INF文件夹中:struts-config.xml、struts-bean.tld、struts-html.tld、struts-logic.tld、struts-nested.tld、struts-template.tld、struts-tiles.tld。
其中struts-config.xml文件最为重要,该文件是Struts框架的主要配置文件,包含有所有的动作映射、数据源、插件等的定义。参见清单1中的例子。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <!-- Data Sources --> <data-sources> </data-sources> <!-- Form Beans --> <form-beans> </form-beans> <!-- Global Exceptions --> <global-exceptions> </global-exceptions> <!-- Global Forwards --> <global-forwards> </global-forwards> <!-- Action Mappings --> <action-mappings> </action-mappings> </struts-config>
作为Struts标签库定义文件,TLD文件可在JSP内部使用,执行各种有用操作,比如HTML呈现、逻辑处理或Tiles支持功能。这些文件可以从Struts 1.1发行版中获得。
接下来,需要对Web部署描述符(web.xml)进行修改,指定Struts配置servlet的位置和相应参数。应将清单2中的代码片断添加到web.xml文件。
<servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>2</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>2</param-value> </init-param> <init-param> <param-name>validate</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>
清单2中的标签定义Action Servlet的位置。Action Servlet是Struts主控制器,负责处理动作的生命周期,并将其映射到forward对象,此类对象由动作返回,并拥有两个字段:名称及路径(通常为JSP文件的URL)。在这里指定struts-config.xml文件的位置,以及用于调试和验证的参数。该servlet在启动时加载,其加载顺序为1,即第一个加载的servlet。如果在调用URL中检测到*.do,就调用该servlet。
现在,我们必须将现有的servlet类转换为动作类,并在struts-config.xml中为其定义适当映射。为了简化这一过程,我们为所有动作提供一个抽象超类,请参见清单3。
package actions; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; // This abstract class overrides Struts action class execute method and provides abstract // performAction method to be overwritten by sub-classes.This helps us isolate some // common error processing into one place, rather than having it several places in the // sub-classes. public abstract class AbstractAction extends Action { public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Define action errors and forward ActionErrors errors = new ActionErrors(); ActionForward forward = new ActionForward(); try { forward = performAction(mapping, form, request, response); } catch (Exception e) { // Report the error using the appropriate name and ID. errors.add("name", new ActionError("id")); } // If a message is required, save the specified key(s) // into the request for use by the <errors> tag. </errors>
在该类中,我们实现了Struts 1.1框架对动作默认调用的execute方法。它在其performAction()方法中处理逻辑,并根据是否有异常抛出来转到成功或失败的处理程序。相应地,必须在Struts配置文件(struts-config.xml)中定义每一动作的成功和失败映射。
具体动作的创建非常容易。可使用Eclipse向导,创建动作类。确保将AbstractAction选择为超类,并复选Inherited abstract methods框(请参见图2)。
这将自动生成带有performAction()方法的CreateCustomerAction类。复制CreateCustomerServlet doGet()方法的内容(参见前期文章:http://java.sys-con.com/read/152270.htm),按清单4所示进行修改,并将其粘贴到performAction()。
if (!errors.isEmpty()) { saveErrors(request, errors); // Forward control to the appropriate 'failure' URI forward = mapping.findForward("failure"); } else { // Forward control to the appropriate 'success' URI if (forward == null) { forward = mapping.findForward("success"); } } // Finish with return (forward); } /** * Perform appropriate actions as defined by the business logic * * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public abstract ActionForward performAction ( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception; } // create customer - get parameters first String first_name = request.getParameter("first_name"); String last_name = request.getParameter("last_name"); String address = request.getParameter("address"); int cust_id = Math.abs((int)System.currentTimeMillis()); // create new customer object Customer c = new Customer(); c.setId(cust_id); c.setFirstName(first_name); c.setLastName(last_name); c.setAddress(address); // construct and execute database command DatabaseCommand command = new CreateCustomer(c); int rows = (Integer)CommandExecutor.getInstance().executeDatabaseCommand(command); return mapping.findForward("customer_created");
显而易见,非Struts代码和Struts代码的惟一区别在于没有使用下述代码:
RequestDispatcher rd = getServletContext(). getRequestDispatcher("/customer_created.jsp"); rd.forward(request, response);
而使用了下述更为简单的代码:
return mapping.findForward("customer_created");
我们不必再在我们的代码中对JSP名称进行硬编码。相反,我们使用customer_created引用,该引用将在struts配置文件中被解析。在<action-mappings>标签中,添加清单5中的片断。
<action path="/CreateCustomer" type="actions.CreateCustomerAction"> <forward name="customer_created" path="/customer_created.jsp"> </forward> <forward name="failure" path="/failure.jsp"> </forward> </action>
在这个例子中,/CreateCustomer将作为调用该动作的URI。定义两个forward():指向customer_created.jsp的customer_created,和指向显示错误的failure.jsp的failure。有必要建立应用程序公共错误页面,我们现在就来创建,请参见清单6。
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"pageEncoding="ISO-8859-1"%> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Failure has occurred</title> </head> <body> <B>Errors occurred</B> <html:errors/> </body> </html>
在本文件中,我们使用Struts HTML标签库显示捕获的错误。
同样,我们将其他servlet转化为Struts动作。别忘了更改index.html和其他文件中的URL,并在动作调用中添加".do"后缀。要在Tomcat服务器中部署新应用程序,使之与旧应用程序并存,需要将引用DTBest改为DBTestStruts。同时,将web.xml中的显示名由DBTest改为DBTestStruts。
从DBTest应用程序源代码和Web部署描述符中去掉旧的servlet定义,只留下动作和动作servlet定义。如欲删除servlet包,只需在servlet包上右击,选择Delete,当出现确认提示时,选择Yes。
要将新的应用程序部署到Tomcat,打开其控制台http://localhost:8080/manager/html ,部署新的WAR文件。确保DBTest.xml已复制到DBTestStruts.xml,所有DBTest引用已改为DBTestStruts。
但是,另外一个问题是,原始解决方案的SQL直接在命令类中进行了硬编码。在文章的下一节,将通过流行的Hibernate框架解决这个问题。该框架支持Java和关系数据库之间的持久性。
Hibernate框架在下列领域提供帮助:
如欲在本应用程序中使用Hibernate,请下载Hibernate框架的最新版本(当前最新版本为3.0)。解压后将hibernate3.jar文件放到应用程序的WEB-INF\lib目录下。根据J2EE标准,这将自动将JAR文件添加到应用程序的编译时和运行时类路径。必须将dom4j.jar添加到WEB-INF\lib目录下,dom4j.jar文件也可以从Hibernate网站下载获得。这是获得Hibernate配置文件所需的XML解析器支持所必需的。
现在我们必须在应用程序层配置Hibernate。在Eclipse的Java Source文件夹中,创建hibernate.cfg.xml配置文件。这样,部署好应用程序之后,该文件将自动进入WEB-INF\classes下的应用程序类路径。
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.datasource">java:comp/env/jdbc/TestDB</property> <property name="show_sql">true</property> <property name="dialect">org.hibernate.dialect.MySQLDialect</property> <!-- Mapping files --> <mapping resource="hibernate.mapping.xml"/> </session-factory> </hibernate-configuration>
清单7中显示的文件包含对下列各项的引用:
hibernate.mapping.xml文件包含了应用程序使用的域对象和相应关系表的实体之间的映射信息。该文件应该与hibernate.cfg.xml文件一起,共同放在Java Source下的同一目录。
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="domain.Customer" table="CUSTOMER" lazy="false"> <id name="id" column="ID"/> <property name="firstName" column="FIRST_NAME"/> <property name="lastName" column="LAST_NAME"/> <property name="address" column="ADDRESS"/> </class> <class name="domain.Order" table="ORDERS" lazy="false"> <id name="id" column="ID"/> <property name="custId" column="CUST_ID"/> <property name="datePlaced" column="DATE_PLACED"/> <property name="orderAmount" column="AMOUNT"/> </class> </hibernate-mapping>
清单8中的文件包含了两个类标签,在标签中,域类Customer和Order分别被映射到相应数据库表中,每个实例变量被映射到数据库中的一个列。lazy是一个值得一提的属性。我们已经明确将其设定为false,这样做的原因是,当lazy设定为默认的ture时,只有在访问特定方法时,才会从数据库读取数据。比如,在进行SQL查询时,只有在调用getFirstName()而不是预读全部客户数据时,才会从数据库读取数据。当读取大量数据,或希望推迟进行开销昂贵的数据库操作时,这样做或许会带来好处。在我们的例子中,我们只读取少量客户信息,不会在以后进行其他操作或数据库访问。例如,如果在数据库会话关闭后,我们仍然试图调用lazy方法,Hibernate就会抛出异常。
配置完Hibernate之后,我们必须对CommandExecutor类稍作修改以使用该框架,并删除硬编码的SQL代码。该类被作为单元素,用于存储数据源并获得数据库连接。首先,我们添加实例变量以存储Hibernate会话工厂。Hibernate会话工厂类似于数据源,不同的是,我们从Hibernate会话工厂获得的不是数据库连接,而是Hibernate数据库会话。实例变量看上去类似于:
private SessionFactory sessionFactory = null;
接下来,为实例变量创建一个访问方法,请参见清单9。这样做可以保存封装的对象状态,同时允许使用延迟初始化技术(只在需要时访问数据)。
// get hibernate session factorypublic SessionFactory getSessionFactory() { if (sessionFactory == null) { sessionFactory = new Configuration().configure().buildSessionFactory(); } return sessionFactory; }
在该方法中,我们首次初始化一个会话工厂。Hibernate的Configuration对象被用于从类路径读取配置文件,从而初始化框架。
在开始使用Hibernate进行数据库操作之前,我们已经使用了executeDatabaseCommand()方法:使用需要executeDatabaseOperation()方法的DatabaseCommand接口。由于现在希望使用Hibernate,我们将分别介绍使用DatabaseCommand接口和CommandExecutor单元素对象的其他方法。这种方法通过Hibernate框架执行所有的数据库操作(请参见清单10)。
// execute a particular hibernate commandpublic Object executeHibernateCommand(DatabaseCommand c) throws Exception { Session session = null; try { session = getSessionFactory().openSession(); Object o = c.executeHibernateOperation(session); return o; } catch (SQLException e) { throw e; } finally { if (session != null) { session.flush(); session.close(); } } }
看上去,它和executeDatabaseCommand()方法非常相似,不同之处在于我们使用的是Hibernate会话对象,而不是普通的JDBC连接。接下来,将下述存根方法添加到DatabaseCommand接口:
public Object executeHibernateOperation(Session session) throws SQLException;
在向接口添加新方法后,Eclipse工作台中所有实现该接口的类均被标上红色标记,因为这些实现该接口的类还必须实现它所需的所有方法。我们有四种实现数据库命令接口的类。
public class CreateCustomer implements DatabaseCommand
public class CreateOrder implements DatabaseCommand
public class ListCustomers implements DatabaseCommand
public class ListCustomerOrders implements DatabaseCommand
因此,我们必须向这四种类中分别添加executeHibernateOperation()实现方法。首先我们来看看CreateCustomer类。该类的executeDatabaseOperation()方法如清单11所示。
public Object executeDatabaseOperation(Connection conn) throws SQLException { PreparedStatement sta = conn.prepareStatement ("INSERT INTO CUSTOMER (ID, FIRST_NAME, LAST_NAME, ADDRESS) VALUES (?, ?, ?, ?)"); sta.setInt(1, cust.getId()); sta.setString(2, cust.getFirstName()); sta.setString(3, cust.getLastName()); sta.setString(4, cust.getAddress()); int rows_updated = sta.executeUpdate(); sta.close(); return new Integer(rows_updated); }
该方法比较冗长,并且要求编码的开发人员对JDBC有所了解:比如,如何创建和执行预处理语句。此外,如果想要将数据库从MySQL转变为其他类型,开发人员需要重新编写SQL,因为不同数据库的SQL也许各不相同。使用Hibernate,只需要更改hibernate.cfg.xml配置文件中的SQL对话。清单12列出了我们的相应executeHibernateOperation()方法。
/** * Execute Hibernate operation */ public Object executeHibernateOperation(Session session) throws SQLException { session.save(cust); session.flush(); return 1; }
在清单12中,我们告诉会话对象将类保存到数据库中。不需要SQL,不需要JDBC知识,不必对列和数据库表名称进行硬编码。如果必须更改表或列名称,我们也不必重新编写应用程序中的多行代码。Hibernate知道如何保存对象,不管该对象是否已经存在于数据库中。执行INSERT或UPDATE操作,Hibernate可以以乐观方式进行检查(即尝试执行UPDATE,如果失败,则执行INSERT),或以悲观方式进行检查(即执行SELECT,检查是否存在该行,如果存在,即执行UPDATE,否则执行INSERT)。命令执行后,将刷新会话并确保所有数据库命令都已经被及时执行,框架缓冲区中没有任何剩余。对CreateOrder类执行类似的操作。
首先,我们在JDBC版本中处理的两个操作,CreateCustomer和CreateOrder,是数据库插入操作。然而,想要让应用程序工作,还必须处理数据库查询。我们使用ListCustomers和ListCustomerOrders命令来实现这一功能。我们先来看看如何获取用户列表。
public Object executeDatabaseOperation(Connection conn) throws SQLException { // List customers in the database ArrayList<customer></customer> list = new ArrayList<customer></customer>(); Statement sta = conn.createStatement(); ResultSet rs = sta.executeQuery("SELECT ID, FIRST_NAME, LAST_NAME, ADDRESS FROM CUSTOMER"); while(rs.next()) { Customer cust = new Customer(); cust.setId(rs.getInt(1)); cust.setFirstName(rs.getString(2)); cust.setLastName(rs.getString(3)); cust.setAddress(rs.getString(4)); list.add(cust); } rs.close(); sta.close(); return list; }
清单13包含了大量JDBC调用。首先,创建SQL语句,并使用硬编码的列和表名执行该语句,接下来,检查数据库查询的ResultSet,根据从数据库表读取的行数据,构建一个用户域对象,同时记住列顺序或名称。这些操作都易于出错,同时一旦需要更改数据库表,这些操作都难以维持。而Hibernate正好可以解决这些问题。Hibernate引入了名为Hibernate Query Language (Hibernet查询语言,HQL)的全新语言,在这种语言中,用户不用查询数据库表,只需要查询对象。清单14列出了我们使用的executeHibernateOperation()方法。
/** * Execute Hibernate select operation */ public Object executeHibernateOperation(Session session) throws SQLException { Query q = session.createQuery("from customer in class domain.Customer"); Iterator iter = q.iterate(); ArrayList<customer></customer> list = new ArrayList<customer></customer>(); while(iter.hasNext()) { Customer cust = (Customer)iter.next(); list.add(cust); } return list; }
同样,这种方法看起来也非常类似于前面的方法。然而,也存在一些显著的区别。这里我们用到了众多Hibernate对象。第一个对象就是Query类。通过该类,用户可以使用HQL(通过createQuery方法)或普通SQL(通过createSQLQuery方法)创建并执行数据库查询。我们来看看这里用到的HQL:
from customer in class domain.Customer
基本上我们选择了domain.Customer类中的customer变量所识别的所有客户。获取查询迭代器就可以让我们将customers放置到任何集合。在此处,就是ArrayList<Customer>。
可以为ListCustomerOrders类编写一个非常相似的方法,但是HQL要复杂一些,如清单15所示。
/** * Execute Hibernate query operation */ public Object executeHibernateOperation(Session session) throws SQLException { Query q = session.createQuery("from order in class domain.Order where order.custId = '" + this.cust_id + "'"); Iterator iter = q.iterate(); ArrayList<order></order> list = new ArrayList<order></order>(); while(iter.hasNext()) { Order order = (Order)iter.next(); list.add(order); } return list; }
在本例中,我们在查询中使用where子句。注意,在where子句中,我们可以使用Order类的实例变量(custId)进行查询。其语法与Java的句点表达法相似。
最后,我们必须更新Struts动作类,以调用executeHibernateOperation()方法,而不是executeDatabaseOperationMethod()方法。使用Eclipse编辑器可以轻松完成这一任务。
使用WTP工具,我们可以轻松地将项目导出到WAR文件,并部署在Tomcat中。选择DBTestStruts Web项目,再从File菜单中选择Export。出现提示信息后,选择WAR文件,指定文件名,WAR文件就可以部署到Tomcat了。
本文旨在探讨如何在使用Eclipse和WTP工具开发的简单Web应用程序中集成Struts和Hibernate支持。这些框架有助于改进应用程序的可维护性、代码的可重用性以及代码的可读性。