論壇上有另外一篇更全面的文章,jinnianshilongnian寫的:http://www.iteye.com/topic/1120924
本文的環境是:
spring-framework-3.1.0
hibernate-4.1.6
junit-4.10
這裡大部分是參考我以前熟悉的配置方法,隻是把hibernate3更新到hibernate4,其實差不了很多,隻要注意幾個要點:
1、以前內建hibernate3和spring的時候,spring的ORM包裡提供了HibernateSupport和HibernateTemplate這兩個輔助類,我用的是HibernateTemplate。不過在Hibernate4裡,spring不再提供這種輔助類,用的是hibernate4的原生API
2、內建hibernate4之後,最小事務級别必須是Required,如果是以下的級别,或者沒有開啟事務的話,無法得到目前的Session
Java代碼

- sessionFactory.getCurrentSession();
執行這行代碼,會抛出No Session found for current thread
對于運作時,這個可能不是很大的問題,因為在Service層一般都會開啟事務,隻要保證級别高于Required就可以了。可是由于在Dao層是不會開啟事務的,是以針對Dao層進行單元測試就有困難了。
解決的辦法是,或者在Dao層的單元測試類上,開啟事務。或者專門準備一個for unit test的配置檔案,在Dao層就開啟事務。我采用的是前者
首先是目錄結構,這裡暫時還沒有內建struts2、spring-mvc等web架構,也尚未包含js、css、jsp等目錄
這裡除了servlet規範規定的web.xml必須放在WEB-INF下之外,其他的所有配置檔案,都放在src根目錄下。這樣做的好處是,後續所有需要引用配置檔案的地方,都可以統一用classpath:字首找到配置檔案。之前試過有的檔案放在WEB-INF下,有的放在src根目錄下,是以在引用的地方會不太統一,比較麻煩。
當然無論配置檔案怎麼放,隻要恰當使用classpath:和file:字首,都是能找到的,隻是個人選擇的問題。另外,由于現在配置檔案還比較少,是以直接扔到src根目錄下沒什麼問題,如果配置檔案增多了,可以再進行劃分
接下來是web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>DevelopFramework</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:beans.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>CXFServlet</servlet-name>
<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/webservice/*</url-pattern>
</servlet-mapping>
</web-app>
這裡沒有什麼要特别注意的,隻是聲明了beans.xml的路徑。這裡的servlet是配置cxf的,與hibernate沒有關系。因為目标是要搭一個完整的開發架構,是以把cxf也事先放上了
接下來是spring的配置檔案beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd">
<import resource="classpath:META-INF/cxf/cxf.xml" />
<context:component-scan base-package="com.huawei.inoc.framework" />
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
</list>
</property>
</bean>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${driverClass}" />
<property name="jdbcUrl" value="${jdbcUrl}" />
<property name="user" value="${user}" />
<property name="password" value="${password}" />
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingLocations" value="classpath:/com/huawei/inoc/framework/model/**/*.hbm.xml" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">25</prop>
<prop key="hibernate.temp.use_jdbc_metadata_defaults">false</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<jaxws:endpoint id="helloWorld" implementor="#helloWorldWebserviceImpl" address="/HelloWorld" />
<jaxws:client id="client"
serviceClass="com.huawei.inoc.dummy.webservice.IDemoSupport"
address="http://localhost:8080/Dummy/webservice/getDate" />
</beans>
這裡有幾點要注意的:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${driverClass}" />
<property name="jdbcUrl" value="${jdbcUrl}" />
<property name="user" value="${user}" />
<property name="password" value="${password}" />
</bean>
這裡把jdbc驅動的參數,放到了專門的配置檔案裡,改動起來會比較友善。另外資料庫連接配接池在實際生産環境可以考慮切換一下,比如聽說阿裡巴巴出的druid就挺不錯,jboss和WAS自帶的連接配接池也是不錯的
<bean id="sessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingLocations" value="classpath:/com/huawei/inoc/framework/model/**/*.hbm.xml" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">25</prop>
<prop key="hibernate.temp.use_jdbc_metadata_defaults">false</prop>
</props>
</property>
</bean>
這裡的sessionFactory改成org.springframework.orm.hibernate4.LocalSessionFactoryBean,如果ORM映射采用的不是配置檔案,是用注解的話,以前hibernate3有一個AnnotationSessionFactoryBean,在hibernate4裡沒看到
這裡ORM映射用的是配置檔案,其實用注解也差不多
這一行:
Xml代碼

- <prop key="hibernate.temp.use_jdbc_metadata_defaults">false</prop>
可以避免啟動容器時報的一個錯誤:
Disabling contextual LOB creation as createClob() method threw error : java.lang.reflect.InvocationTargetException
這個錯誤其實是無所謂的,不過還是不要報錯好看一點
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
這裡是開啟事務,用的是注解,比用配置檔案簡單一點。
用配置檔案的好處,是事務聲明比較集中,不需要在每個Service層接口上單獨聲明。缺點是Service中的方法,命名規範需要事先約定好,否則事務就不能生效
用注解的好處,是Service中的方法命名不需要特别規定,缺點是沒有做到集中聲明,如果在某個Service層的接口忘記聲明事務,那麼事務就無法生效
兩種方法各有好處,我個人更喜歡用注解
然後是DAO層的結構
首先有一個通用的DAO接口,然後有一個通用的DAO抽象實作類。每個具體業務DAO接口,繼承通用DAO接口,具體業務DAO實作,繼承通用DAO抽象實作類
public interface IGenericDAO<T> {
void insert(T t);
void delete(T t);
void update(T t);
T queryById(String id);
List<T> queryAll();
}
因為隻是示例,這裡的方法不是很多,隻包含了基本的增删改查方法
public abstract class GenericDAO<T> implements IGenericDAO<T> {
private Class<T> entityClass;
public GenericDAO(Class<T> clazz) {
this.entityClass = clazz;
}
@Autowired
private SessionFactory sessionFactory;
@Override
public void insert(T t) {
sessionFactory.getCurrentSession().save(t);
}
@Override
public void delete(T t) {
sessionFactory.getCurrentSession().delete(t);
}
@Override
public void update(T t) {
sessionFactory.getCurrentSession().update(t);
}
@SuppressWarnings("unchecked")
@Override
public T queryById(String id) {
return (T) sessionFactory.getCurrentSession().get(entityClass, id);
}
@Override
public List<T> queryAll() {
String hql = "from " + entityClass.getSimpleName();
return queryForList(hql, null);
}
@SuppressWarnings("unchecked")
protected T queryForObject(String hql, Object[] params) {
Query query = sessionFactory.getCurrentSession().createQuery(hql);
setQueryParams(query, params);
return (T) query.uniqueResult();
}
@SuppressWarnings("unchecked")
protected T queryForTopObject(String hql, Object[] params) {
Query query = sessionFactory.getCurrentSession().createQuery(hql);
setQueryParams(query, params);
return (T) query.setFirstResult(0).setMaxResults(1).uniqueResult();
}
@SuppressWarnings("unchecked")
protected List<T> queryForList(String hql, Object[] params) {
Query query = sessionFactory.getCurrentSession().createQuery(hql);
setQueryParams(query, params);
return query.list();
}
@SuppressWarnings("unchecked")
protected List<T> queryForList(final String hql, final Object[] params,
final int recordNum) {
Query query = sessionFactory.getCurrentSession().createQuery(hql);
setQueryParams(query, params);
return query.setFirstResult(0).setMaxResults(recordNum).list();
}
private void setQueryParams(Query query, Object[] params) {
if (null == params) {
return;
}
for (int i = 0; i < params.length; i++) {
query.setParameter(i, params[i]);
}
}
}
這個抽象類實作了IGenericDAO的所有方法,具體業務DAO的實作類,就不需要重複實作這些方法了。
這裡因為session.get()和session.load()方法,都需要傳入一個Class類型的參數,是以定義了entityClass字段,在具體業務類的構造方法中傳入,下面會看到。另外有一個辦法是用反射的方法,來擷取entityClass字段,就不需要在具體子類的構造方法中再傳入了。不過我個人覺得傳入也不是很麻煩,就沒有這麼做
這個類除了實作了IGenericDAO裡定義的public方法之外,還提供了protected的queryForObject()和queryForList()方法,可以為具體子類提供一些便利
這個通用DAO還不是很完善,主要是還可以補充更多的方法,以及考慮分頁。為了簡化的需要,這裡省略了
public interface IUserDAO extends IGenericDAO<User> {
public User queryByName(String userName);
}
是具體業務DAO的接口,除了通用的方法之外,增加了一個按照name查詢的方法,是以就要單獨定義此方法
@Repository
public class UserDAO extends GenericDAO<User> implements IUserDAO {
public UserDAO() {
super(User.class);
}
@Override
public User queryByName(String userName) {
String hql = "from User u where u.name = ?";
return queryForObject(hql, new Object[] { userName });
}
}
這是具體業務DAO的實作類,實作了接口裡的queryByName()方法,并且在構造參數中傳入了User.class,用于初始化GenericDAO裡的entityClass字段
此外,這個類需要用@Repository注解,聲明為spring bean
DAO層裡是不能聲明事務的,也不能自行捕獲異常,如果有特殊需求必須捕獲的話,也要在處理之後,重新抛出來。否則Service層的事務就失效了
接下來是Service層
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public interface IBookService {
void addBook(Book book);
}
隻要在接口上用@Transactional注解,此接口内的所有方法就自動聲明為事務了,方法即是事務的邊界。
注意事務是在接口上聲明的,一般不在實作類上聲明
後面的propagation參數,至少要到REQUIRED,否則No Session found for current thread,我也不知道這算不算一個BUG,還是spring認為是一個強制要求
@Service
public class BookService implements IBookService {
@Autowired
private IBookDAO bookDAO;
@Override
public void addBook(Book book) {
bookDAO.insert(book);
}
}
這個Service的實作類就很簡單了,不需要重複聲明事務,但是需要用@Service注解将自身聲明為一個spring bean(因為可能還會注入上層),另外用@Autowired注解,将之前聲明的DAO注入
接下來說明一下單元測試的方法,在想做單元測試的類上,用右鍵菜單New-->JUnit Test Case
這裡要注意Source folder選到test,不然就會生成到src目錄下了,然後可以視情況勾選setUp()
生成的單元測試類
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:beans.xml")
@Transactional
public class BookDAOTest {
@Autowired
private BookDAO bookDAO;
@Test
public void testQueryByIsbn() {
String isbn = "123";
Book result = bookDAO.queryByIsbn(isbn);
String name = result.getName();
assertEquals("thinking in java", name);
}
@Test
public void testInsert() {
Book book = new Book();
book.setName("bai ye xing");
book.setIsbn("be bought yesterday");
bookDAO.insert(book);
}
@Test
public void testDelete() {
String id = "test_1";
Book target = bookDAO.queryById(id);
bookDAO.delete(target);
}
@Test
public void testUpdate() {
String id = "test_1";
Book target = bookDAO.queryById(id);
target.setName("i am changeid");
bookDAO.update(target);
}
@Test
public void testQueryById() {
String id = "test_1";
Book target = bookDAO.queryById(id);
String name = target.getName();
assertEquals("thinking in java", name);
}
@Test
public void testQueryAll() {
List<Book> books = bookDAO.queryAll();
assertEquals(3, books.size());
}
}
注解為@Test的方法,會被認為是單元測試方法被執行,注解為@Before的方法,會在每個單元測試方法執行之前被執行

- @Autowired
- private BookDAO bookDAO;
這裡是把要單元測試的目标類注入進來
下面重點介紹一下類上面的幾個注解:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:beans.xml")
加上@RunWith注解之後,單元測試類會在spring容器裡執行,這會帶來很多便利。
@ContextConfiguration注解,可以指定要加載的spring配置檔案路徑。如果對spring配置檔案進行了恰當的拆分,就可以在不同的單元測試類裡,僅加載必要的配置檔案
@Transactional
這行注解是最關鍵的,前面已經提到,因為在DAO層是沒有聲明事務的,是以如果直接執行的話,就會抛出No Session found for current thread
是以需要加上這句注解,在執行單元測試時,開啟事務,就可以規避這個問題。同時也不會影響到實際的事務
此外還引入了一個額外的好處,就是加上了這個注解之後,單元測試對資料庫的改動會被自動復原,避免不同單元測試方法之間的耦合。這個特性在實際跑單元測試裡是很友善的
實際運作一下這個單元測試類,可以在控制台看到以下輸出
2012-8-16 19:37:42 org.springframework.test.context.transaction.TransactionalTestExecutionListener startNewTransaction
資訊: Began transaction (1): transaction manager [org.springframework.orm.hibernate4.HibernateTransactionManager@183d59c]; rollback [true]
1903 [main] WARN o.h.hql.internal.ast.HqlSqlWalker - [DEPRECATION] Encountered positional parameter near line 1, column 60. Positional parameter are considered deprecated; use named parameters or JPA-style positional parameters instead.
Hibernate:
select
book0_.ID as ID0_,
book0_.NAME as NAME0_,
book0_.ISBN as ISBN0_
from
developframeworkschema.book book0_
where
book0_.ISBN=?
2012-8-16 19:37:42 org.springframework.test.context.transaction.TransactionalTestExecutionListener endTransaction
資訊: Rolled back transaction after test execution for test context
每個方法開始之前,都會開啟一個新事務,在執行完畢之後,該事務都會被復原
其中還有一行警告資訊:[DEPRECATION] Encountered positional parameter near line 1, column 60. Positional parameter are considered deprecated; use named parameters or JPA-style positional parameters instead.
這是因為在GenericDAO中采用了hibernate4不推薦的寫法:
private void setQueryParams(Query query, Object[] params) {
if (null == params) {
return;
}
for (int i = 0; i < params.length; i++) {
query.setParameter(i, params[i]);
}
}
hibernate4的建議,是把
String hql = "from User u where u.name = ?";
Query query = sessionFactory.getCurrentSession().createQuery(hql);
query.setParameter(0, name);
改成
Java代碼 收藏代碼
String hql = "from User u where u.name = :name";
Query query = this.getSession().createQuery(hql);
query.setParameter("name", name);
鑒于自動復原這個特性很友善,對Service層元件進行單元測試的時候,也推薦加上@Transactional注解
對于spring3和hibernate4的內建,本文就簡單介紹到這裡,歡迎補充