轉自:http://kyfxbl.iteye.com/blog/1634355
論壇上有另外一篇更全面的帖子,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
- 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映射用的是配置文件,其實用註解也差不多
這一行:
- <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);
改成
- String hql = "from User u where u.name = :name";
- Query query = this.getSession().createQuery(hql);
- query.setParameter("name", name);
鑑於自動回滾這個特性很方便,對Service層組件進行單元測試的時候,也推薦加上@Transactional註解
對於spring3和hibernate4的集成,本文就簡單介紹到這裏,歡迎補充