天天看點

內建spring3,hibernate4

論壇上有另外一篇更全面的文章,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代碼  

內建spring3,hibernate4

  1. sessionFactory.getCurrentSession();  

執行這行代碼,會抛出No Session found for current thread

對于運作時,這個可能不是很大的問題,因為在Service層一般都會開啟事務,隻要保證級别高于Required就可以了。可是由于在Dao層是不會開啟事務的,是以針對Dao層進行單元測試就有困難了。

解決的辦法是,或者在Dao層的單元測試類上,開啟事務。或者專門準備一個for unit test的配置檔案,在Dao層就開啟事務。我采用的是前者

首先是目錄結構,這裡暫時還沒有內建struts2、spring-mvc等web架構,也尚未包含js、css、jsp等目錄

內建spring3,hibernate4

這裡除了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代碼  

內建spring3,hibernate4
  1. <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層的結構

內建spring3,hibernate4

首先有一個通用的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

內建spring3,hibernate4

這裡要注意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的方法,會在每個單元測試方法執行之前被執行

內建spring3,hibernate4
  1. @Autowired  
  2.     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的內建,本文就簡單介紹到這裡,歡迎補充