2008年5月7日 星期三

[Struts] 分享我的開發流程

對於 Web Programmer 來說,使用 framework 是一件既方便也是必須作的事

因為 framework 幫助我們減少開發上枯燥乏味的 details

增加我們開發 Application(Web or Alone) 的速度!!

Struts 是一個在 JAVA 環境下的 Web framework

它的主要精神就是在於 MVC (Model-View-Controller) 的結構

以往使用 JSP 開發的 programmer,為了要達到 MVC 的 pattern

往往採用 MVC Model1 - JSP + JavaBean 的使用,

或是採用 Model2 - Servlet + JSP + JavaBean

但是無論是 Model1 or Model2 最困難的問題就是 effort 太高

MVC 之間的元件需要自行管理,Request Processes 需要自行管理等...

這些繁雜瑣碎的事物,我們透過使用 framework 就可以輕鬆的幫我們解決

Struts 對於 MVC 的支援相當的完整,使用者透過向 controller 發出 request

Controller 會決定是否需要到 Model 中取得資料給 View ,或是直接顯示 View來回應使用者

MVC 的 pattern 讓 programmer 之間可以達到 separate of concern,好處有太多,大家可以慢慢的體會~~

不過一種 framework 有再多的好處,如果無法掌握其開發的 process,對於 programmer 來說,那還不如就不要用!!

接下來我就提供我歷經兩個專案後所得到的開發經驗:

MVC 的核心在於 Controller,Controller 是整個 MVC 的開端,但是 Controller 端又需要 Model 端做為 Data Supplier

所以,以我的經驗事先從 MVC 的 M 開始著手

接下來就直接開發 Model components 囉!!

等等,先別急~~

Model components 對於 programmer 來說大多是跟 Database 存取有關

所以市面上有許多 ORM 的 solutions,比如目前最有名的 Hibernate (小弟尚在研究中~~)

這些 ORM 主要就是幫助我們解決 object and relative 的 mapping

畢竟,Database 的世界與 Program 的世界還是相距甚遠~~

今天不談 ORM,所以我們就將 Model components 用 JDBC 來設計

熟悉 JDBC 的人都知道,最中取出來的資料是一個 ResultSet 物件

如果我們就直接將 ResultSet 物件丟給 Controller 去處理,這樣就需要由 Controller 負責將 Model 產生的結果作收尾的動作

因為 ResultSet 是一個持續跟 Database 有連線關係的物件,照理說應該是由 Model 端元件來負責 ResultSet 的資料清裡

所以我們就需要在 Controller and Model 之間改以 JavaBean 做為溝通的橋樑

所以我們的開發第一步就是:開發一個與 Database Table 一樣內容的 JavaBean,也就是開發一個 Entity Bean

有了 Entity Bean,第二步就是:開發我們的 Model 元件

透過撰寫 SQL 語法對 JDBC 作存取的動作,並且在查詢結果 return 之前,Model 元件需要負責關閉 JDBC 物件與 Database 之間的連線,以確保資源的釋放

有了這兩樣的元件後,直觀的來說,就是對 Controller 開始動手囉!!

不過,回到 Struts 的運作,因為使用者可能由 View 端 submit requests,所以 View and Controller 之間的溝通是透過 ActionForm 物件

ActionForm 其實算是一種 JavaBean,不過他提供更多一點的功能

所以我們的第三步就是:開發 ActionForm

開發 ActionForm 物件最主要就是需要釐清使用者會將哪些資料 submit 給 Controller

有了這三樣元件後,我們就是真的要開始開發 Struts 中最核心的 Controller

第四步:開發 Controller

完成了 Controller 的內容,我們就可以知道哪些資訊是需要給使用者觀看的,所以我們就可以開發 View 端

第五步:開發 View

View 端透過一些 tags 來完成顯示的內容

以上是我開發的習慣流程,如果您是正需要使用 Struts 來開發您的 Web Application,您可以參考這樣的開發流程,來減低您 survey 的時間

2008年5月3日 星期六

[Displaytag] Displaytag:一個可以簡單完成分頁的 library

對於 web programmer 來說,分頁的資料顯示方式確實是一項令人頭痛的問題,因為必須先支到資料內容的長度,資料分頁後的頁數,以及使用者目前瀏覽到哪一個分頁的資料等等

Displaytag

是一個可以讓 web programmer 輕鬆就完成多資料分頁功能的 open source solution

官方網站中對於自家的 library 有了一些很詳盡的說明,以下就對於分頁的功能作部分的探討

Displaytag 首先就如同其他 JSP tag 一樣,需要再使用前先宣告

<%@ taglib uri="http://displaytag.sf.net" prefix="display" %>

接著我們就是以 display 做為 Displaytag 的 namespace

簡單的分頁使用就是對 table 設定 pagesize attribute:

<display:table name="myList" pagesize="10">
</display:table>


接著就是如果我們的 framework 使用的 Struts,我們還可以讓每次換頁時,都向 controller 重新取資料,那我們就必須設定 requestURI attribute:
<display:table name="myList" pagesize="10" requestURI="/paging.do">
</display:table>


這樣就會在每次分頁時,重新跟 paging controller 要求資料

不過你一定會很好奇,那他要的資料是要部分呢? 還是全部的資料?

別以為程式會這麼聰明,每次分頁時,當然要的都是全部的資料啊!!但是它只會顯示你要看的中間幾筆資料!!

那這樣記憶體空間不就浪費了!! 沒錯~~

所以在 displaytag 中提供了兩種方式來只取部分資料

不過我只有是成功其中一種 XD

首先建立一個 class 是 implement org.displaytag.pagination.PaginatedList:
public class MyPagingList implement org.displaytag.pagination.PaginatedList {
}

裡面會有一些需要實作出來的 methods:
getFullListSize():告知前端的 displaytag 總共會有多少筆的資料,讓前端可以產生所需要的頁數

getList():讓前端的 displaytag 取資料的 method,List 裡面存放的是取出部分的資料

getObjectsPerPage():告知前端的 displaytag 一頁顯示的資料筆數

getPageNumber():告知前端的 displaytag 目前是屬於第幾頁的資料

getSearchId():讓前端的 display 用於搜尋的功能

getSortCriterion():告知前端的 displaytag 目前排序的依據

getSortDirection():告知前端的 displaytag 目前排序的方向 ASC or DESC

我們可以利用後端的 Model 來取得這些資訊,並將這些資訊給與上面我們所建立的物件,再透過 controller 來給與 view 端的 displaytag 作顯示

對於使用者來說,觀看的內容並沒有任何的差別,但是對於顯示的速度來說確實是會加快(當總資料的筆數有上萬筆來說),因為取部分資料的方式,對於記憶體來說,永遠都會只占有那幾筆的數量,在空間上來說確實是一項節省

[Struts] 建立一個可以讓使用者下載檔案的 Action

Struts 的 Action 扮演 MVC(Model-View-Controller) 中的 Controller 角色

所有 Struts 的 request 都會先經過 Controller 在進行後端的 Model 計算並產生前端的 View

相信對於使用 Struts 的 programmer 來說,一般的 Action 都是不陌生的!!

但是,要如何可以讓使用者透過瀏覽器來下載檔案呢??

當然這檔案不是一個 static file,要對使用者的 request 產生一個 dynamic file

static file要做到就是直接將路徑指到 browser 的 URL 就OK

那 dynamic file呢?

Struts 提供一種 Action - DownloadAction,就如同一般的 Action 使用一樣

但是不同點在於需要 implement 一個 getStreamInfo method

所以我的 Controller 就會長成這樣:

public class MyController extends DownloadAction {
    public StreamInfo getStreamInfo(...) {
    }
}

基本的 class 架構就如上面的樣子,那 getStreamInfo method 呢?? 我要怎樣去完成??

我們先討論為何會有 getStreamInfo method 的存在

這個 method 是讓我們可以撰寫要讓使用者下載檔案的內容

觀察 Struts 的 DownloadAction source code 就可以發現,DownloadAction 是如何執行的:

1.首先呼叫我們撰寫的 getStreamInfo() method 來建立一個 StreamInfo object

2.接著就是由 StreamInfo object 中獲得 content type and inputStream

3.再來就是設定 browser 的 content type,告知 browser 要如何處理該檔案,如果是可以由外部存取的檔案,就會試著呼叫相關的 application 來支援

4.最後就是將 inputStream 的內容 copy 到 response 的 outputStream

使用者就可以透過 browser 取得檔案或是呼叫相關的 application 來處理檔案

知道流程後,我們就必須要撰寫 getStreamInfo() method 來符合流程囉~~

由上述的流程中,我們需要建立一個 StreamInfo object,裡面包含 content type and inputStream,所以重點就在於 StreamInfo object 的撰寫

觀察 Struts API 就會發現我們竟然找不到 StreamInfo interface 的內容!! 該不會 API 出錯??

身為一個 JAVA Programmer 的直覺,StreamInfo 一定是 DownloadAction 的 inner interface,所以我們要改查 DownloadAction.StreamInfo,果然不出我所料!!

既然是一個 interface 那我們就要接著觀察他有哪些 Known Implementing Classes

在 API 中立即就寫出:
All Known Implementing Classes:
    DownloadAction.FileStreamInfo, DownloadAction.ResourceStreamInfo


所以我們就要觀察哪個 implementing class 是我們所需要的!!

從 DownloadAction.FileStreamInfo 中知道這是用來產生檔案下載的,而 DownloadAction.ResourceStreamInfo 是用來產生網路資源的

所以我們就鎖定 DownloadAction.FileStreamInfo class 來使用

決定好關鍵的 class 之後,就是開始撰寫我們 getStreamInfo() method 囉~~

然而,需要建立一個 DownloadAction.FileStreamInfo 物件就需要設定他的 content type and file object(因為 constructor 的 parameter)

所以我們就要先建立一個 file 物件來讓 DownloadAction 的 inputStream 可以完成第四項流程

我們需要將我們的 dynamic file 備份到 local 的 HD 中,再將我們存放的路徑給予 File object

所以我們的 getStreamInfo() method 會先有這樣的內容:

public StreamInfo getStreamInfo(...) {
    String path = "/a.dat";
    File file = new File(path);
}

再來就是 content type 的部分,content type 就是告知 browser 該如何去處理接著要下載的檔案,例如我們設定 application/pdf,browser 就會預設呼叫 Adobe Acrobat Reader 來讀取

但是這樣還是無法完成正確的下載,所以我們必須在 StreamInfo 物件送交之前,對 response 設定 Header 為 "Content-disposition",value 為 "attachment; filename=a.dat"

這段設定的目的在於,強制 browser 不要在 browser 本身開啟檔案,強制 browser 透過下載檔案的方式來處理,而 filename 的部分卻可以讓我們對於使用者下載的檔名給與預設值

所以最後完成的內容應該會包含這樣的樣子:
public class MyController extends DownloadAction {
    public StreamInfo getStreamInfo(...) {

        String path = "/a.dat";
        File file = new File(path);
        String contentType = "application/unknow";
        //商業邏輯...
        response.setHeader(
            "Content-disposition",
            "attachment; filename=a.dat");
        return new FileStreamInfo(contentType,file);

    }
}

這樣我們就可以完成讓使用者下載檔案的功能了!!