無框架架構模型探討:簡化的Java EE開發

無框架架構模型探討:簡化的Java EE開發

2009-10-28 12:39 javaonejcy JavaEye博客 

Java EE向來因爲其複雜性而讓很多廠商望而卻步。本文試圖探討如何簡化Java EE 開發中不必要的複雜,並給出的是一個不使用任何框架的架構模型。

AD:

Java EE是個相當複雜的東西,被很多開發者們視爲龐然大物。在下面的文章中,javaonejcy探討了如何簡化Java EE開發中不必要的複雜,並給出一個不使用任何框架的架構模型。

你可以說可愛的php ,可愛的ror ,可愛的python ,甚至可愛的.net ,但是Java EE ?他太複雜了。相比其他兩種技術,Java EE 的技術體系更全面、更規整也更復雜,他的複雜性也讓很多廠商望而止步,寧可選擇簡單甚至簡陋的php ,這充分說明快速開發是這個時代最迫切的需求。

Java EE 的servlet 、javabean 、jdbc 規範給了我們組件和容器的唯一標準,而更高級的支持,jsf 、jdo 規範卻沒能給予我們唯一的框架級標準,他們被認可的程度遠低於相同領域的開源框架。儘管開源社區給了我們最豐富的選擇,但是相比.net 、php 、ror 的全棧式服務,Java EE 開發者必須DIY 。DIY 不但需要時間而且需要冒險,這種發燒友做的事情是企業所不願意做的。一段時間以來,公司Java EE 方向的招聘幾乎清一色的要求struts 、spring 、hibernate 這幾種主流框架的能力就是一種證明。

Java EE 的開發往往避免不了配置之旅,儘管很多框架都有自動生成工具,但是,面對一箇中型項目,仍然容易造成混亂。配置使你無論在開發、測試、集成還是維護的時都要配置代碼兩頭看。配置給了框架一個注入服務的切入點,但是對人並無優雅可言。ror 給了我們啓發,儘管企業開發是複雜的,但是大多數的需求都是通用的,事實證明,ror 把這部分通用性用約定的方式抽象得很好。其實Java EE 並不缺乏約定,因爲他本身就是建立於一系列規範的基礎之上,而規範就是約定。所以,Java EE 實際上有機會成爲相對簡潔的開發技術,只不過由於種種原因,這種規範並未出現。

在衆多的Java EE 開發框架中,struts+spring+hibernate 有着黃金組合的美譽,用的人多,會的人多,就算是沒出校門的學生,也知道學會ssh 的重要性。但是學會和學懂是兩碼事,對於一箇中型項目,ssh 就成了一柄雙刃劍,需要由高水平的設計師引路,才能披荊斬棘。spring+hibernate 給了設計者廣闊的空間,而設計需要因項目的前進而演進,如果你的項目進度緊張,人手不足,設計質量就難以保障,給系統帶來隱患。

“任何優秀的語言,都可以幫助開發者寫出優秀的代碼,但不能阻止開發者寫出糟糕的代碼”。在這一點上,無論是Java EE ,.net ,ror ,php 都不會例外。而開發框架就像是“一間有很多屋樑的房子”,“框架的強大之處不是他能讓你做什麼,而是他不能讓你做什麼”,其實如同語言一樣,框架雖然可以給予開發一定程度的規範指導,但是這種指導仍然是有限的,這真應了那句老話:事在人爲。

本文試圖探討如何簡化Java EE 開發中不必要的複雜,並給出的是一個不使用任何框架的架構模型,讓我們看看僅僅通過用編碼約定,結構設計和使用方式的組合能不能滿足項目開發的主要需求— 短期培訓,降低隱患和快速開發。

問題的源頭

應用軟件開發是複雜的,但其基本模型至爲簡單,請求-處理-響應。對應於軟件的層次結構就是:請求-Cortrol (C );處理-Model (M );響應-View (V )。在早期的Java EE 應用中,servlet 負責C ,javabean 和jdbc 在M ,jsp 是V 。這些就是Java EE 的基礎設施,他們職責劃分的方式被稱爲JSP Model2 ,已經可以滿足web 開發的基本需要,Java EE 的開發始終都圍繞着這幾項主要技術,框架也不例外。以下的內容,將從這些技術的應用與不足說起,然後介紹主流框架的解決方案,之後再介紹我們不用框架的處理方式。

(C) 選擇控制器

基礎規範的不足

任何web 應用,處理請求之後返回響應是必須的環節,如果編碼規範,傳統的響應就是轉向到某個頁面,servlet 處理轉向有兩種方式,其中request 轉向隱藏着重複提交的問題,response 重定向帶來參數傳遞的編碼解碼的問題,同時衆多的轉向地址直接寫在servlet 中也十分不雅,另外,jsp 和javabean 有一種出色的關聯技術,就是在jsp 裏可以把來自請求表單的數據自動拼裝到javabean 中。糟糕的是,這麼有用的技術卻無法在servlet 中使用,所以Model2 缺乏對錶單數據的自動轉換處理。servlet 有這些不足很好理解,因爲servlet 畢竟是較早出現的技術,他的職責只是將(http )請求轉化爲面向對象的視圖和輸出響應而已,由於他是低階組件,所以部分功能的缺失是正常的。不過這就讓servlet 成爲了Model2 最薄弱的一環。

開發框架的解決方案

由於以上需求是共性的,所以編寫一個通用框架就成爲了很多人努力的事情,struts 很快推出並且很快流行。我們先來看一看struts 的特性:

前端控制器: struts 使用一個servlet 作爲前端控制器,所有請求先經過這裏,再分派給配置指定的action (這裏是指行爲,而不是具體的Action ),意圖是以一箇中間層將視圖層和控制層解耦,這種思路帶來了三種可能的好處:1 視圖和控制分離,所以可以選擇不同的視圖技術,比如視圖模板既可以用jsp ,也可以用Volecity 、FreeMarker ;2 可以對所有請求預處理,和後處理(webwork );3 可以將響應的轉向地址管理起來。前端控制器也存在一種直接的不足:配置繁瑣。

ActionForm : struts 主要是一個控制層框架,所以他並不意圖深入到模型層,ActionForm 是一種無奈的選擇,儘管提供了表單數據到javabean 的轉換,但是遺憾的是這個javabean 並不能直接使用,還要手工的轉換爲模型javabean ,使得ActionForm 的位置有些尷尬。

國際化支持、標籤庫和全局異常: 國際化和標籤庫都是struts 的亮點,不過全局異常作用有限。

我們的選擇

Java EE 的控制器必然是一個servlet ,我們也不能例外,因爲我們必須要運行在servlet 容器之中。不過,我們選擇的是servlet 的演進版本-jsp 。別誤會,我們並不是要退回到JSP Model1 。一個典型的示例是,如果我有一個員工信息錄入的功能點,那麼爲了實現這個功能,我可以建立下面兩個文件:

worker_input.jsp

worker_inputOper.jsp

worker_input.jsp 裏不寫控制代碼,worker_inuptOper.jsp 裏也不寫視圖代碼,這種用法實際是JSP Model1 和JSP Model2 的綜合體。這樣做最大的好處就是,免去配置的煩惱,但是等等.. 前端控制器呢?我們的中間層呢?

考慮一下,你有一個企業信息的表單,表單中有一個企業名稱域,對這個域的要求是不能在已有企業中重名,域旁邊有一個按鈕,業務員可以通過點擊這個按鈕獲得錄入企業名稱是否重複的提示。如果是傳統方式,點擊按鈕將導致一個頁面提交,如果用struts ,將要配置這個action 處理之後轉向的URL 地址,這就是傳統web 應用的響應方式- 基於URL 地址的頁面導航。

web2.0 來了,ajax 來了,異步請求的出現徹底顛覆了傳統的web 交互模型。對於ajax 應用而言,服務器端返回響應只需要out.print ,請求從哪來,回哪去,轉向(如果需要)和更新視圖的任務都交給了客戶端腳本,也就是說,基於異步交互模式的web 應用,根本就沒有需要配置的result URL 路徑。這樣,頁面導航的問題就自動解決了。而對於預處理,我們可以用filter 替代。所以,我們完全可以和前端控制器說:再見。

由於客戶端技術的趨勢,在webappdemo 中我們將全面使用ajax 。也許你會說,如果客戶端瀏覽器禁用腳本呢?這個擔心在如今已經沒有必要,你可以訪問開心或者噹噹,看看禁用腳本他們能不能工作。時代在進步,富客戶RIA 是必然的選擇。

使用jsp 作爲控制器,還使我們得到了另一個關鍵的特性,那就是從form 表單數據到javabean 的自動創建和輸入,使javabean 本身既是模型也是DTO ,再也不必象ActionForm 那樣還要手工轉換。這裏還有一個隱含的好處,就是強制統一了表單域名和模型屬性名,不然,有可能出現這樣的情況:表單域:child_center ;模型屬性:branch 。以下是worker_inputOper.jsp 的寫法:

Jsp代碼 

  1. < jsp:useBean id="worker" class="webappdemo.worker.entity.Worker" scope="page"/>    
  2. < jsp:setProperty name="worker" property="*"/>    
  3.     
  4. < %    
  5.     
  6.     response.setContentType("text/x-json;charset=UTF-8");    
  7.     response.setHeader("Cache-Control""no-cache");    
  8.     
  9.     String method = request.getParameter("method");    
  10.     
  11.     if("save".equals(method)){    
  12.         EntityService es = new EntityService();    
  13.         Message m = es.add(worker);    
  14.         out.print(new JSONObject().put(m.isSucceed()?"succeed":"error", m.getMessage()));    
  15.         return;    
  16.     }    
  17.     
  18. %>   

可以看出,只需將實體類名引入標籤,我們就可以獲得自動拼裝的Worker 對象。對於複雜對象或複合對象,由於request 裏同樣有我們需要的所有請求參數,所以你可以在自動創建的javabean 基礎上修改部分屬性,以符合業務需要。

代碼還展示了基於“method ”的用法,這只是一個字符串,用來告訴oper jsp 要用哪個方法來處理請求,這類似於ror 控制器內部定義的方法以及struts 的DispatchAction 但比他更靈活,變通的解決了jsp 的請求不能直接面向方法的不足。

在調用服務處理請求之後,worker_inputOper.jsp 將處理結果out.print 回客戶端,這句代碼的意思是新建一個JSON 對象,將處理結果添加進去,然後輸出這個對象,方便客戶端js 腳本解析。JSON 對象可以增加多個處理結果,只要他們的key 不同就可以。在實際應用中,往往返回處理消息,或者html 視圖的字符串。最後別忘了return; 否則程序仍然會向下進行。

如果你的項目需要國際化,我們可以使用fmt 標籤,而對於反饋消息的國際化,我們也許就需要建立一個全局MessageSource 對象了,這個問題在webappdemo 中沒有涉及,因爲筆者認爲這不是普遍需求。

對於異常處理,其實jsp 已經提供了簡單的機制,我們可以在web.xml 中配置:

Xml代碼

  1. < error-page>    
  2.     < error-code>404< /error-code>    
  3.     < location>/404.jsp< /location>    
  4. < /error-page>    
  5. < error-page>    
  6.     < error-code>500< /error-code>    
  7.     < location>/500.jsp< /location>    
  8. < /error-page>    

這種簡單的處理其實正是我們需要的全部,因爲筆者認爲,web 應用的系統錯誤和java 的異常沒有區別,即檢測錯誤和運行時錯誤。在web2.0 時代,所有的錯誤都應該被捕獲,並且把內容經處理後在用戶輸入位置反饋給用戶,而不應該重新定向。運行時錯誤屬於系統bug ,是需要修復的代碼級錯誤,這種錯誤是真正的“意外”,所以我們用定製的錯誤頁面反饋給用戶就可以了。

綜上所述,我們用ajax+jsp+ 控制jsp 的方式代替了servlet 或者action ,擺脫了前端控制器,用模型javabean 代替了過程javabean ActionForm ,這些使用方式使我們不需要配置即可以開發應用程序,除了ajax 是相對新概念外不需要額外學習框架技術也是他的優點。

(M)ORM 可以做什麼

基礎規範的不足

jdbc 是java 應用程序數據持久化的基礎,也是衆多數據庫廠商與java 的接口。直接使用jdbc 編寫代碼非常繁瑣,比如數據庫資源的獲得和釋放,異常捕獲和事務處理等等,重複代碼多是他的一個特點。另外,不同的數據庫,在數據類型,主鍵類型還是sql 語句都和SQL 標準小有出入,所以如何使應用程序可以在不同數據庫平臺方便的遷移,也是個問題。

開發框架的解決方案

spring 和hibernate 的出現使情況大爲好轉,spring 面向切面管理事務, hibernate 自動ORM 可以大大簡化開發,spring 和hibernate 都有.net 的版本,這證明了他們的成功。但是“用好hibernate ,需要紮實的掌握關係模型和SQL ”,同時對面向對象設計和hibernate 自身的運行機制也要有非常清晰的認識,只有這種水平才能發揮hibernate 的威力,降低hibernate 帶來的風險。所以,在合適的層面上配置好spring 的事務管理,設計好對象模型,把對hibernate 的直接使用控制在一定範圍內是設計者要解決的基本問題。如果設計不佳,或者直接交給初出校門的開發者去用,那這種組合就會變成洪水猛獸,同時也不利於團隊成員的成長。

我們的選擇

如果只有jdbc ,我們的系統也可以工作,只不過要寫很多重複和機械的代碼,通過框架的ORM 的映射,可以將數據庫表的數據自動填入javabean ,這節省了勞動力,也使系統結構自然清晰。如果不用ORM 工具,我們能不能通過某種形式來模擬他的行爲呢?我們可以創建這樣一個接口:

Java代碼

  1. public interface IOper {    
  2.     
  3.     boolean load(Connection connect) throws SQLException;    
  4.     
  5.     boolean add(Connection connect) throws SQLException;    
  6.     
  7.     boolean update(Connection connect) throws SQLException;    
  8.     
  9.     boolean delete(Connection connect) throws SQLException;    
  10.     
  11. }    

在接口中定義 CRUD 方法。返回類型爲 boolean 而非影響的行數,意圖是對象內部的操作可能是複雜的多步驟的,所以對他的上層應用來說,知道結果成功與否就可以了。接下來在他的實現類裏可以這麼寫:

Java代碼

  1. public class Worker implements IOper {    
  2.     
  3.     // Fields    
  4.     private Integer workerId;    
  5.     
  6.     private String workerName;    
  7.     
  8.     private String logonName;    
  9.     
  10.     private String logonPwd;    
  11.     
  12.     /*   
  13.      * 對於允許爲空的字段賦予初始值,避免頁面顯示null   
  14.      */    
  15.     private String mobile = "";    
  16.     
  17.     private String email = "";    
  18.     
  19.     private String remark = "";    
  20.     
  21.     private String isFreeze = "0";    
  22.     
  23.     // Constructors    
  24.     
  25.     /** default constructor */    
  26.     
  27.     // Property accessors     
  28.     
  29.     public boolean add(Connection connect) throws SQLException {    
  30.         SQLBuffer sql = new SQLBuffer();    
  31.         sql.segment("insert into worker (worker_name,logon_name,logon_pwd,");    
  32.         sql.segment("mobile,email,remark,is_freeze) values (");    
  33.         sql.value(this.workerName);    
  34.         sql.comma();    
  35.         sql.value(this.logonName);    
  36.         sql.comma();    
  37.         sql.value(this.logonPwd);    
  38.         sql.comma();    
  39.         sql.value(this.mobile);    
  40.         sql.comma();    
  41.         sql.value(this.email);    
  42.         sql.comma();    
  43.         sql.value(this.remark);    
  44.         sql.comma();    
  45.         sql.value(this.isFreeze);    
  46.         sql.segment(")");    
  47.         return Proxy.update(connect, sql) == 1;    
  48.     }    
  49.     
  50.     public boolean delete(Connection connect) throws SQLException {    
  51.         // 凍結用戶    
  52.         SQLBuffer sql = new SQLBuffer();    
  53.         sql.segment("update worker set is_isfreeze = ");    
  54.         this.isFreeze = "1";    
  55.         sql.value(this.isFreeze);    
  56.         sql.segment(" where worker_id = ");    
  57.         sql.value(this.workerId);    
  58.         return Proxy.update(connect, sql) == 1;    
  59.     }    
  60.     
  61.     public boolean load(Connection connect) throws SQLException {    
  62.         SQLBuffer sql = new SQLBuffer(    
  63.                 "select worker_name,logon_name,logon_pwd,mobile,email,remark,is_freeze from worker");    
  64.         sql.segment(" where worker_id = ");    
  65.         sql.value(this.workerId);    
  66.         MapRow mr = Proxy.getMapRow(connect, sql);    
  67.         if (mr == null) {    
  68.             return false;    
  69.         }    
  70.         this.workerName = mr.getString("worker_name");    
  71.         this.logonName = mr.getString("logon_name");    
  72.         this.logonPwd = mr.getString("logon_pwd");    
  73.         this.mobile = mr.getString("mobile");    
  74.         this.email = mr.getString("email");    
  75.         this.remark = mr.getString("remark");    
  76.         this.isFreeze = mr.getString("is_freeze");    
  77.         return true;    
  78.     }    
  79.     
  80.     public boolean update(Connection connect) throws SQLException {    
  81.         SQLBuffer sql = new SQLBuffer();    
  82.         sql.segment("update worker set worker_name = ");    
  83.         sql.value(this.workerName);    
  84.         sql.segment(", logon_name = ");    
  85.         sql.value(this.logonName);    
  86.         sql.segment(", logon_pwd = ");    
  87.         sql.value(this.logonPwd);    
  88.         sql.segment(", mobile = ");    
  89.         sql.value(this.mobile);    
  90.         sql.segment(", email = ");    
  91.         sql.value(this.email);    
  92.         sql.segment(", remark = ");    
  93.         sql.value(this.remark);    
  94.         sql.segment(", is_freeze = ");    
  95.         sql.value(this.isFreeze);    
  96.         sql.segment(" where worker_id = ");    
  97.         sql.value(this.workerId);    
  98.         return Proxy.update(connect, sql) == 1;    
  99.     }    
  100. }    

實體 javabean 通過實現 IOper 接口,負責對自身數據的操作。儘管這種實現方式等於是使模型成爲了富血模型,但其實我們仍然可以把這種模型認爲是貧血的,因爲他沒有業務邏輯,只是模擬了 ORM 的行爲。 如果對象關係有包含和聚合,我們同樣也可以通過類似 hibernate 的行爲方式來實現,比如懶加載。以上的代碼使用了筆者所用的 API ,由於操作都在內部,所以換成直接用 jdbc 也是一樣的。在實際應用中, load 方法有些單薄,因爲有的查詢需要一些附加條件,我們可以通過增加一個類屬性來達到這個目的:

String condition;

如果設置了條件,我們就拼接給定的查詢條件取得結果,不過結果只能是一條,這是模型結構所決定的。另外Connection 對象是每個方法的必須的參數,意圖是在實際業務操作中,單一的操作往往是不存在的,所以,總是由外部傳遞資源和釋放資源。但是,在每個調用的地方都要寫重複的try catch 以及獲得和釋放 連接的代碼豈不是很無聊?那麼,我們可以用一個服務類包裝他,這就體現了IOper 接口的用處了:

Java代碼

  1. public class EntityService {    
  2.     
  3.     public Message add(IOper io) {    
  4.     
  5.         try {    
  6.             Connection connect = Proxy.getConnect();    
  7.             connect.setAutoCommit(false);    
  8.     
  9.             // 增加    
  10.             if (!io.add(connect)) {    
  11.                 throw new Exception("增加操作失敗");    
  12.             }    
  13.     
  14.             // 其他操作    
  15.     
  16.             connect.commit();    
  17.             return new Message(true);    
  18.     
  19.         } catch (Exception e) {    
  20.             Proxy.rollbackConnect();    
  21.             e.printStackTrace();    
  22.             return new Message(e);    
  23.         } finally {    
  24.             Proxy.closeConnect();    
  25.         }    
  26.     
  27.     }    
  28.     
  29.     public Message update(IOper io) {    
  30.     
  31.         try {    
  32.             Connection connect = Proxy.getConnect();    
  33.             connect.setAutoCommit(false);    
  34.     
  35.             // 修改    
  36.             if (!io.update(connect)) {    
  37.                 throw new Exception("修改操作失敗");    
  38.             }    
  39.     
  40.             // 其他操作    
  41.     
  42.             connect.commit();    
  43.             return new Message(true);    
  44.     
  45.         } catch (Exception e) {    
  46.             Proxy.rollbackConnect();    
  47.             e.printStackTrace();    
  48.             return new Message(e);    
  49.         } finally {    
  50.             Proxy.closeConnect();    
  51.         }    
  52.     }    
  53.     
  54.     public Message delete(IOper io) {    
  55.     
  56.         try {    
  57.             Connection connect = Proxy.getConnect();    
  58.             connect.setAutoCommit(false);    
  59.     
  60.             // 刪除    
  61.             if (!io.delete(connect)) {    
  62.                 throw new Exception("刪除操作失敗");    
  63.             }    
  64.     
  65.             // 其他操作    
  66.     
  67.             connect.commit();    
  68.             return new Message(true);    
  69.     
  70.         } catch (Exception e) {    
  71.             Proxy.rollbackConnect();    
  72.             e.printStackTrace();    
  73.             return new Message(e);    
  74.         } finally {    
  75.             Proxy.closeConnect();    
  76.         }    
  77.     }    
  78.     
  79.     public Message load(IOper io) {    
  80.     
  81.         try {    
  82.             Connection connect = Proxy.getConnect();    
  83.             connect.setAutoCommit(false);    
  84.     
  85.             // 載入    
  86.             if (!io.load(connect)) {    
  87.                 throw new Exception("載入操作失敗");    
  88.             }    
  89.     
  90.             // 其他操作    
  91.             connect.commit();    
  92.             return new Message(true);    
  93.     
  94.         } catch (Exception e) {    
  95.             Proxy.rollbackConnect();    
  96.             e.printStackTrace();    
  97.             return new Message(e);    
  98.         } finally {    
  99.             Proxy.closeConnect();    
  100.         }    
  101.     }    
  102. }    

從EntityService 的代碼中看到,try catch 應該總是出現在服務層,只有一個獨立的服務纔有必要保證一個完整的事務。如果你要在CUD 操作後記錄業務日誌,那麼你可以寫在“其他操作”的位置。由於對外界信息的依賴,比如用戶信息,功能ID 等等,在實際應用中需要更復雜的設計。至此,我們完成了對ORM 行爲的模擬,在調用的地方,我們只需要:

Java代碼

  1. EntityService es = new EntityService();    
  2. Message m = es.add(worker);   

儘管方法內部的代碼仍然是繁瑣的,但是我們由於把這些代碼的位置聚焦於較少的地方而降低了繁瑣程度,同時也獲得了良好的結構。對數據庫的操作更爲直觀是“手寫”代碼的優點,當然,我們還可以選擇ibatis ,不過結構就不會是這樣的了。同時筆者認爲sql 代碼總是和程序內部息息相關,配置在外部看起來並不一定方便吧。

對於數據庫遷移問題,hibernate 很大程度的解決了這個問題,不過企業開發中遷移數據的需求並不多,而且數據庫遷移不是使用某個工具或者規範(jdbc ,jdo )就可以完全解決的,對於大型複雜應用,遷移數據庫需要付出巨大的努力,所以實際上也沒有人這麼做。

(V) 客戶端MVC

關於客戶端應用的說明沒有內部分段,因爲Java EE 技術(暫不談javafx )本身就和客戶端沒有太大關係,所以也不存在不足的問題,這裏只是意圖糾正一些系統中不良的編碼習慣。

jsf 是Java EE UI 框架規範,前端控制器、事件模型和UI 組件是他的主要組成部分,但是實際開發中jsf 標籤非常繁瑣,並且只有客戶端明確禁用腳本或者其他組件(flex-flash ,javafx 甚至silverlight )時,jsf 纔是最有價值的,糟糕(幸運)的是目前幾乎沒有客戶端瀏覽器不支持以上腳本或插件,所以jsf 的價值大大降低了。

jsp 是Java EE 視圖組件,也是jsf UI 框架的基礎之一,他實際上就是一個環境寬鬆的視圖模板,儘管這種寬鬆貌似不利於軟件分層,但是比起其他模板技術,他至少有一個優點,就是非常象asp ,這使他更容易爲開發者所接受。在web2.0 的時代,如果你的項目不打算用flex-flash 或者javafx ,那麼jsp 就是你最佳的視圖載體,你可以把javacript 、css 、html 、xml 、json 還有java 代碼都放在這裏,爲了實現ajax 的交互邏輯,你可能要寫大段的javascript ,沒關係,jsp 都能承受,儘管,這些東西放在一起看上去就像是一個老鼠窩。

Ajax in action 的作者提出了客戶端MVC 的概念,不過別誤會,這並不是ext 或者flex 的開發模式,而只是一種觀念。html 是由dom 樹建立的視圖結構,所以是M ,css 給html 結構添加效果,所以是V ,javascript 控制顯示邏輯,所是C ,這裏指的客戶端MVC 就是把這三種語言代碼分別建立單獨的文件,在html 頁面中引用外部的css 和js 。同樣拿員工信息錄入舉例,我們可以建立如下幾個文件:

worker_input.jsp

worker_inptu.js

style.css

可以看到,css 文件我們並沒有用功能方式命名,因爲樣式表往往可以被整個系統共用,也就是說,我們只要記得不在html 裏寫css ,js 代碼,並單獨建立js 文件就可以了。不同種類代碼分開建立文件是提高代碼可讀性的關鍵步驟,尤其是在客戶端程序愈發複雜的今天。worker_input.js 是這樣寫的:

Js代碼

  1. // 保存    
  2. function save(){    
  3.     if(check()){    
  4.         AjaxUtil.sendForm(document.forms[0], "method=save"function () {    
  5.             if (this.result.succeed) {    
  6.                 document.forms[0].reset();    
  7.                 messageapp.printMessage(this.result.succeed);    
  8.             } else if (this.result.error) {    
  9.                 messageapp.printMessage(this.result.error);    
  10.             }    
  11.         }, true);    
  12.     }    
  13. }    

這個文件只定義了一個save 方法,這個方法內部發起了ajax 方式的異步請求,如果返回成功,則重置表單,並更新輸出信息到指定位置,反之則僅更新輸出信息。你會注意到,我們返回的JSON 對象起到了作用。

問題:如果我的jsp 代碼裏出現了這樣的情況,算是違背客戶端MVC 的原則嗎?

Html代碼

  1. < tr>    
  2. < td colspan="2">    
  3. < input type="button" value="保    存" onclick="save();"/>    
  4.           
  5.         < input type="reset" value="重    置" />    
  6.               
  7.         < input type="button" value="返    回" onclick="window.history.go(-1);" />    
  8.     < /td>    
  9. < /tr>    

以上代碼片段中有簡短的 js 代碼和方法引用,這種情況,筆者認爲是可以的,項目開發沒必要追去絕對的完美,只要達到結構清晰的目的就行了,我們要的是高質量的產品,而不是高質量的理論。

問題:如果我使用了類似 ror 的 JavaScriptHelper 那樣的腳本標籤庫,生成的腳本與 html 代碼混在一起,這樣算不算違背客戶端 MVC 原則呢?很顯然,也不算,因爲對於輸出前的 jsp 代碼模板,程序結構是清晰的。

(F) 結構和細節

關於MVC 各部分的介紹,再讓我們來看看整體結構,假設我們是在一個eclipse 工程中,打開WebRoot 目錄,如果忽略META-INF 和WEB-INF 目錄的話,我們的示例目錄是這樣的:

module

404.jsp

500.jsp

index.htm

其中module 就是我們應用所有的視圖,由於我們對於jsp 的使用方式,還包括所有的控制器。將應用程序都放在一個目錄下是爲了方便今後使用filter 對目錄進行保護,或者進行某些其他預處理。打開module 目錄,展現的結構是這樣的:

  1. common  
  2. worker  

一個common 目錄,存放所有共用程序文件,一個worker 目錄存放關於員工這個實體的功能文件。實際上你的結構也可能是sys/worker 或者center/sys/worker 等等,就要看實際的應用需求了,總之,通過目錄劃分功能模塊,以正確的樹形結構描述他是沒錯的。我們再來打開common 目錄:

  1. css  
  2. img  
  3. js  
  4. import_page.def  

前三個目錄分別存放的是常用文件,而最後一個是文件,這個文件將用這種方式添加到每個視圖jsp 頭,在< head>< /head> 間加入:

Jsp代碼

  1. < %@ include file=”../common/import_page.def”%>   

這裏使用了相對路徑,增加這個文件是爲了方便的引用其他css 和js 庫文件。使用這種方式可以避免在部署的階段因合併js 或進行css 、js 庫文件的版本變更而帶來的麻煩。

再來看看java src 的目錄結構:

  1. webappdeom  
  2.     common  
  3.     worker  
  4. datasource.xml  
  5. log4j.properties  

除了根目錄,與WebRoot 下的結構是一致的。兩個文件:datasource.xml 是數據源配置文件,也是筆者demo 中使用的API 所需要的,log4j.properties 是log4j 的屬性文件。另外,系統默認使用hsql 數據庫,如果要正常啓動,需要改動web.xml 中的一個配置項,具體位置有註釋說明。

再具體的結構和代碼這裏就不多講了,如果感興趣,可以下載webappdemo 看看,裏面的註釋比較完整。不過在demo 裏還有一些隱藏問題,以及可以擴展的思路,不知道你能不能找出來。

尾聲

老話說,條條大路通羅馬,在開源的java 的世界更是如此,我們可以有自己的想法並且拿出來分享,這就是開源最大的魅力,也是資源的寶庫。儘管sun 被收購,但是java 仍然會是開源最響亮的聲音,javafx 是筆者目前最關注的技術,希望他能一路走好。

from http://developer.51cto.com/art/200910/159202.htm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章