如果關心開發人員的最新熱點,那么您可能聽說過 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面編程)。不過,像許多開發人員一樣,您可能不清楚在自己的開發工作中如何使用這些技術。在本文中,通過具體介紹使用 Hibernate 和 Spring 在企業應用程序中構建一個事務持久層,您會認識到這些技術。
Hibernate 是 Java 平臺上的一種流行的、容易使用的開放源代碼對象關系(OR)映射框架。Spring 是一個 AOP 框架和 IOC 容器。這兩種技術一起提供了本文中介紹的開發工作的基礎。將使用 Hibernate 把一些持久性對象映射到關系數據庫中,用 Spring 使 Hibernate 更容易使用并提供聲明性事務支持。由于為示例類編寫測試代碼時使用了 DbUnit,我還附帶介紹了一點 TDD (測試驅動的開發)的內容。
注意,本文假定讀者熟悉 Java 平臺上的企業開發,包括 JDBC、OR 映射內容、J2EE 設計模式如 DAO,以及聲明性事務支持,如 Enterprise JavaBean (EJB)技術所提供的事務支持。理解這里的討論不需要成為這些技術的專家,也不需要熟悉 AOP、IOC 或者 TDD,因為在本文中對這三者都做了介紹。
我將首先介紹兩種開發技術,然后分析例子。
Hibernate 簡介
Hibernate 是 Java 平臺上的一種全功能的、開放源代碼 OR 映射框架。Hibernate 在許多方面類似于 EJB CMP CMR (容器管理的持久性/容器管理的關系)和 JDO(Java Data Objects)。與 JDO 不同,Hibernate 完全著眼于關系數據庫的 OR 映射,并且包括比大多數商業產品更多的功能。大多數 EJB CMP CMR 解決方案使用代碼生成實現持久性代碼,而 JDO 使用字節碼修飾。與之相反,Hibernate 使用反射和運行時字節碼生成,使它對于最終用戶幾乎是透明的(以前 Hibernate 的實現只使用反射,它有助于調試,當前版本保留了這種選項)。
|
移植基于 Hibernate 的應用程序
如果應用程序必須在多個 RDBMS 系統上運行 ,那么基于 Hibernate 的應用程序可以毫不費力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和許多其他數據庫。我最近甚至將一個應用程序從 MySQL 移植到 Hibernate 沒有很好支持的 Firebird,而這種移植是很容易的。有關在 Postgres 和 MySQL 之間轉換的案例分析,請參閱
參考資料
。
|
|
Hibernate 可以模擬繼承(有幾種方式)、關聯(一對一或者一對多、containment 和 aggregation)和 composition。我將在本文中討論每種關系類型的幾個例子。
Hibernate 提供了一種稱為 Hibernate Query Language (HQL) 的 查詢語言,它類似于 JDO 的 JDOQL 和 EJB 的 EJB QL,盡管它更接近于前者。但是 Hibernate 沒有就此止步:它還可以進行直接的 SQL 查詢和/或使用 object criteria很容易地在運行時構成查詢條件。在本文的例子中我將只使用 HQL。
與 EJB CMP CMR 不同,Hibernate 像 JDO 一樣可以在 J2EE 容器內部或者外部工作,這可以讓那些進行 TDD 和敏捷開發的人受益。
Spring 簡介
AOP 專家 Nicholas Lesiecki 第一次向我解釋 AOP 時,他說的我一個詞也沒理解,我覺得就像第一次考慮使用 IOC 容器的可能性時一樣。每一種技術的概念基礎本身就需要很好地消化,每一種技術所使用的各種各樣的縮寫讓事情更糟了——特別是其中許多術語與我們已經使用的根本不一樣了。
像許多技術一樣,理解這兩種技術的實際使用比學習理論更容易。經過自己對 AOP 和 IOC 容器實現(即 XWork、PicoContainer 和 Spring)的分析,我發現這些技術可以幫助我獲得功能,而不會在多框架中添加基于代碼的依賴性。它們都將成為我后面開發項目的一部分。
簡單地說,AOP 讓開發人員可以創建非行為性的關注點,稱為橫切關注點,并將它們插入到應用程序代碼中。使用 AOP 后,公共服務(比如日志、持久性、事務等)就可以分解成方面并應用到域對象上,同時不會增加域對象的對象模型的復雜性。
|
關于 DbUnit
用新的框架開發而不進行單元測試,就像不帶保護網走鋼絲:當然可以這樣做,但是很可能會受傷。我選擇在有保護網的條件下開發,對我來說這個保護網就是 TDD。在有 DbUnit 之前,對依賴于數據庫的代碼進行測試是不太容易的。DbUnit 是 JUnit 的一個擴展,它提供了依賴于數據庫的單元測試的框架。我用 DbUnit 編寫本文中示例類的測試代碼。雖然在本文中沒有出現,不過在本文源代碼中提供了 DbUnit 代碼(請參閱
參考資料
)。有關 DbUnit 的介紹,請參閱 Philippe Girolami 的“
Control your test-environment with DbUnit and Anthill
” ( developerWorks,2004 年 4 月)。
|
|
IOC 允許創建一個可以構造對象的應用環境,然后向這些對象傳遞它們的協作對象。正如單詞 倒置 所表明的,IOC 就像反過來的 JNDI。沒有使用一堆抽象工廠、服務定位器、單元素(singleton)和直接構造(straight construction),每一個對象都是用其協作對象構造的。因此是由容器管理協作對象(collaborator)。
Spring 既是一個 AOP 框架、也是一個 IOC 容器。我記得 Grady Booch 說過,對象最好的地方是可以替換它們,而 Spring 最好的地方是它有助于您替換它們。有了 Spring,只要用 JavaBean 屬性和配置文件加入依賴性(協作對象)。然后可以很容易地在需要時替換具有類似接口的協作對象。
Spring 為 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是將要用 AOP 為示例應用程序聲明式地添加事務支持,與使用 EJB 技術時的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多內容,請參閱
參考資料
。
具體到業務
在本文的其余部分,所有的討論都將基于一個實際的例子。起點是一個企業應用程序,要為它實現一個事務持久層。持久層是一個對象關系數據庫,它包括像 User
、 User Group
、 Roles
和 ContactInfo
這些熟悉的抽象。
在深入到數據庫的要素——查詢和事務管理——之前,需要建立它的基礎:對象關系映射。我將用 Hibernate 設置它,并只使用一點 Spring。
用 Hibernate 進行 OR 映射
Hibernate 使用 XML ( *.hbm.xml) 文件將 Java 類映射到表,將 JavaBean 屬性映射到數據庫表。幸運的是,有一組 XDoclet
標簽支持 Hibernate 開發,這使得創建所需要的 *.hbm.xml 文件更容易了。清單 1 中的代碼將一個 Java 類映射到數據庫表。關于 XDoclet
標簽的更多內容,請參閱
參考資料
。
清單 1. 將 Java 類映射到 DB 表
[User.java]
/**
*
@hibernate.class table="TBL_USER"
* ..
* ..
* ...
*/
public class User {
private Long id = new Long(-1);
private String email;
private String password;
.
.
.
/**
* @return
*
@hibernate.id column="PK_USER_ID"
* unsaved-value="-1"
* generator-class="native"
*/
public Long getId() {
return id;
}
...
/**
*
@hibernate.property column="VC_EMAIL"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="82"
* @return
*/
public String getEmail() {
return email;
}
/**
*
@hibernate.property column="VC_PASSWORD"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="20"
* @return
*/
public String getPassword() {
return password;
}
...
...
...
}
|
可以看到, @hibernate.class table="TBL_USER"
標簽將 User
映射到 TBL_USER
表。 @hibernate.property column="VC_PASSWORD"
將 JavaBean 屬性 password 映射到 VC_PASSWORD
列。 @hibernate.id column="PK_USER_ID"
標簽聲明 id 屬性是主鍵,它將使用本機( generator-class="native"
)數據庫機制生成鍵(例如,Oracle sequences 和 SQL Server Identity 鍵)。Hibernate 可以指定 generator-class="native"
以外的、其他可以想象的得到主鍵獲得策略,不過我更愿意使用 native。 type 和 length屬性用于從 Hibernate *.hbm.xml OR 映射文件生成表。這些 final 屬性是可選的,因為使用的可能不是 green-field 數據庫。在這個例子中,已經有數據庫了,所以不需要額外的屬性。( green-field 應用程序是一個新的應用程序, green-field 數據是新應用程序的一個新數據庫。不會經常開發一個全新的應用程序,不過偶爾有一兩次也不錯)。
看過了表如何映射到類以及列如何映射到 JavaBean 屬性,該使用 Hibernate 在 OR 數據庫中設置一些關系了。
設置對象關系
在本節中,我將只觸及 Hibernate 提供的設置對象間關系的選項的一小部分。首先設置像 User
、 User Group
、 Roles
和 ContactInfo
這些類之間的關系。其中一些關系如圖 1 所示,這是數據庫的驗證對象模型。
圖 1. 關系的圖示
如您所見,在上述抽象中存在各種各樣的關系。 User
與 ContactInfo
有一對一關系。 ContactInfo
的生命周期與 User
相同(用數據庫的術語,UML 中的組成 aka 級聯刪除)。如果刪除 User
,則相應的 ContactInfo
也會刪除。在 User
s 與 Role
s 之間存在多對多關系(即與獨立生命周期相關聯)。在 Group
s 與 User
s 之間存在一對多關系,因為組有許多用戶。用戶可以存在于組外,即是 aggregation 而不是 composition (用數據庫的說法,在 Group
s 和 Users
之間沒有級聯刪除關系)。此外, User
和 Employee
有子類關系,就是說, Employee
的類型為 User
。表 1 顯示了如何用 XDoclet
標簽創建一些不同類型的對象關系。
表 1. 用 XDoclet 創建對象關系
關系
|
Java/XDoclet
|
SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
|
組包含用戶
一對多
Aggregation
雙向 (Group<-->Users)
|
[Group.java]
/** * * @return * * @hibernate.bag name="users" * cascade="save-update" * lazy="true" * inverse="true" * * @hibernate.collection-key * column="FK_GROUP_ID" * * @hibernate.collection-one-to-many * class="net.sf.hibernateExamples.User" */ public List getUsers() { return users; }
[User.java] /** * @hibernate.many-to-one * column="FK_GROUP_ID" * class="net.sf.hibernateExamples.Group" */ public Group getGroup() { return group; }
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_GROUP ( PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(255), VC_NAME VARCHAR(40) unique, primary key (PK_GROUP_ID) )
alter table TBL_USER add index (FK_GROUP_ID), add constraint FK_111 foreign key (FK_GROUP_ID) references TBL_GROUP (PK_GROUP_ID)
|
用戶有聯系信息
一對一
Composition 單向 (User-->ContactInfo)
|
[User.java]
/** * @return * * @hibernate.one-to-one cascade="all" * */ public ContactInfo getContactInfo() { return contactInfo; }
[ContactInfo.java] (Nothing to see here. Unidirectional!)
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_CONTACT_INFO ( PK_CONTACT_INFO_ID BIGINT not null, ... ... ... primary key (PK_CONTACT_INFO_ID) )
|
用戶與角色關聯
多對多
Association
單向 (Users-->Roles)
|
[User.java]
/** * @return * @hibernate.bag * table="TBL_JOIN_USER_ROLE" * cascade="all" * inverse="true" * * @hibernate.collection-key * column="FK_USER_ID" * * @hibernate.collection-many-to-many * class="net.sf.hibernateExamples.Role" * column="FK_ROLE_ID" * */ public List getRoles() { return roles; }
[Role.java] Nothing to see here. Unidirectional!
|
create table TBL_ROLE ( PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(200), VC_NAME VARCHAR(20), primary key (PK_ROLE_ID) )
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_JOIN_USER_ROLE ( FK_USER_ID BIGINT not null, FK_ROLE_ID BIGINT not null )
|
雇員是用戶
Inheritance
用戶
 雇員
|
[User.java]
/** * @hibernate.class table="TBL_USER" * discriminator-value="2" * @hibernate.discriminator column="USER_TYPE" * ... ... ... */ public class User {
[Employee.java] /** * @hibernate.subclass discriminator-value = "1" */ public class Employee extends User{
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
|
要了解在 Hibernate 中設置對象關系的更多內容,請參閱
參考資料
。
Hibernate 中的查詢
Hibernate 有三種類型的查詢:
-
Criteria, object composition
-
SQL
-
HQL
在下面的例子中將只使用 HQL。本節還要使用 Spring,用它的 AOP-driven HibernateTemplate 簡化 Hibernate 會話的處理。在本節將開發一個 DAO(Data Access Object)。要了解更多關于 DAO 的內容,請參閱
參考資料
。
清單 2 展示了兩個方法:一個使用 HQL 查詢的組查詢,另一個是后面接一個操作的組查詢。注意在第二個方法中,Spring HibernateTemplate 是如何簡化會話管理的。
清單 2. 使用查詢
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
*/
public class UserDAO extends HibernateDaoSupport{
.
.
.
/**
* Demonstrates looking up a group with a HQL query
* @param email
* @return
*/
public Group findGroupByName(String name) {
return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
}
/**
* Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
* @param email
* @return
*/
public Group findPopulatedGroupByName(final String name) {
HibernateCallback callback = new HibernateCallback(){
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Group group =null;
String query = "from Group g where g.name=?";
Query queryObject = getHibernateTemplate().createQuery(session, query);
queryObject.setParameter(0, name);
group = (Group) queryObject.list().get(0);
group.getUsers().size();//force load
return group;
}
};
return (Group) getHibernateTemplate().execute(callback);
}
.
.
.
}
|
您可能會注意到第二個方法比第一個方法復雜得多,因為它強迫加載 users
集合。因為 Group->Users
之間的關系設置為 lazy initialize(即表 2 中 lazy="true"
),組對象需要一個活躍的會話以查詢用戶。在定義 Group
和 User
s 之間關系時設置這個屬性為 lazy="false"
,則不需要第二個方法。在這種情況下,可能使用第一種方法 ( findGroupByName
) 列出組,用第二種方法( findPopulatedGroupByName
)查看組細節。
Spring IOC 和 Hibernate
使用 Spring 時,在 J2EE 容器內和容器外工作一樣容易。比如在最近的項目中,我在 Eclipse 中,使用 HSQL 和本地數據庫對使用 Hibernate 事務管理器的 Hypersonic SQL 數據庫進行持久性單元測試。然后,在部署到 J2EE 服務器時,將持久層轉換為使用 J2EE 數據源(通過 JNDI)、JTA 事務和使用 FireBird (一個開放源代碼版本的 Interbase)。這是用 Spring 作為 IOC 容器完成的。
從清單 3 中可以看出,Spring 允許加入依賴性。注意清單中應用程序上下文文件是如何配置 dataSource
的。 dataSource
傳遞給 sessionFactory
, sessionFactory
傳遞給 UserDAO
。
清單 3. Spring IOC 和 Hibernate
<beans>
<!-- Datasource that works in any application server
You could easily use J2EE data source instead if this were
running inside of a J2EE container.
-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
<property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Must references all OR mapping files. -->
<property name="mappingResources">
<list>
<value>net/sf/hibernateExamples/User.hbm.xml</value>
<value>net/sf/hibernateExamples/Group.hbm.xml</value>
<value>net/sf/hibernateExamples/Role.hbm.xml</value>
<value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
</list>
</property>
<!-- Set the type of database; changing this one property will port this to Oracle,
MS SQL etc. -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</beans>
|
設置了 UserDAO
后,下一步就是定義并使用更多的查詢以展示可以完成的操作。Hibernate 可以用預定義查詢將查詢存儲到源代碼之外,如清單 4 所示。
清單 4. 預定義查詢
[User.java]
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
* @hibernate.class table="TBL_USER" discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
*
* @hibernate.query name="OverheadStaff"
* query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
*
* @hibernate.query name="CriticalStaff"
* query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
*
* @hibernate.query name="GetUsersInAGroup"
* query="select user from Group g join g.users user"
*
* @hibernate.query name="GetUsersNotInAGroup"
* query="select user from User user where user.group is null"
*
* @hibernate.query name="UsersBySalaryGreaterThan"
* query="from User user inner join user.contactInfo info where info.salary > ?1"
*
* @hibernate.query name="UsersBySalaryBetween"
* query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
*
* @hibernate.query name="UsersByLastNameLike"
* query="from User user join user.contactInfo info where info.lastName like ?1"
*
* @hibernate.query name="GetEmailsOfUsers"
* query="select user.email from Group g join g.users as user where g.name = ?1"
*
*/
public class User {
.
.
.
|
上述代碼定義了幾個預定義查詢。 預定義查詢 是存儲在 *.hbm.xml文件中的查詢。在清單 5 中,可以看到如何執行預定義查詢。
清單 5. 使用預定義查詢
[UserDAO.java]
/**
* Demonstrates a query that returns a String.
*/
public String[] getUserEmailsInGroup(String groupName){
List emailList =
getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
return (String [])
emailList.toArray(new String[emailList.size()]);
}
/**
* Demonstrates a query that returns a list of Users
*
* @return A list of emails of all of the users in the authentication system.
*
*/
public List getUsers(){
return getHibernateTemplate().findByNamedQuery("AllUsers");
}
/**
* Demonstrates passing a single argument to a query.
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalary(float salary){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryGreaterThan",
new Float(salary));
}
/**
* Demonstrates passing multiple arguments to a query
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalaryRange(float start, float stop){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryBetween",
new Object[] {new Float(start), new Float(stop)});
}
|
查詢進行時,可以在持久層中加上最后一層:使用 Spring 的事務管理。
用 Spring 管理事務
Spring 可以聲明式地管理事務。例如, UserDAO.addUser
方法當前不是在單個事務中執行的。因此,組中的每一個用戶都插入到自己的事務中,如清單 6 所示。
清單 6. 添加一組用戶
[UserDAO.java]
/**
* @param group
*/
public void addGroup(Group group) {
getHibernateTemplate().save(group);
}
[UserDAOTest.java]
public void testAddGroupOfUsers(){
Group group = new Group();
for (int index=0; index < 10; index++){
User user = new User();
user.setEmail("rick"+index+"@foobar.com" );
user.setPassword("foobar");
group.addUser(user);
}
group.setName("testGroup");
userDAO.addGroup(group);
assertNotNull(group.getId());
Group group2 = userDAO.findPopulatedGroupByName("testGroup");
assertEquals("testGroup",group2.getName());
assertEquals(10, group2.getUsers().size());
String email = ((User)group2.getUsers().get(0)).getEmail();
assertEquals("rick0@foobar.com", email);
}
|
不建議使用上述解決方案,因為每一個 User
都要在自己的事務中插入到數據庫中。如果出現問題,那么只能添加部分用戶。如果希望保留 ACID 屬性(即保證所有都發生或者所有都不發生),可以通過程序進行事務管理,但是它很快就會變得一團糟了。相反,應使用 Spring 的 AOP 來支持聲明式的事務,如清單 7 所示。
清單 7. 聲明式管理事務
[applicationContext.xml]
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
</bean>
<bean id="userDAO"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="target"><ref local="userDAOTarget"/></property>
<property name="transactionAttributes">
<props>
<prop key="addGroup">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
注意在準備清單 7 的代碼時,我重新改寫了 UserDAO
并提取了其接口。這個接口現在是 UserDAO
,它的實現類是 UserDAOImpl
。這樣清單 7 中的事務代碼就使用了帶有事務屬性 (PROPAGATION_REQUIRED)
的 UserDAO.addGroup()
方法。現在只要底層數據庫支持,就可以在一個事務中添加所有用戶。
結束語
在本文中,介紹了如何使用 Hibernate 和 Spring 實現一個事務持久層。Hibernate 是一種先進的 OR 映射工具,而 Spring 是一個 AOP 框架和 IOC 容器。這兩種技術的綜合使用,使得開發人員可以編寫媲美數據庫廠商的代碼,它可以在 J2EE 容器中運行,也可以單獨運行。使用了 DbUnit (JUnit 的擴展)構建和測試本文中例子的所有代碼,雖然這不是討論的重點。
要了解有關 AOP、IOC 容器和測試驅動開發的更多內容,請參閱
參考資料
。
如果關心開發人員的最新熱點,那么您可能聽說過 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面編程)。不過,像許多開發人員一樣,您可能不清楚在自己的開發工作中如何使用這些技術。在本文中,通過具體介紹使用 Hibernate 和 Spring 在企業應用程序中構建一個事務持久層,您會認識到這些技術。
Hibernate 是 Java 平臺上的一種流行的、容易使用的開放源代碼對象關系(OR)映射框架。Spring 是一個 AOP 框架和 IOC 容器。這兩種技術一起提供了本文中介紹的開發工作的基礎。將使用 Hibernate 把一些持久性對象映射到關系數據庫中,用 Spring 使 Hibernate 更容易使用并提供聲明性事務支持。由于為示例類編寫測試代碼時使用了 DbUnit,我還附帶介紹了一點 TDD (測試驅動的開發)的內容。
注意,本文假定讀者熟悉 Java 平臺上的企業開發,包括 JDBC、OR 映射內容、J2EE 設計模式如 DAO,以及聲明性事務支持,如 Enterprise JavaBean (EJB)技術所提供的事務支持。理解這里的討論不需要成為這些技術的專家,也不需要熟悉 AOP、IOC 或者 TDD,因為在本文中對這三者都做了介紹。
我將首先介紹兩種開發技術,然后分析例子。
Hibernate 簡介
Hibernate 是 Java 平臺上的一種全功能的、開放源代碼 OR 映射框架。Hibernate 在許多方面類似于 EJB CMP CMR (容器管理的持久性/容器管理的關系)和 JDO(Java Data Objects)。與 JDO 不同,Hibernate 完全著眼于關系數據庫的 OR 映射,并且包括比大多數商業產品更多的功能。大多數 EJB CMP CMR 解決方案使用代碼生成實現持久性代碼,而 JDO 使用字節碼修飾。與之相反,Hibernate 使用反射和運行時字節碼生成,使它對于最終用戶幾乎是透明的(以前 Hibernate 的實現只使用反射,它有助于調試,當前版本保留了這種選項)。
|
移植基于 Hibernate 的應用程序
如果應用程序必須在多個 RDBMS 系統上運行 ,那么基于 Hibernate 的應用程序可以毫不費力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和許多其他數據庫。我最近甚至將一個應用程序從 MySQL 移植到 Hibernate 沒有很好支持的 Firebird,而這種移植是很容易的。有關在 Postgres 和 MySQL 之間轉換的案例分析,請參閱
參考資料
。
|
|
Hibernate 可以模擬繼承(有幾種方式)、關聯(一對一或者一對多、containment 和 aggregation)和 composition。我將在本文中討論每種關系類型的幾個例子。
Hibernate 提供了一種稱為 Hibernate Query Language (HQL) 的 查詢語言,它類似于 JDO 的 JDOQL 和 EJB 的 EJB QL,盡管它更接近于前者。但是 Hibernate 沒有就此止步:它還可以進行直接的 SQL 查詢和/或使用 object criteria很容易地在運行時構成查詢條件。在本文的例子中我將只使用 HQL。
與 EJB CMP CMR 不同,Hibernate 像 JDO 一樣可以在 J2EE 容器內部或者外部工作,這可以讓那些進行 TDD 和敏捷開發的人受益。
Spring 簡介
AOP 專家 Nicholas Lesiecki 第一次向我解釋 AOP 時,他說的我一個詞也沒理解,我覺得就像第一次考慮使用 IOC 容器的可能性時一樣。每一種技術的概念基礎本身就需要很好地消化,每一種技術所使用的各種各樣的縮寫讓事情更糟了——特別是其中許多術語與我們已經使用的根本不一樣了。
像許多技術一樣,理解這兩種技術的實際使用比學習理論更容易。經過自己對 AOP 和 IOC 容器實現(即 XWork、PicoContainer 和 Spring)的分析,我發現這些技術可以幫助我獲得功能,而不會在多框架中添加基于代碼的依賴性。它們都將成為我后面開發項目的一部分。
簡單地說,AOP 讓開發人員可以創建非行為性的關注點,稱為橫切關注點,并將它們插入到應用程序代碼中。使用 AOP 后,公共服務(比如日志、持久性、事務等)就可以分解成方面并應用到域對象上,同時不會增加域對象的對象模型的復雜性。
|
關于 DbUnit
用新的框架開發而不進行單元測試,就像不帶保護網走鋼絲:當然可以這樣做,但是很可能會受傷。我選擇在有保護網的條件下開發,對我來說這個保護網就是 TDD。在有 DbUnit 之前,對依賴于數據庫的代碼進行測試是不太容易的。DbUnit 是 JUnit 的一個擴展,它提供了依賴于數據庫的單元測試的框架。我用 DbUnit 編寫本文中示例類的測試代碼。雖然在本文中沒有出現,不過在本文源代碼中提供了 DbUnit 代碼(請參閱
參考資料
)。有關 DbUnit 的介紹,請參閱 Philippe Girolami 的“
Control your test-environment with DbUnit and Anthill
” ( developerWorks,2004 年 4 月)。
|
|
IOC 允許創建一個可以構造對象的應用環境,然后向這些對象傳遞它們的協作對象。正如單詞 倒置 所表明的,IOC 就像反過來的 JNDI。沒有使用一堆抽象工廠、服務定位器、單元素(singleton)和直接構造(straight construction),每一個對象都是用其協作對象構造的。因此是由容器管理協作對象(collaborator)。
Spring 既是一個 AOP 框架、也是一個 IOC 容器。我記得 Grady Booch 說過,對象最好的地方是可以替換它們,而 Spring 最好的地方是它有助于您替換它們。有了 Spring,只要用 JavaBean 屬性和配置文件加入依賴性(協作對象)。然后可以很容易地在需要時替換具有類似接口的協作對象。
Spring 為 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是將要用 AOP 為示例應用程序聲明式地添加事務支持,與使用 EJB 技術時的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多內容,請參閱
參考資料
。
具體到業務
在本文的其余部分,所有的討論都將基于一個實際的例子。起點是一個企業應用程序,要為它實現一個事務持久層。持久層是一個對象關系數據庫,它包括像 User
、 User Group
、 Roles
和 ContactInfo
這些熟悉的抽象。
在深入到數據庫的要素——查詢和事務管理——之前,需要建立它的基礎:對象關系映射。我將用 Hibernate 設置它,并只使用一點 Spring。
用 Hibernate 進行 OR 映射
Hibernate 使用 XML ( *.hbm.xml) 文件將 Java 類映射到表,將 JavaBean 屬性映射到數據庫表。幸運的是,有一組 XDoclet
標簽支持 Hibernate 開發,這使得創建所需要的 *.hbm.xml 文件更容易了。清單 1 中的代碼將一個 Java 類映射到數據庫表。關于 XDoclet
標簽的更多內容,請參閱
參考資料
。
清單 1. 將 Java 類映射到 DB 表
[User.java]
/**
*
@hibernate.class table="TBL_USER"
* ..
* ..
* ...
*/
public class User {
private Long id = new Long(-1);
private String email;
private String password;
.
.
.
/**
* @return
*
@hibernate.id column="PK_USER_ID"
* unsaved-value="-1"
* generator-class="native"
*/
public Long getId() {
return id;
}
...
/**
*
@hibernate.property column="VC_EMAIL"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="82"
* @return
*/
public String getEmail() {
return email;
}
/**
*
@hibernate.property column="VC_PASSWORD"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="20"
* @return
*/
public String getPassword() {
return password;
}
...
...
...
}
|
可以看到, @hibernate.class table="TBL_USER"
標簽將 User
映射到 TBL_USER
表。 @hibernate.property column="VC_PASSWORD"
將 JavaBean 屬性 password 映射到 VC_PASSWORD
列。 @hibernate.id column="PK_USER_ID"
標簽聲明 id 屬性是主鍵,它將使用本機( generator-class="native"
)數據庫機制生成鍵(例如,Oracle sequences 和 SQL Server Identity 鍵)。Hibernate 可以指定 generator-class="native"
以外的、其他可以想象的得到主鍵獲得策略,不過我更愿意使用 native。 type 和 length屬性用于從 Hibernate *.hbm.xml OR 映射文件生成表。這些 final 屬性是可選的,因為使用的可能不是 green-field 數據庫。在這個例子中,已經有數據庫了,所以不需要額外的屬性。( green-field 應用程序是一個新的應用程序, green-field 數據是新應用程序的一個新數據庫。不會經常開發一個全新的應用程序,不過偶爾有一兩次也不錯)。
看過了表如何映射到類以及列如何映射到 JavaBean 屬性,該使用 Hibernate 在 OR 數據庫中設置一些關系了。
設置對象關系
在本節中,我將只觸及 Hibernate 提供的設置對象間關系的選項的一小部分。首先設置像 User
、 User Group
、 Roles
和 ContactInfo
這些類之間的關系。其中一些關系如圖 1 所示,這是數據庫的驗證對象模型。
圖 1. 關系的圖示
如您所見,在上述抽象中存在各種各樣的關系。 User
與 ContactInfo
有一對一關系。 ContactInfo
的生命周期與 User
相同(用數據庫的術語,UML 中的組成 aka 級聯刪除)。如果刪除 User
,則相應的 ContactInfo
也會刪除。在 User
s 與 Role
s 之間存在多對多關系(即與獨立生命周期相關聯)。在 Group
s 與 User
s 之間存在一對多關系,因為組有許多用戶。用戶可以存在于組外,即是 aggregation 而不是 composition (用數據庫的說法,在 Group
s 和 Users
之間沒有級聯刪除關系)。此外, User
和 Employee
有子類關系,就是說, Employee
的類型為 User
。表 1 顯示了如何用 XDoclet
標簽創建一些不同類型的對象關系。
表 1. 用 XDoclet 創建對象關系
關系
|
Java/XDoclet
|
SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
|
組包含用戶
一對多
Aggregation
雙向 (Group<-->Users)
|
[Group.java]
/** * * @return * * @hibernate.bag name="users" * cascade="save-update" * lazy="true" * inverse="true" * * @hibernate.collection-key * column="FK_GROUP_ID" * * @hibernate.collection-one-to-many * class="net.sf.hibernateExamples.User" */ public List getUsers() { return users; }
[User.java] /** * @hibernate.many-to-one * column="FK_GROUP_ID" * class="net.sf.hibernateExamples.Group" */ public Group getGroup() { return group; }
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_GROUP ( PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(255), VC_NAME VARCHAR(40) unique, primary key (PK_GROUP_ID) )
alter table TBL_USER add index (FK_GROUP_ID), add constraint FK_111 foreign key (FK_GROUP_ID) references TBL_GROUP (PK_GROUP_ID)
|
用戶有聯系信息
一對一
Composition 單向 (User-->ContactInfo)
|
[User.java]
/** * @return * * @hibernate.one-to-one cascade="all" * */ public ContactInfo getContactInfo() { return contactInfo; }
[ContactInfo.java] (Nothing to see here. Unidirectional!)
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_CONTACT_INFO ( PK_CONTACT_INFO_ID BIGINT not null, ... ... ... primary key (PK_CONTACT_INFO_ID) )
|
用戶與角色關聯
多對多
Association
單向 (Users-->Roles)
|
[User.java]
/** * @return * @hibernate.bag * table="TBL_JOIN_USER_ROLE" * cascade="all" * inverse="true" * * @hibernate.collection-key * column="FK_USER_ID" * * @hibernate.collection-many-to-many * class="net.sf.hibernateExamples.Role" * column="FK_ROLE_ID" * */ public List getRoles() { return roles; }
[Role.java] Nothing to see here. Unidirectional!
|
create table TBL_ROLE ( PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(200), VC_NAME VARCHAR(20), primary key (PK_ROLE_ID) )
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_JOIN_USER_ROLE ( FK_USER_ID BIGINT not null, FK_ROLE_ID BIGINT not null )
|
雇員是用戶
Inheritance
用戶
 雇員
|
[User.java]
/** * @hibernate.class table="TBL_USER" * discriminator-value="2" * @hibernate.discriminator column="USER_TYPE" * ... ... ... */ public class User {
[Employee.java] /** * @hibernate.subclass discriminator-value = "1" */ public class Employee extends User{
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
|
要了解在 Hibernate 中設置對象關系的更多內容,請參閱
參考資料
。
Hibernate 中的查詢
Hibernate 有三種類型的查詢:
-
Criteria, object composition
-
SQL
-
HQL
在下面的例子中將只使用 HQL。本節還要使用 Spring,用它的 AOP-driven HibernateTemplate 簡化 Hibernate 會話的處理。在本節將開發一個 DAO(Data Access Object)。要了解更多關于 DAO 的內容,請參閱
參考資料
。
清單 2 展示了兩個方法:一個使用 HQL 查詢的組查詢,另一個是后面接一個操作的組查詢。注意在第二個方法中,Spring HibernateTemplate 是如何簡化會話管理的。
清單 2. 使用查詢
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
*/
public class UserDAO extends HibernateDaoSupport{
.
.
.
/**
* Demonstrates looking up a group with a HQL query
* @param email
* @return
*/
public Group findGroupByName(String name) {
return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
}
/**
* Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
* @param email
* @return
*/
public Group findPopulatedGroupByName(final String name) {
HibernateCallback callback = new HibernateCallback(){
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Group group =null;
String query = "from Group g where g.name=?";
Query queryObject = getHibernateTemplate().createQuery(session, query);
queryObject.setParameter(0, name);
group = (Group) queryObject.list().get(0);
group.getUsers().size();//force load
return group;
}
};
return (Group) getHibernateTemplate().execute(callback);
}
.
.
.
}
|
您可能會注意到第二個方法比第一個方法復雜得多,因為它強迫加載 users
集合。因為 Group->Users
之間的關系設置為 lazy initialize(即表 2 中 lazy="true"
),組對象需要一個活躍的會話以查詢用戶。在定義 Group
和 User
s 之間關系時設置這個屬性為 lazy="false"
,則不需要第二個方法。在這種情況下,可能使用第一種方法 ( findGroupByName
) 列出組,用第二種方法( findPopulatedGroupByName
)查看組細節。
Spring IOC 和 Hibernate
使用 Spring 時,在 J2EE 容器內和容器外工作一樣容易。比如在最近的項目中,我在 Eclipse 中,使用 HSQL 和本地數據庫對使用 Hibernate 事務管理器的 Hypersonic SQL 數據庫進行持久性單元測試。然后,在部署到 J2EE 服務器時,將持久層轉換為使用 J2EE 數據源(通過 JNDI)、JTA 事務和使用 FireBird (一個開放源代碼版本的 Interbase)。這是用 Spring 作為 IOC 容器完成的。
從清單 3 中可以看出,Spring 允許加入依賴性。注意清單中應用程序上下文文件是如何配置 dataSource
的。 dataSource
傳遞給 sessionFactory
, sessionFactory
傳遞給 UserDAO
。
清單 3. Spring IOC 和 Hibernate
<beans>
<!-- Datasource that works in any application server
You could easily use J2EE data source instead if this were
running inside of a J2EE container.
-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
<property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Must references all OR mapping files. -->
<property name="mappingResources">
<list>
<value>net/sf/hibernateExamples/User.hbm.xml</value>
<value>net/sf/hibernateExamples/Group.hbm.xml</value>
<value>net/sf/hibernateExamples/Role.hbm.xml</value>
<value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
</list>
</property>
<!-- Set the type of database; changing this one property will port this to Oracle,
MS SQL etc. -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</beans>
|
設置了 UserDAO
后,下一步就是定義并使用更多的查詢以展示可以完成的操作。Hibernate 可以用預定義查詢將查詢存儲到源代碼之外,如清單 4 所示。
清單 4. 預定義查詢
[User.java]
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
* @hibernate.class table="TBL_USER" discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
*
* @hibernate.query name="OverheadStaff"
* query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
*
* @hibernate.query name="CriticalStaff"
* query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
*
* @hibernate.query name="GetUsersInAGroup"
* query="select user from Group g join g.users user"
*
* @hibernate.query name="GetUsersNotInAGroup"
* query="select user from User user where user.group is null"
*
* @hibernate.query name="UsersBySalaryGreaterThan"
* query="from User user inner join user.contactInfo info where info.salary > ?1"
*
* @hibernate.query name="UsersBySalaryBetween"
* query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
*
* @hibernate.query name="UsersByLastNameLike"
* query="from User user join user.contactInfo info where info.lastName like ?1"
*
* @hibernate.query name="GetEmailsOfUsers"
* query="select user.email from Group g join g.users as user where g.name = ?1"
*
*/
public class User {
.
.
.
|
上述代碼定義了幾個預定義查詢。 預定義查詢 是存儲在 *.hbm.xml文件中的查詢。在清單 5 中,可以看到如何執行預定義查詢。
清單 5. 使用預定義查詢
[UserDAO.java]
/**
* Demonstrates a query that returns a String.
*/
public String[] getUserEmailsInGroup(String groupName){
List emailList =
getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
return (String [])
emailList.toArray(new String[emailList.size()]);
}
/**
* Demonstrates a query that returns a list of Users
*
* @return A list of emails of all of the users in the authentication system.
*
*/
public List getUsers(){
return getHibernateTemplate().findByNamedQuery("AllUsers");
}
/**
* Demonstrates passing a single argument to a query.
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalary(float salary){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryGreaterThan",
new Float(salary));
}
/**
* Demonstrates passing multiple arguments to a query
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalaryRange(float start, float stop){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryBetween",
new Object[] {new Float(start), new Float(stop)});
}
|
查詢進行時,可以在持久層中加上最后一層:使用 Spring 的事務管理。
用 Spring 管理事務
Spring 可以聲明式地管理事務。例如, UserDAO.addUser
方法當前不是在單個事務中執行的。因此,組中的每一個用戶都插入到自己的事務中,如清單 6 所示。
清單 6. 添加一組用戶
[UserDAO.java]
/**
* @param group
*/
public void addGroup(Group group) {
getHibernateTemplate().save(group);
}
[UserDAOTest.java]
public void testAddGroupOfUsers(){
Group group = new Group();
for (int index=0; index < 10; index++){
User user = new User();
user.setEmail("rick"+index+"@foobar.com" );
user.setPassword("foobar");
group.addUser(user);
}
group.setName("testGroup");
userDAO.addGroup(group);
assertNotNull(group.getId());
Group group2 = userDAO.findPopulatedGroupByName("testGroup");
assertEquals("testGroup",group2.getName());
assertEquals(10, group2.getUsers().size());
String email = ((User)group2.getUsers().get(0)).getEmail();
assertEquals("rick0@foobar.com", email);
}
|
不建議使用上述解決方案,因為每一個 User
都要在自己的事務中插入到數據庫中。如果出現問題,那么只能添加部分用戶。如果希望保留 ACID 屬性(即保證所有都發生或者所有都不發生),可以通過程序進行事務管理,但是它很快就會變得一團糟了。相反,應使用 Spring 的 AOP 來支持聲明式的事務,如清單 7 所示。
清單 7. 聲明式管理事務
[applicationContext.xml]
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
</bean>
<bean id="userDAO"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="target"><ref local="userDAOTarget"/></property>
<property name="transactionAttributes">
<props>
<prop key="addGroup">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
注意在準備清單 7 的代碼時,我重新改寫了 UserDAO
并提取了其接口。這個接口現在是 UserDAO
,它的實現類是 UserDAOImpl
。這樣清單 7 中的事務代碼就使用了帶有事務屬性 (PROPAGATION_REQUIRED)
的 UserDAO.addGroup()
方法。現在只要底層數據庫支持,就可以在一個事務中添加所有用戶。
結束語
在本文中,介紹了如何使用 Hibernate 和 Spring 實現一個事務持久層。Hibernate 是一種先進的 OR 映射工具,而 Spring 是一個 AOP 框架和 IOC 容器。這兩種技術的綜合使用,使得開發人員可以編寫媲美數據庫廠商的代碼,它可以在 J2EE 容器中運行,也可以單獨運行。使用了 DbUnit (JUnit 的擴展)構建和測試本文中例子的所有代碼,雖然這不是討論的重點。
要了解有關 AOP、IOC 容器和測試驅動開發的更多內容,請參閱
參考資料
。
如果關心開發人員的最新熱點,那么您可能聽說過 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面編程)。不過,像許多開發人員一樣,您可能不清楚在自己的開發工作中如何使用這些技術。在本文中,通過具體介紹使用 Hibernate 和 Spring 在企業應用程序中構建一個事務持久層,您會認識到這些技術。
Hibernate 是 Java 平臺上的一種流行的、容易使用的開放源代碼對象關系(OR)映射框架。Spring 是一個 AOP 框架和 IOC 容器。這兩種技術一起提供了本文中介紹的開發工作的基礎。將使用 Hibernate 把一些持久性對象映射到關系數據庫中,用 Spring 使 Hibernate 更容易使用并提供聲明性事務支持。由于為示例類編寫測試代碼時使用了 DbUnit,我還附帶介紹了一點 TDD (測試驅動的開發)的內容。
注意,本文假定讀者熟悉 Java 平臺上的企業開發,包括 JDBC、OR 映射內容、J2EE 設計模式如 DAO,以及聲明性事務支持,如 Enterprise JavaBean (EJB)技術所提供的事務支持。理解這里的討論不需要成為這些技術的專家,也不需要熟悉 AOP、IOC 或者 TDD,因為在本文中對這三者都做了介紹。
我將首先介紹兩種開發技術,然后分析例子。
Hibernate 簡介
Hibernate 是 Java 平臺上的一種全功能的、開放源代碼 OR 映射框架。Hibernate 在許多方面類似于 EJB CMP CMR (容器管理的持久性/容器管理的關系)和 JDO(Java Data Objects)。與 JDO 不同,Hibernate 完全著眼于關系數據庫的 OR 映射,并且包括比大多數商業產品更多的功能。大多數 EJB CMP CMR 解決方案使用代碼生成實現持久性代碼,而 JDO 使用字節碼修飾。與之相反,Hibernate 使用反射和運行時字節碼生成,使它對于最終用戶幾乎是透明的(以前 Hibernate 的實現只使用反射,它有助于調試,當前版本保留了這種選項)。
|
移植基于 Hibernate 的應用程序
如果應用程序必須在多個 RDBMS 系統上運行 ,那么基于 Hibernate 的應用程序可以毫不費力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和許多其他數據庫。我最近甚至將一個應用程序從 MySQL 移植到 Hibernate 沒有很好支持的 Firebird,而這種移植是很容易的。有關在 Postgres 和 MySQL 之間轉換的案例分析,請參閱
參考資料
。
|
|
Hibernate 可以模擬繼承(有幾種方式)、關聯(一對一或者一對多、containment 和 aggregation)和 composition。我將在本文中討論每種關系類型的幾個例子。
Hibernate 提供了一種稱為 Hibernate Query Language (HQL) 的 查詢語言,它類似于 JDO 的 JDOQL 和 EJB 的 EJB QL,盡管它更接近于前者。但是 Hibernate 沒有就此止步:它還可以進行直接的 SQL 查詢和/或使用 object criteria很容易地在運行時構成查詢條件。在本文的例子中我將只使用 HQL。
與 EJB CMP CMR 不同,Hibernate 像 JDO 一樣可以在 J2EE 容器內部或者外部工作,這可以讓那些進行 TDD 和敏捷開發的人受益。
Spring 簡介
AOP 專家 Nicholas Lesiecki 第一次向我解釋 AOP 時,他說的我一個詞也沒理解,我覺得就像第一次考慮使用 IOC 容器的可能性時一樣。每一種技術的概念基礎本身就需要很好地消化,每一種技術所使用的各種各樣的縮寫讓事情更糟了——特別是其中許多術語與我們已經使用的根本不一樣了。
像許多技術一樣,理解這兩種技術的實際使用比學習理論更容易。經過自己對 AOP 和 IOC 容器實現(即 XWork、PicoContainer 和 Spring)的分析,我發現這些技術可以幫助我獲得功能,而不會在多框架中添加基于代碼的依賴性。它們都將成為我后面開發項目的一部分。
簡單地說,AOP 讓開發人員可以創建非行為性的關注點,稱為橫切關注點,并將它們插入到應用程序代碼中。使用 AOP 后,公共服務(比如日志、持久性、事務等)就可以分解成方面并應用到域對象上,同時不會增加域對象的對象模型的復雜性。
|
關于 DbUnit
用新的框架開發而不進行單元測試,就像不帶保護網走鋼絲:當然可以這樣做,但是很可能會受傷。我選擇在有保護網的條件下開發,對我來說這個保護網就是 TDD。在有 DbUnit 之前,對依賴于數據庫的代碼進行測試是不太容易的。DbUnit 是 JUnit 的一個擴展,它提供了依賴于數據庫的單元測試的框架。我用 DbUnit 編寫本文中示例類的測試代碼。雖然在本文中沒有出現,不過在本文源代碼中提供了 DbUnit 代碼(請參閱
參考資料
)。有關 DbUnit 的介紹,請參閱 Philippe Girolami 的“
Control your test-environment with DbUnit and Anthill
” ( developerWorks,2004 年 4 月)。
|
|
IOC 允許創建一個可以構造對象的應用環境,然后向這些對象傳遞它們的協作對象。正如單詞 倒置 所表明的,IOC 就像反過來的 JNDI。沒有使用一堆抽象工廠、服務定位器、單元素(singleton)和直接構造(straight construction),每一個對象都是用其協作對象構造的。因此是由容器管理協作對象(collaborator)。
Spring 既是一個 AOP 框架、也是一個 IOC 容器。我記得 Grady Booch 說過,對象最好的地方是可以替換它們,而 Spring 最好的地方是它有助于您替換它們。有了 Spring,只要用 JavaBean 屬性和配置文件加入依賴性(協作對象)。然后可以很容易地在需要時替換具有類似接口的協作對象。
Spring 為 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是將要用 AOP 為示例應用程序聲明式地添加事務支持,與使用 EJB 技術時的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多內容,請參閱
參考資料
。
具體到業務
在本文的其余部分,所有的討論都將基于一個實際的例子。起點是一個企業應用程序,要為它實現一個事務持久層。持久層是一個對象關系數據庫,它包括像 User
、 User Group
、 Roles
和 ContactInfo
這些熟悉的抽象。
在深入到數據庫的要素——查詢和事務管理——之前,需要建立它的基礎:對象關系映射。我將用 Hibernate 設置它,并只使用一點 Spring。
用 Hibernate 進行 OR 映射
Hibernate 使用 XML ( *.hbm.xml) 文件將 Java 類映射到表,將 JavaBean 屬性映射到數據庫表。幸運的是,有一組 XDoclet
標簽支持 Hibernate 開發,這使得創建所需要的 *.hbm.xml 文件更容易了。清單 1 中的代碼將一個 Java 類映射到數據庫表。關于 XDoclet
標簽的更多內容,請參閱
參考資料
。
清單 1. 將 Java 類映射到 DB 表
[User.java]
/**
*
@hibernate.class table="TBL_USER"
* ..
* ..
* ...
*/
public class User {
private Long id = new Long(-1);
private String email;
private String password;
.
.
.
/**
* @return
*
@hibernate.id column="PK_USER_ID"
* unsaved-value="-1"
* generator-class="native"
*/
public Long getId() {
return id;
}
...
/**
*
@hibernate.property column="VC_EMAIL"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="82"
* @return
*/
public String getEmail() {
return email;
}
/**
*
@hibernate.property column="VC_PASSWORD"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="20"
* @return
*/
public String getPassword() {
return password;
}
...
...
...
}
|
可以看到, @hibernate.class table="TBL_USER"
標簽將 User
映射到 TBL_USER
表。 @hibernate.property column="VC_PASSWORD"
將 JavaBean 屬性 password 映射到 VC_PASSWORD
列。 @hibernate.id column="PK_USER_ID"
標簽聲明 id 屬性是主鍵,它將使用本機( generator-class="native"
)數據庫機制生成鍵(例如,Oracle sequences 和 SQL Server Identity 鍵)。Hibernate 可以指定 generator-class="native"
以外的、其他可以想象的得到主鍵獲得策略,不過我更愿意使用 native。 type 和 length屬性用于從 Hibernate *.hbm.xml OR 映射文件生成表。這些 final 屬性是可選的,因為使用的可能不是 green-field 數據庫。在這個例子中,已經有數據庫了,所以不需要額外的屬性。( green-field 應用程序是一個新的應用程序, green-field 數據是新應用程序的一個新數據庫。不會經常開發一個全新的應用程序,不過偶爾有一兩次也不錯)。
看過了表如何映射到類以及列如何映射到 JavaBean 屬性,該使用 Hibernate 在 OR 數據庫中設置一些關系了。
設置對象關系
在本節中,我將只觸及 Hibernate 提供的設置對象間關系的選項的一小部分。首先設置像 User
、 User Group
、 Roles
和 ContactInfo
這些類之間的關系。其中一些關系如圖 1 所示,這是數據庫的驗證對象模型。
圖 1. 關系的圖示
如您所見,在上述抽象中存在各種各樣的關系。 User
與 ContactInfo
有一對一關系。 ContactInfo
的生命周期與 User
相同(用數據庫的術語,UML 中的組成 aka 級聯刪除)。如果刪除 User
,則相應的 ContactInfo
也會刪除。在 User
s 與 Role
s 之間存在多對多關系(即與獨立生命周期相關聯)。在 Group
s 與 User
s 之間存在一對多關系,因為組有許多用戶。用戶可以存在于組外,即是 aggregation 而不是 composition (用數據庫的說法,在 Group
s 和 Users
之間沒有級聯刪除關系)。此外, User
和 Employee
有子類關系,就是說, Employee
的類型為 User
。表 1 顯示了如何用 XDoclet
標簽創建一些不同類型的對象關系。
表 1. 用 XDoclet 創建對象關系
關系
|
Java/XDoclet
|
SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
|
組包含用戶
一對多
Aggregation
雙向 (Group<-->Users)
|
[Group.java]
/** * * @return * * @hibernate.bag name="users" * cascade="save-update" * lazy="true" * inverse="true" * * @hibernate.collection-key * column="FK_GROUP_ID" * * @hibernate.collection-one-to-many * class="net.sf.hibernateExamples.User" */ public List getUsers() { return users; }
[User.java] /** * @hibernate.many-to-one * column="FK_GROUP_ID" * class="net.sf.hibernateExamples.Group" */ public Group getGroup() { return group; }
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_GROUP ( PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(255), VC_NAME VARCHAR(40) unique, primary key (PK_GROUP_ID) )
alter table TBL_USER add index (FK_GROUP_ID), add constraint FK_111 foreign key (FK_GROUP_ID) references TBL_GROUP (PK_GROUP_ID)
|
用戶有聯系信息
一對一
Composition 單向 (User-->ContactInfo)
|
[User.java]
/** * @return * * @hibernate.one-to-one cascade="all" * */ public ContactInfo getContactInfo() { return contactInfo; }
[ContactInfo.java] (Nothing to see here. Unidirectional!)
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_CONTACT_INFO ( PK_CONTACT_INFO_ID BIGINT not null, ... ... ... primary key (PK_CONTACT_INFO_ID) )
|
用戶與角色關聯
多對多
Association
單向 (Users-->Roles)
|
[User.java]
/** * @return * @hibernate.bag * table="TBL_JOIN_USER_ROLE" * cascade="all" * inverse="true" * * @hibernate.collection-key * column="FK_USER_ID" * * @hibernate.collection-many-to-many * class="net.sf.hibernateExamples.Role" * column="FK_ROLE_ID" * */ public List getRoles() { return roles; }
[Role.java] Nothing to see here. Unidirectional!
|
create table TBL_ROLE ( PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(200), VC_NAME VARCHAR(20), primary key (PK_ROLE_ID) )
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_JOIN_USER_ROLE ( FK_USER_ID BIGINT not null, FK_ROLE_ID BIGINT not null )
|
雇員是用戶
Inheritance
用戶
 雇員
|
[User.java]
/** * @hibernate.class table="TBL_USER" * discriminator-value="2" * @hibernate.discriminator column="USER_TYPE" * ... ... ... */ public class User {
[Employee.java] /** * @hibernate.subclass discriminator-value = "1" */ public class Employee extends User{
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
|
要了解在 Hibernate 中設置對象關系的更多內容,請參閱
參考資料
。
Hibernate 中的查詢
Hibernate 有三種類型的查詢:
-
Criteria, object composition
-
SQL
-
HQL
在下面的例子中將只使用 HQL。本節還要使用 Spring,用它的 AOP-driven HibernateTemplate 簡化 Hibernate 會話的處理。在本節將開發一個 DAO(Data Access Object)。要了解更多關于 DAO 的內容,請參閱
參考資料
。
清單 2 展示了兩個方法:一個使用 HQL 查詢的組查詢,另一個是后面接一個操作的組查詢。注意在第二個方法中,Spring HibernateTemplate 是如何簡化會話管理的。
清單 2. 使用查詢
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
*/
public class UserDAO extends HibernateDaoSupport{
.
.
.
/**
* Demonstrates looking up a group with a HQL query
* @param email
* @return
*/
public Group findGroupByName(String name) {
return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
}
/**
* Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
* @param email
* @return
*/
public Group findPopulatedGroupByName(final String name) {
HibernateCallback callback = new HibernateCallback(){
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Group group =null;
String query = "from Group g where g.name=?";
Query queryObject = getHibernateTemplate().createQuery(session, query);
queryObject.setParameter(0, name);
group = (Group) queryObject.list().get(0);
group.getUsers().size();//force load
return group;
}
};
return (Group) getHibernateTemplate().execute(callback);
}
.
.
.
}
|
您可能會注意到第二個方法比第一個方法復雜得多,因為它強迫加載 users
集合。因為 Group->Users
之間的關系設置為 lazy initialize(即表 2 中 lazy="true"
),組對象需要一個活躍的會話以查詢用戶。在定義 Group
和 User
s 之間關系時設置這個屬性為 lazy="false"
,則不需要第二個方法。在這種情況下,可能使用第一種方法 ( findGroupByName
) 列出組,用第二種方法( findPopulatedGroupByName
)查看組細節。
Spring IOC 和 Hibernate
使用 Spring 時,在 J2EE 容器內和容器外工作一樣容易。比如在最近的項目中,我在 Eclipse 中,使用 HSQL 和本地數據庫對使用 Hibernate 事務管理器的 Hypersonic SQL 數據庫進行持久性單元測試。然后,在部署到 J2EE 服務器時,將持久層轉換為使用 J2EE 數據源(通過 JNDI)、JTA 事務和使用 FireBird (一個開放源代碼版本的 Interbase)。這是用 Spring 作為 IOC 容器完成的。
從清單 3 中可以看出,Spring 允許加入依賴性。注意清單中應用程序上下文文件是如何配置 dataSource
的。 dataSource
傳遞給 sessionFactory
, sessionFactory
傳遞給 UserDAO
。
清單 3. Spring IOC 和 Hibernate
<beans>
<!-- Datasource that works in any application server
You could easily use J2EE data source instead if this were
running inside of a J2EE container.
-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
<property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Must references all OR mapping files. -->
<property name="mappingResources">
<list>
<value>net/sf/hibernateExamples/User.hbm.xml</value>
<value>net/sf/hibernateExamples/Group.hbm.xml</value>
<value>net/sf/hibernateExamples/Role.hbm.xml</value>
<value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
</list>
</property>
<!-- Set the type of database; changing this one property will port this to Oracle,
MS SQL etc. -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</beans>
|
設置了 UserDAO
后,下一步就是定義并使用更多的查詢以展示可以完成的操作。Hibernate 可以用預定義查詢將查詢存儲到源代碼之外,如清單 4 所示。
清單 4. 預定義查詢
[User.java]
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
* @hibernate.class table="TBL_USER" discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
*
* @hibernate.query name="OverheadStaff"
* query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
*
* @hibernate.query name="CriticalStaff"
* query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
*
* @hibernate.query name="GetUsersInAGroup"
* query="select user from Group g join g.users user"
*
* @hibernate.query name="GetUsersNotInAGroup"
* query="select user from User user where user.group is null"
*
* @hibernate.query name="UsersBySalaryGreaterThan"
* query="from User user inner join user.contactInfo info where info.salary > ?1"
*
* @hibernate.query name="UsersBySalaryBetween"
* query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
*
* @hibernate.query name="UsersByLastNameLike"
* query="from User user join user.contactInfo info where info.lastName like ?1"
*
* @hibernate.query name="GetEmailsOfUsers"
* query="select user.email from Group g join g.users as user where g.name = ?1"
*
*/
public class User {
.
.
.
|
上述代碼定義了幾個預定義查詢。 預定義查詢 是存儲在 *.hbm.xml文件中的查詢。在清單 5 中,可以看到如何執行預定義查詢。
清單 5. 使用預定義查詢
[UserDAO.java]
/**
* Demonstrates a query that returns a String.
*/
public String[] getUserEmailsInGroup(String groupName){
List emailList =
getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
return (String [])
emailList.toArray(new String[emailList.size()]);
}
/**
* Demonstrates a query that returns a list of Users
*
* @return A list of emails of all of the users in the authentication system.
*
*/
public List getUsers(){
return getHibernateTemplate().findByNamedQuery("AllUsers");
}
/**
* Demonstrates passing a single argument to a query.
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalary(float salary){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryGreaterThan",
new Float(salary));
}
/**
* Demonstrates passing multiple arguments to a query
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalaryRange(float start, float stop){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryBetween",
new Object[] {new Float(start), new Float(stop)});
}
|
查詢進行時,可以在持久層中加上最后一層:使用 Spring 的事務管理。
用 Spring 管理事務
Spring 可以聲明式地管理事務。例如, UserDAO.addUser
方法當前不是在單個事務中執行的。因此,組中的每一個用戶都插入到自己的事務中,如清單 6 所示。
清單 6. 添加一組用戶
[UserDAO.java]
/**
* @param group
*/
public void addGroup(Group group) {
getHibernateTemplate().save(group);
}
[UserDAOTest.java]
public void testAddGroupOfUsers(){
Group group = new Group();
for (int index=0; index < 10; index++){
User user = new User();
user.setEmail("rick"+index+"@foobar.com" );
user.setPassword("foobar");
group.addUser(user);
}
group.setName("testGroup");
userDAO.addGroup(group);
assertNotNull(group.getId());
Group group2 = userDAO.findPopulatedGroupByName("testGroup");
assertEquals("testGroup",group2.getName());
assertEquals(10, group2.getUsers().size());
String email = ((User)group2.getUsers().get(0)).getEmail();
assertEquals("rick0@foobar.com", email);
}
|
不建議使用上述解決方案,因為每一個 User
都要在自己的事務中插入到數據庫中。如果出現問題,那么只能添加部分用戶。如果希望保留 ACID 屬性(即保證所有都發生或者所有都不發生),可以通過程序進行事務管理,但是它很快就會變得一團糟了。相反,應使用 Spring 的 AOP 來支持聲明式的事務,如清單 7 所示。
清單 7. 聲明式管理事務
[applicationContext.xml]
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
</bean>
<bean id="userDAO"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="target"><ref local="userDAOTarget"/></property>
<property name="transactionAttributes">
<props>
<prop key="addGroup">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
注意在準備清單 7 的代碼時,我重新改寫了 UserDAO
并提取了其接口。這個接口現在是 UserDAO
,它的實現類是 UserDAOImpl
。這樣清單 7 中的事務代碼就使用了帶有事務屬性 (PROPAGATION_REQUIRED)
的 UserDAO.addGroup()
方法。現在只要底層數據庫支持,就可以在一個事務中添加所有用戶。
結束語
在本文中,介紹了如何使用 Hibernate 和 Spring 實現一個事務持久層。Hibernate 是一種先進的 OR 映射工具,而 Spring 是一個 AOP 框架和 IOC 容器。這兩種技術的綜合使用,使得開發人員可以編寫媲美數據庫廠商的代碼,它可以在 J2EE 容器中運行,也可以單獨運行。使用了 DbUnit (JUnit 的擴展)構建和測試本文中例子的所有代碼,雖然這不是討論的重點。
要了解有關 AOP、IOC 容器和測試驅動開發的更多內容,請參閱
參考資料
。
posted on 2006-05-18 20:03
崛起的程序員 閱讀(603)
評論(0) 編輯 收藏 所屬分類:
java