UML软件工程组织

 

 

构建基于Hibernate的servlet
 
2007-11-20 作者: 朱先忠编译 出处:51CTO.com
 

在上一篇,我们讨论了如何配置Tomcat 5.5的Context.xml文件,它能够建立Tomcat 5.5中的只读JNDI。通过这个JNDI,可以为你的Tomcat servlet提供数据源—无论在你的servlet中使用的是Hibernate还是原始JDBC。而且在上一篇中,相应的示例servlet使用了原始JDBC存取我们的MySQL数据库。

在本篇中,相应的示例servlet则使用Hibernate(不再是原始JDBC)存取MySQL数据库,但是将继续使用相同的Context.xml文件(没有作任何改变)。因此,在本文中,Hibernate仍将利用与原始JDBC(已在上篇中使用)相同的JNDI数据源。

一、Web.xml

让我们先从分析web.xml入手(我们假定,你已经理解web.xml的作用—用于标识和配置任何Java web应用程序组件,并且你已经理解了这个文件的格式):


xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
tomcatJndi.HibernateAppListener

Retrieval
tomcatJndi.Retrieval

RetrieveViaHibernate
tomcatJndi.RetrieveViaHibernate

InsertViaHibernate
tomcatJndi.InsertViaHibernate


Retrieval
/Retrieval

RetrieveViaHibernate
/RetrieveViaHibernate

InsertViaHibernate
/InsertViaHibernate

上面描述了web.xml的所有内容(适用于本系列中的三篇)。它包含了三个servlet:

  • Retrieval—在第一篇中利用原始JDBC的servlet。
  • RetrieveViaHibernate—本篇中的servlet,它查询与第一篇中相同的MySQL数据库,但现在使用的是Hibernate。
  • InsertViaHibernate—第三篇中的servlet,它在一个事务(从Hibernate中获取)边界上执行数据库插入操作。

另外,在靠近web.xml的顶部是一个xml listener元素—在本例中,它用于定义一个ServletContextListener。该监听器是一个类,当web应用程序已经准备好进行请求处理(例如在Tomcat服务器启动时)时,这个类被实例化。具体地说,在处理任何请求之前,应用程序将调用这个类的contextInitialized()方法来进行必要的初始化。例如,在第一次调用这个web.xml中的任何一个servlet之前,在我们的应用程序中会首先激活contextInitialized()方法。此后,这个方法将不被再次激活,除非应用程序重新运行或Tomcat重新启动。

二、ServletContextListener

在我们的例子中,我们想让ServletContextListener加载我们的HibernateUtil类,以便再由该类负责实例化一个静态Hibernate会话工厂。结果是,在我们的任何一个servlet收到Http请求之前,该Hibernate会话工厂已经准备好了。下面,让我们来分析一下我们的监听器(tomcatJndi.HibernateAppListener)的相应代码:

package tomcatJndi;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;
import org.apache.log4j.Logger;
import org.hibernate.Session;
public class HibernateAppListener implements ServletContextListener {
/*应用程序启动事件*/
public void contextInitialized(ServletContextEvent ce) {
Logger loggerConnect = Logger.getLogger("Connect");
try  {
loggerConnect.debug("In HibernateAppListener.contextInitialized");
Class.forName("tomcatJndi.HibernateUtil").newInstance();   
loggerConnect.debug("In HibernateAppListener, Class.forName for

tomcatJndi.HibernateUtil successful");
}
catch (Exception e)  {
loggerConnect.debug("In HibernateAppListener, Class.forName for

tomcatJndi.HibernateUtil throws Exception");
}
}
/*应用程序退出事件*/
public void contextDestroyed(ServletContextEvent ce) {}
}

注意上面调用的Class.forName(...).newInstance()方法,它试图确保指定的类(tomcatJndi.HibernateUtil)被加载,这样才能调用HibernateUtil中的静态初始化器。接下来,让我们分析一下HibernateUtil。

三、HibernateUtil

package tomcatJndi;
import org.hibernate.*;
import org.hibernate.cfg.*;
import org.apache.log4j.Logger;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
Logger loggerRoot = Logger.getRootLogger();
Logger loggerConnect = Logger.getLogger("Connect");
loggerRoot.debug("In HibernateUtil try-clause");
loggerRoot.error("In HibernateUtil try-clause");
loggerConnect.debug("In HibernateUtil try-clause via loggerConnect DEBUG*****");
//从hibernate.cfg.xml中创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
  注意,在此,静态SessionFactory(一个singleton)被buildSessionFactory()所实例化。接口org.hibernate.SessionFactory描述了一个Hibernate工厂—我们的代码能够从这个工厂中获得org.hibernate.session对象。在此,这个Hibernate会话(Session)是我们的应用程序中的重点;它负责实现与Hibernate的交互,并且要求它(Hibernate)完成任何功能。
注意,我们是简单地从Hibernate.org网站(http://www.hibernate.org)复制了这个HibernateUtil类。而且,该站点也提供了旧版本的HibernateUtil类(并有旧版本的Hibernate);只是更复杂些,而且加入了一个ThreadLocal实例。在当前的Hibernate.org站点上,你能够找到一篇文章“Hibernate会话与事务”(http://www.hibernate.org/42.html),其中提到:

“……注意:在网上存在许多种更为复杂的HibernateUtil类。然而,对于Hibernate 3.1来说,上面的代码才是唯一需要的代码。其它任何HibernateUtil类对于Hibernate 3.1都已过时……”

让我们假定你的servlet仅局限于几种类型的数据库查询—没有以任何方式更新数据库。于是,它可以以下列方式使用HibernateUtil类中提供的Hibernate SessionFactory:

Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
...[retrieve desired results via a Hibernate query]
session.getTransaction().commit();    //自动关闭会话

【注意】根据来自Hibernate参考文档的意见,即使是一个利用SessionFactory.openSession()实现的简单查询也应该使用一个Hibernate事务并实现事务提交。我们将遵循这一建议—它不会有任何负作用。
比如说,你还有一个servlet,它使用Hibernate执行某种数据库更新;那么,你的代码可能类似如下(与上面基本相同):

Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
Car bigSUV = new Car();
bigSUV.setManufacturer("Ford");
bigSUV.setModel("Expedition");
bigSUV.setYear(2005);
Driver jill = getDriverByName("Jill");
addCarToDriver(jill, bigSUV);
session.getTransaction().commit();  //自动关闭会话

四、Hibernate.cfg.xml

HibernateUtil.java中的方法org.hibernate.cfg.Configuration().configure()负责在classpath内查找Hibernate配置文件hibernate.cfg.xml。因此,下一步我们必须查找这个文件。它应该位于Tomcat应用程序的WEB-INF/classes目录(或其下的一个子目录)下的classpath中。下面的屏幕快照显示了hibernate.cfg.xml(以及log4j.properties文件,它也要求位于这个classpath下)的位置。

 

下面是我们在本系列文章中所使用的这个Hibernate配置文件的完整源码。


"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

java:comp/env/jdbc/Sampdb

true

org.hibernate.dialect.MySQLDialect

thread

org.hibernate.cache.NoCacheProvider

 

在此,我们需要解释一下这个配置文件中的有关内容;当然,还有一些没有出现在此文件中的内容。首先应该注意的是这个文件内的 —正是在此处,Hibernate将使用我们在第一篇中所配置的Context.xml中的已有JNDI数据源。如果我们不使用这个JNDI入口的话,那么,我们需要一个类似下列的hibernate.cfg.xml中的入口(而且,需要从这个文件中移除上面的connection.datasource入口):

com.mysql.jdbc.Driver
jdbc:mysql://localhost:3306/sampdb
root
sammy11

 

然后注意,入口 被设置为thread。这个入口基本上允许使用上面介绍的新的简化版本的HibernateUtil;这样以来,我们不必再维护我们自己的ThreadLocal以便为我们的Hibernate会话对象提供线程安全。

另外,你在这个文件中并没有看到一个xml属性(而不是一个子元素)。具体地说,你没有在session-factory元素中看到name属性。也即是,你没有看到类似如下的内容:

如果存在name属性—正如你在一个应用程序服务器环境下期望的那样,那么,HibernateUtil内的静态初始化器将调用BuildSessionFactory():

sessionFactory =new
Configuration().configure().buildSessionFactory();

于是发生下列情况:Hibernate试图把会话工厂入口安装到JNDI中。这在一个应用程序服务器情况下的确不错,但是在Tomcat只读版本的JNDI情况下却不是这样(将抛出一个异常)。在一个应用程序服务器中,对于当前版本的HibernateUtil,第二种可能性的变化是:getSessionfactory()方法将从JNDI中(而不是从静态变量中)检索SessionFactory引用。

下面是你在这个配置文件中所没有看到的其它若干属性:
  • transaction.factory_class—JDBCTransactionFactory的缺省值被自动地应用于这个配置。如果你希望的话,你也可以包括transaction.factory_class属性,并显式地指定其(缺省的)值JDBCTransactionFactory。在一个应用程序服务器环境下,你也可以包括这个属性—指定其值为JTATransactionFactory。注意,通常情况下,你不能使用Tomcat来实现赋值JTATransactionFactory(除非你使用一种第三方JTA实现库,而这明显不会是Tomcat发行的一部分)。
  • jta.UserTransaction—一个应用程序服务器环境,通常你将把它设置为java:comp/UserTransaction,从而支持一个UserTransaction的JNDI查询。基于上面所陈述的理由,你也不能使用Tomcat来实现这一功能。
  • transaction.manager_lookup_class—在一个应用程序服务器环境下,这个属性可以用于显式地标识JTA实现的提供者。而通常,这个属性是无法与Tomcat联用的。

顺便说一下,简单地把jar文件jta.jar放到你的Tomcat上下文classpath下并不会使你支持JTA事务。事实上,它仅为你定义了接口UserTransaction,但是却没有提供这个接口的相应实现。注意,你可以在应用程序服务器中实现这个接口,或在一种第三方开源工具(例如JOTM)中实现(注意,JTA实现并没有随同Tomcat一起发行)。

五、Hibernate映射文件

在上面展示的中配置文件Hibernate.cfg.xml中,列出了如下两个映射文件:

  • tomcatJndi/Car.hbm.xml
  • tomcatJndi/Driver.hbm.xml

这些映射文件必须出现在应用程序的classpath中,它们位于文件夹WEB-INF/classes下的包目录下,如下图中的屏幕快照所示:

 

如你所见,一个特定的映射文件也会出现在与相应的Java类相同目录下,例如Car.hbm.xml和Car.class。
在展示我们所使用的两个映射文件内容之前,让我们先来解释一下本文相应的应用程序及其简单的数据模型。我们的应用程序是围绕一个小型家庭展开的:丈夫—Jack;妻子—Jill;儿子—James;他们家共拥有四辆汽车。他们家之所以有四辆车是因为,其中的一辆车(运动跑车)经常需要维修。另外,James开车时总是易出事故—当然,他是永远不会负责的。而且,有意思的是,当他驾驶那辆运动跑车时总是易发生事故。注意,这个家庭成员中的任何一位都不允许别人开自己的车。结果是,在车主(司机)与汽车之间建立了一种“一对多”的关联关系(在大多数家庭中,这种关联很可能是“多对多”,但是在我们的示例家庭中却是“一对多”)。

因此,上面的数据模型应该是:Driver.hbm.xml文件必须反映出一个司机有可能潜在地驾驶的汽车的集合。而对于文件Car.hbm.xml,却不需要存在类似的集合。所以,Driver.hbm.xml更复杂一些。首先让我们来看一下较简单的文件—Car.hbm.xml:


"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

让我们来分析一下相应的Java源文件:

package tomcatJndi;
public class Car {
private Long car_id;
private String manufacturer;
private String model;
private int year;
public Car() {}
public Long getId() {
return car_id;
}
private void setId(Long car_id){//注意,private类型的可见范围
this.car_id = car_id;
}
public String getManufacturer() { return manufacturer;  }
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getModel() { return model; }
public void setModel(String model) {  this.model = model;  }
public int getYear() { return year; }
public void setYear(int year) {  this.year = year; }
}

下面是MySQL的create table语句。其中,每一条汽车记录都必须包含指向车主(司机)的外键(foreign key):

create table car(
car_id bigint(20) PRIMARY KEY not null auto_increment,
manufacturer varchar(40),
model varchar(40),
year int,
fk_driver_id bigint(20),
FOREIGN KEY (fk_driver_id) REFERENCES driver (driver_id)
)

如上面所述,Driver.hbm.xml映射文件(及相应的Java源文件Driver.java)更复杂一些—由于存在“一对多”的映射要求。

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

上面的文件中的 元素描述了Java集合类型—set。这个xml元素及其子元素指定表格car包含一个外键列—fk_driver_id,由它来引用处于“一对多”关系中的“一”端的司机。

下面是相应的Java源文件:

package tomcatJndi;
import java.util.Set;
public class Driver{
private Long driver_id;
private String name;
private int age;
private Set carsOwned;
public Driver() {}
public Long getId() {  return driver_id; }
private void setId(Long driver_id) {//注意,private类型的可见范围
this.driver_id = driver_id;
}
public String getName() {   return name;    }
public void setName(String name) {   this.name = name;  }
public int getAge() {   return age;    }
public void setAge(int age) {    this.age = age;    }
public Set getCarsOwned() {   return carsOwned;    }
public void setCarsOwned(Set carsOwned) { this.carsOwned = carsOwned; }
}

下面是MySQL创建表格的相应语句:

create table driver(
driver_id bigint(20) PRIMARY KEY not null,
name varchar(40),
age int(11)
)

 

至此,我们建立了上面所要求的在司机及其驾驶的汽车之间的一对多的关系。

最后,下面的示例servlet使用Hibernate查询这两个表格:

package tomcatJndi;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import org.apache.log4j.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.Session;
import org.hibernate.Query;
import org.hibernate.cfg.Configuration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RetrieveViaHibernate extends HttpServlet {
public RetrieveViaHibernate() { super();}
public void doGet(HttpServletRequest request,

HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("");
out.println("");
out.println("  ");
out.println("  ");
out.print("    This is BRAND NEW ");
out.print(this.getClass());
out.println(," using the GET method
");
doQuery(out);    //使用Hibernate存取Driver和Car表
out.println("  ");
out.println("");
out.flush();
out.close();
}
private void doQuery(PrintWriter out) {
  System.out.println("In doQuery");
try  {
//读取hibernate.cfg.xml并且准备好hibernate
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
String SQL_QUERY ="from Driver driver";
Query query = session.createQuery(SQL_QUERY);
for(Iterator it=query.iterate(); it.hasNext(); )  {
Driver driver=(Driver)it.next();
out.println("Driver name: " + driver.getName() + ," " );
 out.println("Driver age: " + driver.getAge() + "
" );
 Set carsOwned = (Set)driver.getCarsOwned();
 for (Iterator itOwned = carsOwned.iterator(); itOwned.hasNext(); ) {
     Car carOwned = (Car)itOwned.next();
     out.println("   Manufacturer: " + carOwned.getManufacturer() + ," " );
     out.println("Model: " + carOwned.getModel() + ," " );
     out.println("Age: " + carOwned.getYear() );
     out.println("
");
 }
}
session.getTransaction().commit();//自动地关闭会话
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
finally{}
}
}

注意,上面的这个servlet从HibernateUtil的SessionFactory中取得当前会话(Session),并启动一个Hibernate事务,并在上面例程的结尾处提交这个Hibernate事务—尽管仅执行了一个查询(并非一个数据库更新)。

在这个servlet示例中,Hibernate实现了几个面向对象功能。首先,请注意,并不象在第一篇中所使用的原始JDBC,上面的Hibernate数据库查询实际上返回的是Driver和Car类的实例(如在Driver.java,Car.java和相应的hbm.xml映射文件中所定义的)。注意,上面的代码并没有返回原始数据结果集(ResultSet)。

其次,让我们来分析一下上面代码中的查询:“from Driver driver”(假定使用了“select *”语法,但并不需要)。这个查询中没有涉及Car表格,但是却能返回可用的汽车。实际上,在这个查询中的单词“Driver”并不是指Driver表格—它指的是Driver Java类,而这个类的确包括一个被一个司机所拥有和驾驶的汽车集合。因此,查询将直接返回你想使用的Java对象实例—根本不需要从结果集(ResultSet)转换到目标对象。而且,从一个Java对象类型(司机)到被引用的Java对象集合(汽车对象)的引用都是预先填充的。

六、深入讨论

对于上面的servlet,我们不妨做进一步讨论。首先请注意下列一行临近方法doQuery()顶部的代码:

Session session = HibernateUtil.getSessionFactory().openSession();

其实,我们也可以选择使用下列的语法(但是对于Tomcat的情况,我们却永远不能这样做):

Session session = HibernateUtil.getSessionFactory().getCurrentSession();

那么,为什么对于Tomcat时我们却不能这样做呢?在openSession()和getCurrentSession()之间的区别在于:在每次使用openSession()时,它都提供一个新的Hibernate会话。这正是我们想在我们的servlet中实现的。而相比之下,getCurrentSession则试图把一个Hibernate会话关联到一个特定的线程(Singleton-per-Thread模式)—Hibernate通过使用一个嵌入式(隐藏的)ThreadLocal实现了这一点。遗憾的是,Tomcat维持了一个线程池,并且在一个特定的Http请求使用完它以后再次重用这个给定的线程。因此,一个新的Http请求可以接收一个以前使用的线程—而它已经有一个Hibernate会话与之关联了(经由ThreadLocal);这样以来,当getCurrentSession()必须接收一个新的会话时有可能碰巧收到一个与之无关的Hibernate会话。从逻辑上讲,我们可以有一个新的Http会话和一个新的线程;但是,在物理上,我们却在重用一个现有的线程。这样以来,Tomcat 5.5.x和Hibernate 3.1都有可能会使对方感到“疑惑”。

通过使用SessionFactory.openSession,我们选择完全绕过这个问题,并且避免在一种Tomcat环境下使用SessionFactory.getCurrentSession。

【注意】上面我们所描述的冲突很容易进行试验。为此,我们可以建立一种Tomcat 5.5环境—仅允许极少数目的并发进程—比如说3或4个(你可以通过位于Tomcat Root\conf\server.xml中的一个入口来实现这一目的。也就是,把这个文件中可用的 元素的maxThreads属性设置为3或4)。然后,创建几个不同的简单的业务事务(对话型事务或长期运行的事务)—它们将跨越Http请求进行通讯。试着把一些信息保存在一个ThreadLocal中(可以是任何信息,而且不需要与Hibernate有关;可以是进行一些跟踪/日志,用于显示线程ID)。你将会看到,Tomcat线程池最终会对另一个业务事务循环利用线程ID,从而使不恰当地存取ThreadLocal内容的情况发生。

最后,我希望你对ThreadLocal作些小研究。

七、小结

本篇向你展示了如何配置Tomcat 5.5和Hibernate 3.1以便于二者相互配合使用;同时,还讨论了如何在这种环境下实现面向对象的查询。

在接下来的最后一篇中,我们将向你展示如何在此环境下实现数据库更新;同时,还将通过一个购物车的示例来讨论有关多Http请求(Multi-Http-Request)的业务事务问题。

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号