返回介绍

把所有组件装配在一起

发布于 2025-04-21 21:42:16 字数 16687 浏览 0 评论 0 收藏

如果我们不知道如何创建一个 Spring ApplicationContext 和运行我们的代码,所有这些 Spring 配置也就没什么用途。在本节,我们打算调整前面的示例 CreateTest、QueryTest、AlbumTest 类,实现一个 Test 接口,不是直接从命令行运行它们,而是创建一个 TestRunner 来执行这些从 Spring ApplicationContext 获取的测试对象。

Transactions:测试接口

本章稍后会编写一个名为 TestRunner 的类,这个类会知道怎么从 Spring ApplicationContext 中取回一个 bean,这个 bean 应该实现 Test 接口,需要调用执行该 bean 的 run()方法。TestRunner 使用的 bean 源于对前面几章的 CreateTest、QueryTest 以及 AlbumTest 的修改调整。为了支持这种新的运行方式,我们让它们分别实现一个公共的 Test 接口,如例 13-9 所示。

例 13-9:Test 接口

package com.oreilly.hh;

import org.springframework.transaction.annotation.Transactional;

/**

*A common interface for our example classes.We'll need this

*because TestHarness needs to cast CreateTest, QueryTest, or

*AlbumTest to a common interface after it retrieves the bean

*from the Spring application context.

*/

public interface Test{

/**

*Runs a simple example

*/

@Transactional(readOnly=false)

public void run();

}

这个 Test 接口用于为 TestRunner 提供一个公共接口,它也为我们添加 Transactional 标注提供了一种方便的方法。Transactional 标注负责将一个 Session 对象绑定到当前线程,启动一个事务处理,如果方法正常返回,则提交事务,如果有异常抛出,则回滚事务。

有关 @Transactional 标注的更多信息,请参阅附录 D。

如何激活事务标注

为了打开 Transactional 标注的处理,需要在我们的 applicationContext.xml 文件中添加以下一段配置信息:

<!--enable the configuration of transactional behavior based on annotations-->

<tx:annotation-driven transaction-manager="transactionManager"/>

<bean id="transactionManager"

class="org.springframework.orm.hibernate3.HibernateTransactionManager">

<property name="sessionFactory">

<ref local="sessionFactory"/>

</property>

</bean>

tx:annotation-driven 元素简单地激活了 Transactional 标注,将它指向一个 PlatformTransaction-Manager。HibernateTransactionManager 是 Spring Framework 的 PlatformTransactionManager 接口的一个实现。它负责将来自会话工厂的一个 Hibernate Session 对象用会话工厂 Utils 绑定到当前线程(Thread)。因为我们的 DAO 对象都继承自 HibernateDaoSupport,也都使用 HibernateTemplate,所以这些持久化对象就能够参与到事务管理当中,并获得相同线程上的会话对象。这不仅仅是因为事务处理的需要,在使用延迟加载的关联时,它也是必需的。Transactional 标注可以确保在执行标注过的方法时,让同一会话对象保持打开,并绑定到当前线程。如果没有这个标注,Hibernate 就会为每个需要会话的操作都创建一个新的会话对象,你也就不能取回原来用 Hibernate 检索到的对象上的任何关联。

为什么会这样?让我们回顾一下第 5 章介绍过的主题。在 Hibernate 3 中,映射对象之间的关联默认都使用延迟加载。除非为某个类或关联显式地改变这种默认加载方式,直到你访问某个特定对象时,才真正从数据库中检索相关联的对象。例如,如果从数据库中检索回一个 Album 对象,直到你调用 album.getAlbumTracks()方法时,才会从数据库中再检索回 AlbumTrack 的 List 列表对象。为此,Hibernate 要做两件事:

1.Hibernate 返回一个“代理”对象,用于代表还没有加载的对象。当检索 Track 对象时,返回的对象是一个 Track,不过相关联的集合(例如 track.getArtists())则是 PersistentSet 的一个实例。

2.PersistentSet 由 Hibernate 负责管理,你通常不需要考虑这一对象。与这里的讨论相关的是,它是 PersistentCollection 的一个实现,包含了对会话对象的一个引用。换句话说,在按照需要而获取相关联的 Artists 时,涉及的是 PersistentSet。你可以取回一个 Track 对象,但是在调用 track.getArtists()之前,并不会取回任何 Artist 对象;而且即便取回关联的 Artist 对象,也得再次通过会话对象。

只有当 PersistentSet 引用了一个活动的会话对象时,延迟加载关联才有效果。如果没有一个打开的会话,这时再试图访问延迟加载的关联时,就会抛出一个异常。在 Web 应用程序中,可以使用 Spring 的 OpenSessionInViewFilter 之类的东西来确保在一个请求中持有对一个会话对象的引用。在这个应用程序中,我们依靠 Transactional 标注来确保 run()方法实现的所有代码都可以访问到同一个 Hibernate 会话对象。

调整 CreateTest、QueryTest 以及 AlbumTest

现在我们已经定义好了 Test 接口,而且为这个接口的实现还建立了一个稳定的事务管理环境。接下来就可以修订我们原来的 CreateTest、QueryTest 以及 AlbumTest 类。首先按例 13-10 所示来修改 CreateTest 类。

例 13-10:为了在 Spring 中使用而修改 CreateTest 类

package com.oreilly.hh;

import java.sql.Time;

import java.util.*;

import com.oreilly.hh.dao.*;

import com.oreilly.hh.data.*;

/**

*Create sample data, letting Hibernate persist it for us.

*/

public class CreateTest implements Test{

private ArtistDAO artistDAO;

private TrackDAO trackDAO;

/**

*Utility method to associate an artist with a track

*/

private static void addTrackArtist(Track track, Artist artist){

track.getArtists().add(artist);

}

/*(non-Javadoc)

*@see com.oreilly.hh.Test#run()

*/

public void run(){

StereoVolume fullVolume=new StereoVolume();

Track track=new Track("Russian Trance","vol2/album610/track02.mp3",

Time.valueOf("00:03:30"),new HashSet<Artist>(),new Date(),

fullVolume, SourceMedia.CD, new HashSet<String>());

addTrackArtist(track, artistDAO.getArtist("PPK",true));

trackDAO.persist(track);

}

public ArtistDAO getArtistDAO(){return artistDAO;}

public void setArtistDAO(ArtistDAO artistDAO){

this.artistDAO=artistDAO;

}

public TrackDAO getTrackDAO(){return trackDAO;}

public void setTrackDAO(TrackDAO trackDAO){

this.trackDAO=trackDAO;

}

}

注意 CreateTest 类有两个私有的成员变量:artistDAO 和 trackDAO,它们都配备了作为 bean 属性的访问器(accessor)方法。接着,按照 Test 接口的规定,我们实现了一个简单的 run()方法,它最终会调用 trackDAO.makePersistent()来完成 Track 对象的持久化。所有的处理就是这样的,没有 try/catch/finally 块,也没有涉及事务管理。在 DAO 类的帮助下,我们差不多将所有持久化处理都交给了 Spring 框架。例 13-11 是从 applicationContext.xml 摘取的一段代码,该配置将 CreateTest 类创建成一个 ID 为 createTest 的 bean,并将它的 artistDAO 和 trackDAO 属性组装为相应 DAO bean 的引用。

例 13-11:配置 createTest bean

<bean id="createTest"class="com.oreilly.hh.CreateTest">

<property name="trackDAO"ref="trackDAO"/>

<property name="artistDAO"ref="artistDAO"/>

</bean>

将 CreateTest 的这个实现与例 3-3 的原始版本进行比较,你会发现它现在已经面目全非了。非 Spring 版本的 CreateTest 类必须负责维护会话对象的创建、事务管理、异常处理以及配置。而最新的版本甚至连会话的影子也没有看到。事实上,在 CreateTest 的最新版本中没有一点 Hibernate 特定的东西:DAO 类让我们的应用程序的业务逻辑不必直接处理底层的持久化机制。换句话说,在你熟悉了 Spring Framework,安装好它以后,通过 Spring 进行持久化要比直接使用 Hibernate 容易很多。再看看例 13-12。

例 13-12:为了在 Spring 中使用而修改 QueryTest 类

package com.oreilly.hh;

import java.sql.Time;

import java.util.List;

import org.apache.log4j.Logger;

import com.oreilly.hh.dao.TrackDAO;

import com.oreilly.hh.data.Track;

/**

*Retrieve data as objects

*/

public class QueryTest implements Test{

private static Logger log=Logger.getLogger(QueryTest.class);

private TrackDAO trackDAO;

public void run(){

//Print the tracks that will fit in five minutes

List<Track>tracks=trackDAO.tracksNoLongerThan(

Time.valueOf("00:05:00"));

for(Track track:tracks){

//For each track returned, print out the

//title and the playTime

log.info("Track:\""+track.getTitle()+"\","

+track.getPlayTime());

}

}

public TrackDAO getTrackDAO(){return trackDAO;}

public void setTrackDAO(TrackDAO trackDAO){

this.trackDAO=trackDAO;

}

}

重新实现的 QueryTest 也定义了一个私有成员变量,用于引用 TrackDAO 对象。run()方法调用 trackDAO.tracksNoLongerThan()方法,并为它传递了一个表示 5 分钟的 Java.sql.Time 类型的变量。这段代码循环访问查询结果,用 Log4J 打印输出 Track 对象的 title 和 playTime 属性。最后看看例 13-13。

例 13-13:重新实现的 AlbumTest

package com.oreilly.hh;

import java.sql.Time;

import java.util.*;

import org.apache.log4j.Logger;

import com.oreilly.hh.dao.*;

import com.oreilly.hh.data.*;

/**

*Create sample album data, letting Hibernate persist it for us.

*/

public class AlbumTest implements Test{

private static Logger log=Logger.getLogger(AlbumTest.class);

private AlbumDAO albumDAO;❶

private ArtistDAO artistDAO;

private TrackDAO trackDAO;

public void run(){

//Retrieve(or create)an Artist matching this name

Artist artist=artistDAO.getArtist("Martin L.Gore",true);❷

//Create an instance of album, add the artist and persist it

//to the database.

Album album=new Album("Counterfeit e.p.",1,

new HashSet<Artist>(),new HashSet<String>(),

new ArrayList<AlbumTrack>(5),new Date());

album.getArtists().add(artist);

album=albumDAO.persist(album);❸

//Add two album tracks

addAlbumTrack(album,"Compulsion","vol1/album83/track01.mp3",

Time.valueOf("00:05:29"),artist,1,1);

addAlbumTrack(album,"In a Manner of Speaking",

"vol1/album83/track02.mp3",Time.valueOf("00:04:21"),

artist,1,2);

//persist the album

album=albumDAO.persist(album);❹

log.info(album);

}

/**

*Quick and dirty helper method to handle repetitive portion of creating

*album tracks.A real implementation would have much more flexibility.

*/

private void addAlbumTrack(Album album, String title, String file,

Time length, Artist artist, int disc,

int positionOnDisc){

//Create a new Track object and add the artist

Track track=new Track(title, file, length, new HashSet<Artist>(),

new Date(),new StereoVolume(),SourceMedia.CD,

new HashSet<String>());

track.getArtists().add(artist);

//Persist the track to the database

track=trackDAO.persist(track);

//Add a new instance of AlbumTrack with the persisted

//album and track objects

album.getTracks().add(new AlbumTrack(track, disc, positionOnDisc));

}

public AlbumDAO getAlbumDAO(){return albumDAO;}

public void setAlbumDAO(AlbumDAO albumDAO){

this.albumDAO=albumDAO;

}

public ArtistDAO getArtistDAO(){return artistDAO;}

public void setArtistDAO(ArtistDAO artistDAO){

this.artistDAO=artistDAO;

}

public TrackDAO getTrackDAO(){return trackDAO;}

public void setTrackDAO(TrackDAO trackDAO){

this.trackDAO=trackDAO;

}

}

AlbumTest 比 CreateTest 和 QueryTest 都要更复杂,因为它要处理多个对象的创建和持久化,以及关联效果。可以逐步看看它的代码:

❶就像 CreateTest 和 QueryTest 一样,AlbumTest 类也定义了一系列私有字段来引用它需要的所有 DAO 对象:trackDAO、artistDAO 以及 albumDAO。

❷AlbumTest 首先使用 artistDAO.getArtist()取回一个 Artist 对象,如果这个方法没有找到请求的艺人对象,就会创建一个新的 Artist 对象。

❸持久化 Album 实例。这一步会在数据库中创建一行数据,并返回一个具有非 null 值 id 属性的 Album 对象。我们现在正在持久化 Album 记录,这样就能够使用新的 Album 实例来创建多个 Track 对象,再把它们关联到这个新的 Ablum 对象。为了让这一步能够正常运行,需要确保我们的 Album 和 Track 对象均具有非 null 的 id 属性。

❹接着再增加一系列 Track 对象。要创建 Track 对象,我们首先创建一个新的 Track 实例,再用 trackDAO.persist()方法来持久化该 Track 对象。在 addAlbumTrack()方法中,我们创建了几个 Track 对象,再将它们与 Album 组合起来,放到 AlbumTrack 关系对象中。Album 上的 tracks 属性有一个一对多关系,它的 cascade 属性设置为 CacscadeType.ALL,所以当我们再次持久化专辑对象时,它会自动在 ALBUM_TRACKS 表中创建相应的数据行。

这就是我们对 Test 接口的实现。所有通用的处理已经转换到了所有 DAO 的持久化代码中,接着再把我们单独的 CreateTest、QueryTest 以及 AlbumTest 类移植到其属性引用了这些 DAO 的 bean 中,同时将实际的测试代码移植到 Test 接口要求的 run()方法中。这样,Spring 就可以将所有这些组件串接在一起。下一节我们将看看如何执行这些测试类。

TestRunner:加载 Spring ApplicationContext

如果我们没办法加载 Spring 的 ApplicationContext,并执行我们的 Test 对象,前面的所有代码就无法使用。为此,我们要创建一个带有 static main()方法的 TestRunner 类,并从我们的 Ant build.xml 中调用该方法。例 13-14 完整地列出了 TestRunner 类的内容。这个类负责加载我们的 Spring ApplicationContext,取回一个 Test 实现,并执行它。

例 13-14:加载 Spring ApplicationContext

package com.oreilly.hh;

import org.apache.log4j.Logger;

import org.apache.log4j.PropertyConfigurator;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

/**

*A simple harness to run our tests.Configures Log4J,

*creates an ApplicationContext, retrieves a bean from Spring

*/

public class TestRunner{

private static Logger log;

public static void main(String[]args)throws Exception{

//Configure Log4J from a properties file

PropertyConfigurator.configure(

TestRunner.class.getResource("/log4j.properties"));❶

log=Logger.getLogger(TestRunner.class);

//Load our Spring Application Context

log.info("Initializing TestRunner……");

log.info("Loading Spring Configuration……");

ApplicationContext context=❷

new ClassPathXmlApplicationContext("applicationContext.xml");

//Retrieve the test name from the command line and

//run the test.

String testName=args[0];

log.info("Running test:"+testName);

Test test=(Test)context.getBean(testName);❸

test.run();

}

}

TestRunner 负责为我们做三件事情,如 JavaDoc 中所示:

❶通过引用类路径的根目录下的 log4j.properties 来配置 Log4J。

❷使用 ClassPathXmlApplicationContex 对象来创建一个 Spring 的 ApplicationContext 对象。ClassPathXmlApplicationContext 的构造函数接受一个字符串参数,用这个参数来指明 Spring XML 配置文件在类路径中的位置。在这个例子中,我们的 applicationContext.xml 位于类路径的根目录(紧挨着 log4j.properties 文件)。

❸最后,我们从命令行参数中得到 bean 的名称,再从 ApplicationContext 中取回相应的 Test 对象。可以看到,从 ApplicationContext 中取回特定名称的 bean 是非常容易的,只需要调用 context.getBean(名称),再将取回的结果转换为想要的类型。

运行 CreateTest、QueryTest 以及 AlbumTest

为了运行 TestRunner,并从我们的 Spring ApplicationContext 中取回正确的 bean 对象,还需要修改我们的 Ant build.xml 脚本。找到名为 ctest、qtest 以及 atest 的构建目标,修改它们以包含以下 XML,如例 13-15 所示。

例 13-15:从 Ant 中执行 TestRunner

<target name="atest"description="Creates and persists some album data"

depends="compile">

<java classname="com.oreilly.hh.TestRunner"fork="true">

<classpath refid="project.class.path"/>

<arg value="albumTest"/>

</java>

</target>

<target name="ctest"description="Creates and persists some sample data"

depends="compile">

<java classname="com.oreilly.hh.TestRunner"fork="true"failonerror="true">

<classpath refid="project.class.path"/>

<arg value="createTest"/>

</java>

</target>

<target name="qtest"description="Runs a query"depends="compile">

<java classname="com.oreilly.hh.TestRunner"fork="true">

<classpath refid="project.class.path"/>

<arg value="queryTest"/>

</java>

</target>

TestRunner 类使用它的第一个命令行参数作为要从 Spring ApplicationContext 获取的 bean 的名称。在 build.xml 中,我们在调用 TestRunner 时,就将 bean(applicationContext.xml 中的)的名称作为参数传递给它。

为了创建测试数据库,像平常那样运行 ant schema;为了将数据插入到数据库中,则需要运行我们新版本的 ant ctest:

%ant schema

%ant ctest

Buildfile:build.xml

prepare:

compile:

ctest:

[java]INFO TestRunner:20-Initializing TestRunner……

[java]INFO TestRunner:21-Loading Spring Configuration……

[java]INFO TestRunner:25-Running test:createTest

BUILD SUCCESSFUL

Total time:3 seconds

运行 ant qtest,以调用新的 QueryTest 示例,并确认我们组装起来的所有东西是否可以正常工作:

%ant qtest

Buildfile:build.xml

prepare:

compile:

qtest:

[java]INFO TestRunner:20-Initializing TestRunner……

[java]INFO TestRunner:21-Loading Spring Configuration……

[java]INFO TestRunner:25-Running test:queryTest

[java]INFO QueryTest:25-Track:"Russian Trance",00:03:30

[java]INFO QueryTest:25-Track:"Video Killed the Radio Star",00:03:49

[java]INFO QueryTest:25-Track:"Test Tone 1",00:00:10

BUILD SUCCESSFUL

Total time:3 seconds

最后,我们可以运行新的 AlbumTest 示例。输入 ant atest 命令,你应该能够看到以下输出内容:

%ant atest

Buildfile:build.xml

prepare:

compile:

atest:

[java]INFO TestRunner:16-Initializing TestRunner……

[java]INFO TestRunner:17-Loading Spring Configuration……

[java]INFO TestRunner:21-Running test:albumTest

[java]INFO AlbumTest:40-Persisted Album:1

[java]INFO AlbumTest:59-Saved an album named Counterfeit e.p.

[java]INFO AlbumTest:60-With 2 tracks.

BUILD SUCCESSFUL

Total time:2 seconds

一切正常,现在还要做什么

Spring Framework 和 Hibernate 彼此取长补短,配合得相当默契。如果你准备在一个大型应用程序中采用 Hibernate,则应该考虑在 Spring Framework 的基础上来构建你的应用程序。在你投入时间学会了这种框架以后,我们相信你会发现为事务处理、连接管理以及 Hibernate Session 管理而编写的代码数量一定会有所减少。在这些常规任务上花费的时间越少,就可以投入更多的时间到你的应用程序特定的需求和业务逻辑处理上。可移植性(portability)是使用 Spring(或任何类似的 IoC 容器)和 DAO 模式的另一个原因。虽然 Hibernate 是目前众多持久化库中的首选,但也说不定在未来的 10 年中又会冒出什么新技术。如果你将 Hibernate 特定的代码与应用程序的其他部分隔离开来,万一要试验下一种什么新技术时,就方便多了。

注意:Spring 确实可以负责许多乏味的工作。但这不应该成为我们不去学习 Hibernate 细节的一个借口。

当和 Spring 配合使用时,小心不要被 Hibernate 的简单性所迷惑。本书的几位作者一致同意,虽然 Hibernate 是一件非常好的东西,但有时也会因为某些原因而很难调试和诊断 Hibernate:输入错了的一个字符、没有正确映射的数据表、稍微不正确的 flush(刷新)模式或者是一些神秘的 JDBC 驱动程序的不兼容问题。Spring 之所以让 Hibernate 变得容易,是因为它提供了一些实用的抽象,让你的操作变得更简单。但是这样的简单性减少了需要直接在数据库中执行 SQL 语句的机会,让你更加脱离底层细节。虽然你可能不必自己编写事务处理代码,但在遇到问题时,这些抽象也让你更难诊断出产生错误的根本原因。不要误解,我是不会脱离 Spring 而单独使用 Hibernate 的,但是如果你牢固地掌握好了 Hibernate 的底层细节,那么就能更快地诊断出问题是出在了哪儿。

在下一章,我们将向你展示更高级的技术—如何将 Hibernate 集成到一个称为 Stripes 的 Web 应用程序框架中。在这个 Web 程序框架中,你将看到如何将 Spring 作为 Stripes 和 Hibernate 之间一个中立的代理。在你阅读学习下一章时,你应该牢记一个事实:目前使用的大多数流行的 Web 应用程序框架都提供了与 Spring 直接集成的某种功能。如果你使用的是 Struts 2、Wicket 或者 Spring MVC,它们中的许多概念是相同的。Spring 是软件的 Rosetta.Stone(译 [1] ),在你接受它以后,就可以访问为与 Spring 集成而设计的所有开发库。以 Spring 作为基石,你就可以随着需求的改变而更容易地在不同技术之间进行转换。比如,不用 Java,而是用 JRuby 或 Groovy 来编写 DAO 组件;使用 Quartz 来集成 cron-like 表达式;以及使用像 Apache CXF 之类的库将服务对象发布为 SOAP 服务端点等。

[1] 美国 Rosetta Stone(罗赛塔石碑语言学习软件)是风靡世界的多媒体英语教学软件。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。