20 September 2005

Stateful 魔人 Gavin King 終於完成了他的夢想:Seam。JBoss Seam 昨天在 The Server Side 上面發表 Beta 1.0。當然啦這不是 Gavin King 一人的傑作,我只是取個聳動的名字 :-P

如果你熟悉 Hibernate Session 的運作,應該能體會 Hibernate Team 極欲推廣Application Transaction的理念。Application Transaction 不是一般的 database transaction,而是從使用者的角度來看整個 "use case"。整個 use case 做完,對 user 來說才算是真的commit了,這中間可能橫跨了數個網頁,數個 db transaction。這句 "數個 db transaction" 是重點,按一般設計的原則,我們會在 commit 完 db transaction 之後,就馬上關閉 db connection 和 Hibernate Session。然而 Hibernate Session 並非適合如此開開關關,常開常關的結果就是常看到 LazyInitialization/NonUniqueObjectException 之類的錯誤,這一點用過 Hibernate 的人在熟悉不過了,而且也是痛苦的來源之一。Hibernate Session 的開關長度其實等同於 Application Transaction;讓 Session 在整個 use case 中都不用關閉,自然不會再遇到那些難搞的 Exception。上面的理念在 Hibernate in Action 中有詳細的討論

每一個 request 之間要互相關聯,聯系,必需要由一個以上的 state 來記錄、追蹤各個 request 的狀態,如此才能串連起來。這樣子的設計稱為 Stateful

ok, 如果我們接受了 Application Transaction (AppTx) 的理念,那麼下個問題就是何時打開 AppTx,何時關閉它。因為它跨 request,所以可不能像 open-session-in-view 那樣,掛個 filter 就搞定了。我們需要一個 stateful 的元件記錄何時算開始,何時算結束。從這裡我們也不難理解為什麼 Hiberante Team 會看上 Stateful Session Bean (SFSB) -- 由 Container 控管生命週期,會適時的加以啟動與關閉,如此就不會有 resource (Hibernate Session) 沒有機會關閉的問題。把這個 "Stateful + 適時關閉 resource" 的觀念加以衍生擴大就是 JBoss Seam。

Anyway。回正題... 讀了一些 Seam 的 document,發現它真的是個完完全全 Stateful 的 framework,這跟 Spring Framework 傾向 stateless 的設計方式完全的不同。來看看他的範例程式吧:

@Stateful                                                                                (1)
@Name("hotelBooking")
@Interceptor(SeamInterceptor.class)
@Conversational(ifNotBegunOutcome="main")                                                (2)
@LoggedIn                                                                                (3)
public class HotelBookingAction implements HotelBooking, Serializable {
   private static final Logger log = Logger.getLogger(HotelBooking.class);

   @PersistenceContext(type=EXTENDED)                                                    (4)
   private EntityManager bookingDatabase;

   private String searchString;

   @DataModel                                                                            (5)
   private List
   
     hotels;
   @DataModelSelectionIndex                                                              (6)
   private int hotelIndex;

   @Out(required=false)                                                                  (7)
   private Hotel hotel;

   @In(required=false)
   @Out(required=false)
   @Valid
   private Booking booking;

   @In
   private User user;

   @In
   private transient FacesContext facesContext;

   @Begin                                                                                (8)
   public String find()   {
      hotel = null;
      String searchPattern = searchString==null ? "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
      String searchQuery = "from Hotel where lower(city) like :search" +
                           " or lower(zip) like :search" +
                           " or lower(address) like :search";
      hotels = bookingDatabase.createQuery(searchQuery)
            .setParameter("search", searchPattern)
            .setMaxResults(50)
            .getResultList();

      log.info(hotels.size() + " hotels found");

      return "main";
   }

   public String getSearchString() {
      return searchString;
   }

   public void setSearchString(String searchString){
      this.searchString = searchString;
   }

   public String selectHotel(){...}
   public String nextHotel(){...}
   public String lastHotel(){...}
   private void setHotel(){...}

   public String bookHotel(){
      if (hotel==null) return "main";
      booking = new Booking(hotel, user);
      booking.setCheckinDate( new Date() );
      booking.setCheckoutDate( new Date() );
      return "book";
   }

   @IfInvalid(outcome=REDISPLAY)
   public String setBookingDetails(){
      if (booking==null || hotel==null) return "main";
      if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) ){
         log.info("invalid booking dates");
         FacesMessage facesMessage =
           new FacesMessage("Check out date must be later than check in date");
         facesContext.addMessage(null, facesMessage);
         return null;
      }
      else{
         log.info("valid booking");
         return "success";                                                               (9)
      }
   }

   @End
   public String confirm(){
      if (booking==null || hotel==null) return "main";
      bookingDatabase.persist(booking);
      log.info("booking confirmed");                                                     (10)
      return "confirmed";
   }

   @Destroy @Remove
   public void destroy() {
      log.info("destroyed");
   }
}

   
  • 終於出現了!!Annotation Monster!看看第一行寫的: @Stateful @Name @Interceptor @Conversational @LoggedIn .... 這是啥?這個擺明的就是單一 Class 負責太多事情了!先扣個十分再說。
  • 這個 class 裡面有 EntityManager + SQL 與 FaceContext... Oh My God, 大鍋炒!什麼都混在一起了!要混在一起我乾脆寫 servlet 就好了,還用高級的 EJB Application Server 幹嘛?再扣個十分。
  • 總共有 Stateless context, Event (or request) context, Conversation context,Session context,Business process context ,Application context 等等 六種 context.... 乖乖,JBoss 你真的有想要簡化開發?再扣個十分。
  • 拜 Stateful 之賜,user, booking, hotels...等等都出現在 instance variable 了,雖然都是 instance variable,其實他們的 life 並不是等同於這個 class 的 life 長度。它們的 scope 取決於 inject與 outject (統稱 Bijection) 的 context,如果是放在 event context,那下個 request 就不見了。inject /outject 說穿了只是 context.set("foo",foo), context.get("foo"),跟一般 session.setAttrubuite(), session.getAttrubuite() 沒啥兩樣,只是現在用 @In @Out 兩個 annotation 包裝起來、自動幫你呼叫而已。看到這... 我很懷疑... HotelBookingAction 這個看起來很 POJO 的東西真的算是物件嗎?它有 private state,不過卻可以任意的進出 class,任意的由外部改變生命週期。而不是封裝在 class 中,讓生命週期隨著 class 或生或滅。這... 這還算是物件導向嗎?!
  • 限定 EJB3
  • 限定 Java EE 5
  • 限定 JSF
  • JBoss only (是的,它沒有限制一定要在 JBoss 上面跑,不過大概沒有人會在 WebSphere/Weblogic 上裝這種東西吧...)

Conclusion:從 Application Transaction 的概念起,最後的產物是一頭 Annontation Bijection Monster... 阿門....


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>