星期日, 5月 15, 2005

use AOP to simplify hibernateTemplate (Hibernate3)

Spring ORM 替 Hibernate 的 API (Session and Query) 做了一番修整,也就是 HibernateTemplate ,它最主要的目的是 resource 控管及 exception 的轉換。有了這個 persistence layer 的 code 就可以大量的減少並且降低出錯的機會。然而這個 API 卻抹剎了 Hibernate API 的簡單易用優點,舉例來說,如果藉由 hibernateTemplate 控制 Hibernate Session (ex. Criteria),唯一的做法就是寫很難看的 Callback:

public MyData findByNo(final String no) {
final String hql = " select data from MyData data "
+ " where data.no = :no ";

HibernateCallback hc = new HibernateCallback() {
public Object doInHibernate(Session session) {
Query q = session.createQuery(hql);
q.setString("no", no);
return q.uniqueResult();
}
};

return (MyData) hibernateTemplate.execute(hc);
}

inner class + final.... 有夠難看。下面要介紹的是,利用 Spring AOP 的 AutoProxy 的功能,自動將 DAO 的所有 method 用 HibernateCallback 包起來。如此一來,便可以自由在 DAO 中自由存取 Hibernate Session 了:

總共需要三個 class:
第一個是 Advice,也就是將所有 method 都先用 HibernateCallback 包起來,然後將 Hibernate Session 放入 HibernateDAOWarpper 裡。

public class HibernateDAOWrapperMethodInterceptor
implements MethodInterceptor {

private HibernateTemplate hibernateTemplate;

public Object invoke(final MethodInvocation methodInvocation)
throws Throwable {

try {
return hibernateTemplate.execute(new HibernateCallback() {

public Object doInHibernate(Session session)
throws HibernateException, SQLException {
HibernateDAOWrapper.currentSession.set(session);
try {
return methodInvocation.proceed();
} catch (Throwable e) {
if (e instanceof HibernateException) {
throw (HibernateException) e;
} else if (e instanceof SQLException) {
throw (SQLException) e;
} else {
//must re-throw as RuntimeException, and let
//hibernateTemplate deal with resource management
throw new ExceptionCarrier(e);
}
}
}

});
} catch (ExceptionCarrier e) {
throw e.throwable;
}

}

private static class ExceptionCarrier extends RuntimeException {
private Throwable throwable;

public ExceptionCarrier(Throwable t) {
this.throwable = t;
}
}

public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
}
接下來就是 AutoProxyCreator,當它遇到 bean 的 class 為 HibernateDAOWrapper 的 subclass 時,它就會自動替該 bean 做 proxy。

public class HibernateDAOWrapperAutoProxyCreator extends
AbstractAutoProxyCreator {

protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass,
String beanName, TargetSource customTargetSource)
throws BeansException {
if (HibernateDAOWrapper.class.isAssignableFrom(beanClass)) {
return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS;
} else {
return DO_NOT_PROXY;
}
}
}

有了這個,再搭配上面的 Advice,我們就可以將所有 HibernateDAOWrapper 的 subclass 的所有 method 都用 HibernateCallback 包起來。Spring 的設定如下:

<bean id="hibernateDAOWrapperAutoProxyCreator"
class="org.bioinfo.util.spring.dao.HibernateDAOWrapperAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>hibernateDAOWrapperMethodInterceptor</value>
</list>
</property>
<!-- for use CGlib to create proxy,
(default to false, which will use Proxy if it detects interface presence) -->
<property name="proxyTargetClass">
<value>true</value>
</property>
</bean>
<bean id="hibernateDAOWrapperMethodInterceptor"
class="org.bioinfo.util.spring.dao.HibernateDAOWrapperMethodInterceptor">
<property name="hibernateTemplate">
<ref local="hibernateTemplate"/>
</property>
</bean>

這個只要設一次就夠了。接下來我們再來看看主角:

/**
* A special HibernateTemplate wrapper for DAO. You need to do:
*
* (1) Extends this class and use getSession() to obtain Hibernate3 Session
*
* <code>
* public class YourDAO extends HibernateDAOWrapper {
*
* public MyProject findByProjectCode(String code) {
* String hql = "select prj from MyProject prj where prj.code = :code";
*
* return (MyProject) getSession().createQuery(hql)
* .setString("code",code").uniqueResult();
* }
* }
* </code>
*
* (2) At your spring *.xml, just write one line:
*
* <code>
* <bean id="yourDAO" class="some.thing.YourDAO"></bean>
* </code>
*
* done !!
* @author ingram
*
*/
public abstract class HibernateDAOWrapper {

final static ThreadLocal currentSession = new ThreadLocal();

protected final Session getSession() {
return (Session) currentSession.get();
}
}

呵,小的可憐,它的功能就是提供 getSession() 的 method 給 subclass 的 DAO 而已。這個 session 來自於 Advice,並且放在 ThreadLocal裡,因此是 thread-safe 的。使用方式就如上面的 javadoc 所說,將 DAO 直接 subclass 這個 Wrapper,並在 spring 裡定義即可。

這樣一來 DAO 就可以快快樂樂的用 Hibernate API 啦,爽!

(註:僅適用 Hibernate3,於 Hibernate 3.0.3 + Spring 1.2 final 測試 )

星期六, 5月 14, 2005

(draft v2) Best Practice for Struts+Hibernate+Spring

最近又看了一本 Pro Spring,喔~ 裡面的第十一章 (Designing and implementating spring-based applications) 真是太棒囉!很多建議都相當的實用,解決了許多困惑我很久的疑問。這樣子差不多可以整理出一個通用的開發架構了:

o J2EE without EJB
採用 Struts + Hibernate +Spring 三個 framework 開發專案,每個framework都提供較 j2ee 更簡單、更快速、更open的實作。
  • Struts 提供 presentation layer 的各項服務
  • Hibernate 提供 domain layer 的 persistence 及 query 服務
  • Spring 提供 transaction/mail/remoting... 等等 middle-ware 的服務
o Writing unit test for everything
Struts Action/ActionForm, Hibernate DAO, Spring Service.... etc 從上到下所有的 java code 都依循 TDD 的方式開發。目前除了 jsp 之外,全部都要寫 unit test。

o 4 layers
  • Presentation Layer - 與使用者互動
  • Application Service Layer - 此層可明確表示 use case / user story / user requirement
  • Domain Layer - domain problem mapping as domain objects interaction.
  • Persistence Layer - 負責物件的儲存及查詢. (物件的中間生命週期)
o 保持 domain layer 乾淨
  • no DAO
    • 進入 domain layer 前,先將資料轉換為 domain object,無論是從 database 來或是使用者輸入。
    • domain object 間的 association,如果需要從 database query,盡可能改用 ORM 直接做associate ,而不是再透過 DAO 才連結。(除非 performance 的考量)
  • no Service
    • domain object 不應與任何 framework, container, context, enviroment... etc 等相關。所有存在於Domain Layer 的物件僅僅是互相連結的 POJO。
o Domain Object >= Persistence Object
  • Domain Object is not nessasary identical with Persistence Object(PO). just choose domain objects that need to persistence and query as PO.
  • All Persistence Object's getter/setter should be in default package scope.
o Design Domain Object fine grain and immutable
  • Do not afraid introduceing new Class.
  • Prefer immutable object. Immutable Object enforce you mapping domain problem to domain object correctly, and reduce maintain effort. Only Persistence Object has mutable characteristic.
  • no getter/setter. Object's property is hidden and only expose behavior to public.
  • no Singleton and no static. If you find something can apply singleton pattern, or design as static. It may be not belong to domain layer.
o Domain Object 跨全域
  • Domain Object 會傳到 presentation layer 與使用者互動,會傳到 application layer 使用 middle-ware 服務、會扮演 PO 進入 persistence layer 儲存。
  • 安全地跨全域的關鍵在於 Immutable。
  • DTO is non-sense.
o Classify Root of Domain Object in domain layer
  • ex. A Car has engine/wheel... etc. A engine can not work or sell directly. so typically Car class is Root of car/engine/wheel.
  • ex. An engine Factory produce various engines and sell them to down-stream factory. Obviously now the engine is Root class for this domain.
o Service Layer:
  • thin business logic
  • coordinate:
    • Root of Domain Object
    • DAO
    • Other middle-ware service (mail/remoting/transaction)
  • each method is unit of work of user's single operation
  • method boundary is transaction boundary, declare in Spring.
  • Spring managed resources are all in this layer.
  • Apply IoC in this layer heavily to make test easier. (flexible design)
  • Every service is a interface and pair with at least one implementation: FooService/FooServiceImpl
o Persistence Layer:
  • Not all database table has it's own DAO, only Root of Domain Object has DAO
  • See Hibernate 3 Reference chap. 7 to choose best association for your ORM
  • Efficiently use Hibernate's optimistic lock feature for concurrecy issue.
  • Efficiently use Hibernate's lazy/cache/fetch join technique to improve ORM performance
  • All POs have an primary key: 'Long id'. DO NOT use compose key !
  • All POs' getter/setter scope is package default (private is better)
  • Perfer HibernateTemplate and JdbcTemplate, with this we don't require additional interface for DAOs.
o Struts ActionForm:
  • ActionForm will be populated from Domain Object for user input.
  • After user input, the ActionForm should convert user's input back to domain object if possible.
  • It's ok that treat ActionForm as Domain Object and transfer into service layer, but use with care because ActionForm is highly mutable. If you want to protect Domain Layer and reuse ActionForm's code, Just let ActionForm implement Domain Object interface.
o Struts Action:
  • each Action only call one method of a single service. (because of transaction boundary)
  • Action's responsibility is to coordinate Invoking service method, ActionForm population, ActionForward flow, and compose ActionMessage... etc, nothing more.
  • does nothing about business logic.
o Design Business Exception:
  • All business exceptions are checked and user recoverable. if the exception is user un-recoverable, it should be classified as RuntimeException
  • All business exception should extends 'BusinessException', the Root of all business exceptions
  • If you want your business exception support i18n, use 'ResourceKeyBusinessException', which extends from 'BusinessException'
  • Each module only has one its own Root business exception, and all rest of exceptions for this module should extends its own Root business exception
  • This is checked exception, but you will not try to catch it except in Struts Action. business exception is 'user' recoverable, so it should always throw away until Struts Action. Struts Action catch Business Exception, convert to meaningful message and forward user to error page.
好像越寫越多... 看起來亂亂的... 而且英文佔了一大半 -_-;
啊,就先這樣就好了,以後有空在補。