1. 程式人生 > >一個計算機專業學生幾年的Java程式設計經驗彙總

一個計算機專業學生幾年的Java程式設計經驗彙總

2.關於Java的多執行緒程式設計
關於Java的執行緒,初學或者接觸不深的大概也能知道一些基本概念,同時又會很迷惑執行緒到底是怎麼回事?如果有人認為自己已經懂了不妨來回答下面的問題:
a. A物件實現Runnable介面,A.start()執行後所謂的執行緒物件是誰?是A麼?
b. 執行緒的wait()、notify()方法到底是做什麼時候用的,什麼時候用?
c. 為什麼執行緒的suspend方法會被標註過時,不推薦再使用,執行緒還能掛起麼?
d. 為了同步我們會對執行緒方法宣告Synchronized來加鎖在物件上,那麼如果父類的f()方法加了Synchronized,子類重寫f()方法必須 也加Synchronized麼?如果子類的f()方法重寫時宣告Synchronized並呼叫super.f(),那麼子類物件上到底有幾把鎖呢?會 因為競爭產生死鎖麼?

呵呵,各位能回答上來幾道呢?如果這些都能答上來,說明對執行緒的概念還是滿清晰的,雖說還遠遠不能算精通。筆者這裡一一做回答,礙於篇幅的原因,筆者儘量說得簡介一點,如果大家有疑惑的歡迎一起討論。

首先第一點,執行緒跟物件完全是兩回事,雖然我們也常說執行緒物件。但當你用run()和start()來啟動一個執行緒之後,執行緒其實跟這個繼承了 Thread或實現了Runnable的物件已經沒有關係了,物件只能算記憶體中可用資源而物件的方法只能算記憶體正文區可以執行的程式碼段而已。既然是資源和 程式碼段,另外一個執行緒當然也可以去訪問,main函式執行就至少會啟動兩個執行緒,一個我們稱之為主執行緒,還一個是垃圾收集器的執行緒,主執行緒結束就意味著程 序結束,可垃圾收集器執行緒很可能正在工作。

第二點,wait()和sleep()類似,都是讓執行緒處於阻塞狀態暫停一段時間,不同之處在於wait會釋放當前執行緒佔有的所有的鎖,而 sleep不會。我們知道獲得鎖的唯一方法是進入了Synchronized保護程式碼段,所以大家會發現只有Synchronized方法中才會出現 wait,直接寫會給警告沒有獲得當前物件的鎖。所以notify跟wait配合使用,notify會重新把鎖還給阻塞的執行緒重而使其繼續執行,當有多個 物件wait了,notify不能確定喚醒哪一個,必經鎖只有一把,所以一般用notifyAll()來讓它們自己根據優先順序等競爭那唯一的一把鎖,競爭 到的執行緒執行,其他執行緒只要繼續wait。

從前Java允許在一個執行緒之外把執行緒掛起,即呼叫suspend方法,這樣的操作是極不安全的。根據面向物件的思想每個物件必須對自己的行為負責,而對 自己的權力進行封裝。如果任何外步物件都能使執行緒被掛起而阻塞的話,程式往往會出現混亂導致崩潰,所以這樣的方法自然是被斃掉了啦。

最後一個問題比較有意思,首先回答的是子類重寫f()方法可以加Synchronized也可以不加,如果加了而且還內部呼叫了super.f ()的話理論上是應該對同一物件加兩把鎖的,因為每次呼叫Synchronized方法都要加一把,呼叫子類的f首先就加了一把,進入方法內部呼叫父類的 f又要加一把,加兩把不是互斥的麼?那麼調父類f加鎖不就必須永遠等待已經加的鎖釋放而造成死鎖麼?實際上是不會的,這個機制叫重進入,當父類的f方法試 圖在本物件上再加一把鎖的時候,因為當前執行緒擁有這個物件的鎖,也可以理解為開啟它的鑰匙,所以同一個執行緒在同一物件上還沒釋放之前加第二次鎖是不會出問 題的,這個鎖其實根本就沒有加,它有了鑰匙,不管加幾把還是可以進入鎖保護的程式碼段,暢通無阻,所以叫重進入,我們可以簡單認為第二把鎖沒有加上去。

總而言之,Synchronized的本質是不讓其他執行緒在同一物件上再加一把鎖。

Java雜談(五)


本來預計J2se只講了第四篇就收尾了,可是版主厚愛把帖子置頂長期讓大家瀏覽讓小弟倍感責任重大,務必追求最到更好,所以關於J2se一些沒有提到的部 分,決定再寫幾篇把常用的部分經驗全部寫出來供大家討論切磋。這一篇準備講一講Xml解析包和Java Swing,然後下一篇再講java.security包關於Java沙箱安全機制和RMI機制,再進入J2ee的部分,暫時就做這樣的計劃了。如果由於 實習繁忙更新稍微慢了一些,希望各位見諒! 

1. Java關於XML的解析 
相信大家對XML都不陌生,含義是可擴充套件標記語言。本身它也就是一個數據的載體以樹狀表現形式出現。後來慢慢的資料變成了資訊,區別是資訊可以包括可變的 狀態從而針對程式硬編碼的做法變革為針對統一介面硬編碼而可變狀態作為資訊進入了XML中儲存。這樣改變狀態實現擴充套件的唯一工作是在XML中新增一段文字 資訊就可以了,程式碼不需要改動也不需要重新編譯。這個靈活性是XML誕生時候誰也沒想到的。 

當然,如果介面要能提取XML中配置的資訊就需要程式能解析規範的XML檔案,Java中當然要提高包對這個行為進行有利支援。筆者打算講到的兩個包是 org.w3c.dom和javax.xml.parsers和。(大家可以瀏覽一下這些包中間的介面和類定義) 

Javax.xml.parsers包很簡單,沒有介面,兩個工廠配兩個解析器。顯然解析XML是有兩種方式的:DOM解析和SAX解析。本質上並沒有誰好誰不好,只是實現的思想不一樣罷了。給一個XML檔案的例子: 
<?xml version=”1.0” encoding=”UTF-8” > 
<root > 
<child name=”Kitty” > 
A Cat 
</child > 
</root > 

所謂DOM解析的思路是把整個樹狀圖存入記憶體中,需要那個節點只需要在樹上搜索就可以讀到節點的屬性,內容等,這樣的好處是所有節點皆在記憶體可以反覆搜尋重複使用,缺點是需要消耗相應的記憶體空間。 

自然SAX解析的思路就是為了克服DOM的缺點,以事件觸發為基本思路,順序的搜尋下來,碰到了Element之前觸發什麼事件,碰到之後做什麼動作。由 於需要自己來寫觸發事件的處理方案,所以需要藉助另外一個自定義的Handler,處於org.xml.sax.helpers包中。它的優點當然是不用 整個包都讀入記憶體,缺點也是隻能順序搜尋,走完一遍就得重來。 

大家很容易就能猜到,接觸到的J2ee框架用的是哪一種,顯然是DOM。因為類似Struts,Hibernate框架配置檔案畢竟是很小的一部分配置信 息,而且需要頻繁搜尋來讀取,當然會採用DOM方式(其實SAX內部也是用DOM採用的結構來儲存節點資訊的)。現在無論用什麼框架,還真難發現使用 SAX來解析XML的技術了,如果哪位仁兄知道,請讓筆者也學習學習。 

既然解析方式有了,那麼就需要有解析的儲存位置。不知道大家是否發現org.w3c.dom這個包是沒有實現類全部都是介面的。這裡筆者想說一下Java 如何對XML解析是Jdk應該考慮的事,是它的責任。而w3c組織是維護定義XML標準的組織,所以一個XML結構是怎麼樣的由w3c說了算,它不關心 Java如何去實現,於是乎規定了所有XML儲存的結構應該遵循的規則,這就是org.w3c.dom裡全部的介面目的所在。在筆者看來,簡單理解介面的 概念就是實現者必須遵守的原則。 

整個XML對應的結構叫Document、子元素對應的叫做Element、還有節點相關的Node、NodeList、Text、Entity、 CharacterData、CDATASection等介面,它們都可以在XML的語法中間找到相對應的含義。由於這裡不是講解XML基本語法,就不多 介紹了。如果大家感興趣,筆者也可以專門寫一篇關於XML的語法規則帖與大家分享一下。 

2. Java Swing 
Swing是一個讓人又愛又恨的東西,可愛之處在於上手很容易,較AWT比起來Swing提供的介面功能更加強大,可恨之處在於編複雜的介面工作量實在是 巨大。筆者寫過超過3000行的Swing介面,感覺使用者體驗還不是那麼優秀。最近又寫過超過6000行的,由於功能模組多了,整體效果還只是一般般。體 會最深的就一個字:累! 所以大家現在都陸續不怎麼用Swing在真正開發的專案上了,太多介面技術可以取代它了。筆者去寫也是迫於無奈組裡面大家都沒寫過,我不入地域誰入? 

儘管Swing慢慢的在被人忽略,特別是隨著B/S慢慢的在淹沒C/S,筆者倒是很願意站出來為Swing正身。每一項技術的掌握絕不是為了流行時尚跟 風。真正喜歡Java的朋友們還是應該好好體會一下Swing,相信在校的很多學生也很多在學習它。很可能從Jdk 1.1、1.2走過來的很多大學老師可能是最不熟悉它的。 
Swing提供了一組輕元件統稱為JComponent,它們與AWT元件的最大區別是JComponent全部都是Container,而 Container的特點是裡面可以裝載別的元件。在Swing元件中無論是JButton、JLabel、JPanel、JList等都可以再裝入任何 其他元件。好處是程式設計師可以對Swing元件實現“再開發”,針對特定需求構建自己的按鈕、標籤、畫板、列表之類的特定元件。 

有輕自然就有重,那麼輕元件和重元件區別是?重元件表現出來的形態因作業系統不同而異,輕元件是Swing自己提供GUI,在跨平臺的時候最大程度的保持一致。 
那麼在程式設計的時候要注意一些什麼呢?筆者談談自己的幾點經驗: 

a. 明確一個概念,只有Frame元件才可以單獨顯示的,也許有人會說JOptionPane裡面的靜態方法就實現了單獨窗口出現,但追尋原始碼會發現其實現 實出來的Dialog也需要依託一個Frame窗體,如果沒有指定就會預設產生一個然後裝載這個Dialog顯示出來。 

b. JFrame是由這麼幾部分組成: 
最底下一層JRootPane,上面是glassPane (一個JPanel)和layeredPane (一個JLayeredPane),而layeredPane又由contentPane(一個JPanel)和menuBar構成。我們的元件都是加在 contentPane上,而背景圖片只能加在layeredPane上面。 至於glassPane是一個透明的覆蓋了contentPane的一層,在特定效果中將被利用到來記錄滑鼠座標或掩飾元件。 

c. 為了增強使用者體驗,我們會在一些按鈕上新增快捷鍵,但Swing裡面通常只能識別鍵盤的Alt鍵,要加入其他的快捷鍵,必須自己實現一個ActionListener。 

d. 通過setLayout(null)可以使得所有元件以setBounds()的四個引數來精確定位各自的大小、位置,但不推薦使用,因為好的程式設計風格不 應該在Swing程式碼中硬編碼具體數字,所有的數字應該以常數的形式統一存在一個靜態無例項資源類檔案中。這個靜態無例項類統一負責Swing介面的風 格,包括字型和顏色都應該包括進去。 

e. 好的介面設計有一條Golden Rule: 使用者不用任何手冊通過少數嘗試就能學會使用軟體。所以儘量把按鈕以選單的形式(不管是右鍵選單還是窗體自帶頂部選單)呈現給顧客,除非是頻繁點選的按鈕才有必要直接呈現在介面中。 

其實Swing的功能是相當強大的,只是現在應用不廣泛,專門去研究大概是要花不少時間的。筆者在各網站論壇瀏覽關於Swing的技巧文章還是比較可信 的,自己所學非常有限,各人體會對Swing各個元件的掌握就是一個實踐積累的過程。筆者只用到過以上這些,所以只能談談部分想法,還望大家見諒!

Java雜談(六)


這篇是筆者打算寫的J2se部分的最後一篇了,這篇結束之後,再寫J2ee部分,不知道是否還合適寫在這個版塊?大家可以給點意見,謝謝大家對小弟這麼鼓 勵一路寫完前六篇Java雜談的J2se部分。最後這篇打算談一談Java中的RMI機制和JVM沙箱安全框架。 

1. Java中的RMI機制 
RMI的全稱是遠端方法呼叫,相信不少朋友都聽說過,基本的思路可以用一個經典比方來解釋:A計算機想要計算一個兩個數的加法,但A自己做不了,於是叫另 外一臺計算機B幫忙,B有計算加法的功能,A呼叫它就像呼叫這個功能是自己的一樣方便。這個就叫做遠端方法呼叫了。 

遠端方法呼叫是EJB實現的支柱,建立分散式應用的核心思想。這個很好理解,再拿上面的計算加法例子,A只知道去call計算機B的方法,自己並沒有B的 那些功能,所以A計算機端就無法看到B執行這段功能的過程和程式碼,因為看都看不到,所以既沒有機會竊取也沒有機會去改動方法程式碼。EJB正式基於這樣的思 想來完成它的任務的。當簡單的加法變成複雜的資料庫操作和電子商務交易應用的時候,這樣的安全性和分散式應用的便利性就表現出來優勢了。 

好了,回到細節上,要如何實現遠端方法呼叫呢?我希望大家學習任何技術的時候可以試著依賴自己的下意識判斷,只要你的想法是合理健壯的,那麼很可能實際上 它就是這麼做的,畢竟真理都蘊藏在平凡的生活細節中。這樣只要帶著一些薄弱的Java基礎來思考RMI,其實也可以想出個大概來。 

a) 需要有一個伺服器角色,它擁有真正的功能程式碼方法。例如B,它提供加法服務 
b) 如果想遠端使用B的功能,需要知道B的IP地址 
c) 如果想遠端使用B的功能,還需要知道B中那個特定服務的名字 

我們很自然可以想到這些,雖然不完善,但已經很接近正確的做法了。實際上RMI要得以實現還得意於Java一個很重要的特性,就是Java反射機制。我們需要知道服務的名字,但又必須隱藏實現的程式碼,如何去做呢?答案就是:介面! 
舉個例子: 
public interface Person(){ 
public void sayHello(); 


Public class PersonImplA implements Person{ 
public PersonImplA(){} 

public void sayHello(){ System.out.println(“Hello!”);} 


Public class PersonImplB implements Person{ 
public PersonImplB(){} 

public void sayHello(){ System.out.println(“Nice to meet you!”);} 


客戶端:Person p = Naming.lookup(“PersonService”); 
p.sayHello(); 

就這幾段程式碼就包含了幾乎所有的實現技術,大家相信麼?客戶端請求一個say hello服務,伺服器執行時接到這個請求,利用Java反射機制的Class.newInstance()返回一個物件,但客戶端不知道伺服器返回的是 ImplA還是ImplB,它接受用的引數簽名是Person,它知道實現了Person介面的物件一定有sayHello()方法,這就意味著客戶端並 不知道伺服器真正如何去實現的,但它通過了解Person介面明確了它要用的服務方法名字叫做sayHello()。 

如此類推,伺服器只需要暴露自己的接口出來供客戶端,所有客戶端就可以自己選擇需要的服務。這就像餐館只要拿出自己的選單出來讓客戶選擇,就可以在後臺廚房一道道的按需做出來,它怎麼做的通常是不讓客戶知道的!(祖傳菜譜吧,^_^) 

最後一點是我呼叫lookup,查詢一個叫PersonService名字的物件,伺服器只要看到這個名字,在自己的目錄(相當於電話簿)中找到對應的對 象名字提供服務就可以了,這個目錄就叫做JNDI (Java命名與目錄介面),相信大家也聽過的。 

有興趣的朋友不妨自己做個RMI的應用,很多前輩的部落格中有簡單的例子。提示一下利用Jdk的bin目錄中rmi.exe和 rmiregistry.exe兩個命令就可以自己建起一個伺服器,提供遠端服務。因為例子很容易找,我就不自己舉例子了! 

2. JVM沙箱&框架 
RMI羅唆得太多了,實在是盡力想把它說清楚,希望對大家有幫助。最後的最後,給大家簡單講一下JVM框架,我們叫做Java沙箱。Java沙箱的基本元件如下: 
a) 類裝載器結構 
b) class檔案檢驗器 
c) 內置於Java虛擬機器的安全特性 
d) 安全管理器及Java API 

其中類裝載器在3個方面對Java沙箱起作用: 
a. 它防止惡意程式碼去幹涉善意的程式碼 
b. 它守護了被信任的類庫邊界 
c. 它將程式碼歸入保護域,確定了程式碼可以進行哪些操作 

虛擬機器為不同的類載入器載入的類提供不同的名稱空間,名稱空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個名稱空間是由Java虛擬機器為每一個類裝載器維護的,它們互相之間甚至不可見。 

我們常說的包(package)是在Java虛擬機器第2版的規範第一次出現,正確定義是由同一個類裝載器裝載的、屬於同一個包、多個型別的集合。類裝載器 採用的機制是雙親委派模式。具體的載入器框架我在Java雜談(一)中已經解釋過了,當時說最外層的載入器是AppClassLoader,其實算上網路 層的話AppClassLoader也可以作為parent,還有更外層的載入器URLClassLoader。為了防止惡意攻擊由URL載入進來的類文 件我們當然需要分不同的訪問名稱空間,並且制定最安全的載入次序,簡單來說就是兩點:

a. 從最內層JVM自帶類載入器開始載入,外層惡意同名類得不到先載入而無法使用 
b. 由於嚴格通過包來區分了訪問域,外層惡意的類通過內建程式碼也無法獲得許可權訪問到內層類,破壞程式碼就自然無法生效。 

附:關於Java的平臺無關性,有一個例子可以很明顯的說明這個特性: 
一般來說,C或C++中的int佔位寬度是根據目標平臺的字長來決定的,這就意味著針對不同的平臺編譯同一個C++程式在執行時會有不同的行為。然而對於 Java中的int都是32位的二進位制補碼標識的有符號整數,而float都是遵守IEEE 754浮點標準的32位浮點數。 

PS: 這個小弟最近也沒時間繼續研究下去了,只是想拋磚引玉的提供給大家一個初步認識JVM的印象。有機會了解一下JVM的內部結構對今後做Java開發是很有好處的。

Java雜談(七)--介面& 元件、容器

終於又靜下來繼續寫這個主題的續篇,前六篇主要講了一些J2se方面的經驗和感受, 眼下Java應用範圍已經被J2ee佔據了相當大的一塊領域,有些人甚至聲稱Java被J2ee所取代了。不知道大家如何來理解所謂的J2ee (Java2 Enterprise Edition),也就是Java企業級應用? 

筆者的觀點是,技術的發展是順應世界變化的趨勢的,從C/S過渡到B/S模式,從客戶端的角度考慮企業級應用或者說電子商務領域不在關心客戶端維護問題, 這個任務已經交給了任何一臺PC都會有的瀏覽器去維護;從伺服器端的角度考慮,以往C/S中的TCP/IP協議實現載體ServerSocket被Web Server Container所取代,例如大家都很熟悉的Tomcat、JBoss、WebLogic等等。總之一切的轉變都是為了使得Java技術能更好的為人類 生產生活所服務。 

有人會問,直接去學J2ee跳過J2se行否?筆者是肯定不贊成的,實際上確實有人走這條路,但筆者自身體會是正是由於J2se的基礎很牢固,才會導致在J2ee學習的道路上順風順水,知識點上不會有什麼迷惑的地方。舉個簡單的例子吧: 

筆者曾經跟大學同學討論下面這兩種寫法的區別: 
ArrayList list = new ArrayList(); //筆者不說反對,但至少不贊成 
List list = new ArrayList(); //筆者支援 
曾經筆者跟同學爭論了幾個小時,他非說第一種寫法更科學,第二種完全沒有必要。我無法完全說服他,但筆者認為良好的習慣和意識是任何時候都應該針對介面程式設計,以達到解耦合和可擴充套件性的目的。下面就以介面開始進入J2ee的世界吧: 

1. J2ee與介面 

每一個版本的J2ee都對應著一個確定版本的JDK,J2ee1.4對應Jdk1.4,現在比較新的是JDK5.0,自然也會有J2EE 5.0。其實筆者一直在用的是J2EE1.4,不過沒什麼關係,大家可以下任何一個版本的J2ee api來稍微瀏覽一下。筆者想先宣告一個概念,J2ee也是源自Java,所以底層的操作依然呼叫到很多J2se的庫,所以才建議大家先牢牢掌握J2se 的主流技術。 

J2ee api有一個特點,大家比較熟悉的幾個包java.jms、javax.servlet.http、javax.ejb等都以interface居多,實 現類較少。其實大家真正在用的時候百分之六十以上都在反覆的查著javax.servlet.http這個包下面幾個實現類的api函式,其他的包很少問 津。筆者建議在學習一種技術之前,對整體的框架有一個瞭解是很有必要的,J2ee旨在通過interface的宣告來規範實現的行為,任何第三方的廠商想 要提供自己品牌的實現前提也是遵循這些介面定義的規則。如果在從前J2se學習的道路上對介面的理解很好的話,這裡的體會將是非常深刻的,舉個簡單的例 子: 

public interface Mp3{ 
public void play(); 
public void record(); 
public void stop(); 


如果我定義這個簡單的介面,釋出出去,規定任何第三方的公司想推出自己的名字為Mp3的產品都必須實現這個介面,也就是至少提供介面中方法的具體實現。這 個意義已經遠遠不止是面向物件的多型了,只有廠商遵循J2ee的介面定義,世界上的J2ee程式設計師才能針對統一的介面進行程式設計,最終不用改變程式碼只是 因為使用了不同廠商的實現類而有不同的特性罷了,本質上說,無論哪一種廠商實現都完成了職責範圍內的工作。這個就是筆者想一直強調的,針對介面程式設計的思 想。 

介面到底有什麼好處呢?我們這樣設想,現在有AppleMp3、SonyMp3、SamsungMp3都實現了這個Mp3的介面,於是都有了play、 record、stop這三個功能。我們將Mp3產品座位一個元件的時候就不需要知道它的具體實現,只要看到介面定義知道這個物件有3個功能就可以使用 了。那麼類似下面這樣的業務就完全可以在任何時間從3個品牌擴充套件到任意個品牌,開個玩笑的說,專案經理高高在上的寫完10個接口裡的方法宣告,然後就丟給 手下的程式設計師去寫裡面的細節,由於介面已經統一(即每個方法傳入和傳出的格式已經統一),經理只需關注全域性的業務就可以天天端杯咖啡走來走去了,^_^: 
public Mp3 create(); 
public void copy(Mp3 mp3); 
public Mp3 getMp3(); 

最後用一個簡單的例子說明介面:一個5號電池的手電筒,可以裝入任何牌子的5號電池,只要它符合5號電池的規範,裝入之後任何看不到是什麼牌子,只能感受 到手電筒在完成它的功能。那麼生產手電筒的廠商和生產5號電池的廠商就可以完全解除依賴關係,可以各自自由開發自己的產品,因為它們都遵守5號電池應有的 形狀、正負極位置等約定。這下大家能對介面多一點體會了麼? 

2. 元件和容器 
針對介面是筆者特意強調的J2ee學習之路必備的思想,另外一個就是比較常規的元件和容器的概念了。很多教材和專業網站都說J2EE的核心是一組規範與指 南,強調J2ee的核心概念就是元件+容器,這確實是無可厚非的。隨著越來越多的J2ee框架出現,相應的每種框架都一般有與之對應的容器。 

容器,是用來管理元件行為的一個集合工具,元件的行為包括與外部環境的互動、元件的生命週期、元件之間的合作依賴關係等等。J2ee包含的容器種類大約有 Web容器、Application Client容器、EJB容器、Applet客戶端容器等。但在筆者看來,現在容器的概念變得有點模糊了,大家耳熟能詳是那些功能強大的開源框架,比如 Hibernate、Struts2、Spring、JSF等,其中Hibernate就基於JDBC的基礎封裝了對事務和會話的管理,大大方便了對資料 庫操作的繁瑣程式碼,從這個意義上來說它已經接近容器的概念了,EJB的實體Bean也逐漸被以Hibernate為代表的持久化框架所取代。 

元件,本意是指可以重用的程式碼單元,一般代表著一個或者一組可以獨立出來的功能模組,在J2ee中元件的種類有很多種,比較常見的是EJB元件、DAO組 件、客戶端元件或者應用程式元件等,它們有個共同特點是分別會打包成.war,.jar,.jar,.ear,每個元件由特定格式的xml描述符檔案進行 描述,而且伺服器端的元件都需要被部署到應用伺服器上面才能夠被使用。 

稍微理解完元件和容器,還有一個重要的概念就是分層模型,最著名的當然是MVC三層模型。在一個大的工程或專案中,為了讓前臺和後臺各個模組的程式設計人員能 夠同時進行工作提高開發效率,最重要的就是實現層與層之間的耦合關係,許多分層模型的宗旨和開源框架所追求的也就是這樣的效果。在筆者看來,一個完整的 Web專案大概有以下幾個層次: 

a) 表示層(Jsp、Html、Javascript、Ajax、Flash等等技術對其支援) 
b) 控制層(Struts、JSF、WebWork等等框架在基於Servlet的基礎上支援,負責把具體的請求資料(有時解除安裝重新裝載)導向適合處理它的模型層物件) 
c) 模型層(筆者認為目前最好的框架是Spring,實質就是處理表示層經由控制層轉發過來的資料,包含著大量的業務邏輯) 
d) 資料層(Hibernate、JDBC、EJB等,由模型層處理完了持久化到資料庫中) 

當然,這僅僅是筆者個人的觀點,僅僅是供大家學習做一個參考,如果要實現這些層之間的完全分離,那麼一個大的工程,可以僅僅通過增加人手就來完成任務。雖 然《人月神話》中已經很明確的闡述了增加人手並不能是效率增加,很大程度上是因為彼此做的工作有順序上的依賴關係或者說難度和工作量上的巨大差距。當然理 想狀態在真實世界中是不可能達到的,但我們永遠應該朝著這個方向去不斷努力。最開始所提倡的針對介面來程式設計,哪怕是小小的細節,寫一條List list= = new ArrayList()語句也能體現著處處皆使用介面的思想在裡面。Anyway,這只是個開篇,筆者會就自己用過的J2ee技術和框架再細化談一些經驗.

Java雜談(八)--Servlet/Jsp


終於正式進入J2ee的細節部分了,首當其衝的當然是Servlet和Jsp了,上篇曾經提到過J2ee只是一個規範和指南,定義了一組必須要遵循的接 口,核心概念是元件和容器。曾經有的人問筆者Servlet的Class檔案是哪裡來的?他認為是J2ee官方提供的,我舉了一個簡單的反例:稍微檢查了 一下Tomcat5.0裡面的Servlet.jar檔案和JBoss裡面的Servlet.jar檔案大小,很明顯是不一樣的,至少已經說明了它們不是 源自同根的吧。其實Servlet是由容器根據J2ee的介面定義自己來實現的,實現的方式當然可以不同,只要都遵守J2ee規範和指南。 

上述只是一個常見的誤區罷了,告訴我們要編譯執行Servlet,是要依賴於實現它的容器的,不然連jar檔案都沒有,編譯都無法進行。那麼Jsp呢? Java Server Page的簡稱,是為了開發動態網頁而誕生的技術,其本質也是Jsp,在編寫完畢之後會在容器啟動時經過編譯成對應的Servlet。只是我們利用Jsp 的很多新特性,可以更加專注於前後臺的分離,早期Jsp做前臺是滿流行的,畢竟裡面支援Html程式碼,這讓前臺美工人員可以更有效率的去完成自己的工作。 然後Jsp將請求轉發到後臺的Servlet,由Servlet處理業務邏輯,再轉發回另外一個Jsp在前臺顯示出來。這似乎已經成為一種常用的模式,最 初筆者學習J2ee的時候,大量時間也在編寫這樣的程式碼。 

儘管現在做前臺的技術越來越多,例如Flash、Ajax等,已經有很多人不再認為Jsp重要了。筆者覺得Jsp帶來的不僅僅是前後端分離的設計理念,它 的另外一項技術成就了我們今天用的很多框架,那就是Tag標籤技術。所以與其說是在學習Jsp,不如更清醒的告訴自己在不斷的理解Tag標籤的意義和本 質。 

1. Servlet以及Jsp的生命週期 
Servlet是Jsp的實質,儘管容器對它們的處理有所區別。Servlet有init()方法初始化,service()方法進行Web服務, destroy()方法進行銷燬,從生到滅都由容器來掌握,所以這些方法除非你想自己來實現Servlet,否則是很少會接觸到的。正是由於很少接觸,才 容易被廣大初學者所忽略,希望大家至少記住Servlet生命週期方法都是回撥方法。回撥這個概念簡單來說就是把自己注入另外一個類中,由它來呼叫你的方 法,所謂的另外一個類就是Web容器,它只認識介面和介面的方法,注入進來的是怎樣的物件不管,它只會根據所需呼叫這個物件在介面定義存在的那些方法。由 容器來呼叫的Servlet物件的初始化、服務和銷燬方法,所以叫做回撥。這個概念對學習其他J2ee技術相當關鍵! 

那麼Jsp呢?本事上是Servlet,還是有些區別的,它的生命週期是這樣的: 
a) 一個客戶端的Request到達伺服器 -> 
b) 判斷是否第一次呼叫 -> 是的話編譯Jsp成Servlet 
c) 否的話再判斷此Jsp是否有改變 -> 是的話也重新編譯Jsp成Servlet 
d) 已經編譯最近版本的Servlet裝載所需的其他Class 
e) 釋出Servlet,即呼叫它的Service()方法 

所以Jsp號稱的是第一次Load緩慢,以後都會很快的執行。從它的生命的週期確實不難看出來這個特點,客戶端的操作很少會改變Jsp的原始碼,所以它不需 要編譯第二次就一直可以為客戶端提供服務。這裡稍微解釋一下Http的無狀態性,因為發現很多人誤解,Http的無狀態性是指每次一張頁面顯示出來了,與 伺服器的連線其實就已經斷開了,當再次有提交動作的時候,才會再次與伺服器進行連線請求提供服務。當然還有現在比較流行的是Ajax與伺服器非同步通過 xml互動的技術,在做前臺的領域潛力巨大,筆者不是Ajax的高手,這裡無法為大家解釋。 

2. Tag標籤的本質 
筆者之前說了,Jsp本身初衷是使得Web應用前後臺的開發可以脫離耦合分開有效的進行,可惜這個理念的貢獻反倒不如它帶來的Tag技術對J2ee的貢獻 要大。也許已經有很多人開始使用Tag技術了卻並不瞭解它。所以才建議大家在學習J2ee開始的時候一定要認真學習Jsp,其實最重要的就是明白標籤的本 質。 

Html標籤我們都很熟悉了,有 <html> 、 <head> 、 <body> 、 <title> ,Jsp帶來的Tag標籤遵循同樣的格式,或者說更嚴格的Xml格式規範,例如 <jsp:include> 、 <jsp:useBean> 、 <c:if> 、 <c:forEach> 等等。它們沒有什麼神祕的地方,就其源頭也還是Java Class而已,Tag標籤的實質也就是一段Java程式碼,或者說一個Class檔案。當配置檔案設定好去哪裡尋找這些Class的路徑後,容器負責將頁 面中存在的標籤對應到相應的Class上,執行那段特定的Java程式碼,如此而已。 
說得明白一點的話還是舉幾個簡單的例子說明一下吧: 

<jsp:include> 去哪裡找執行什麼class呢?首先這是個jsp類庫的標籤,當然要去jsp類庫尋找相應的class了,同樣它也是由Web容器來提供,例如 Tomcat就應該去安裝目錄的lib資料夾下面的jsp-api.jar裡面找,有興趣的可以去找一找啊! 

<c:forEach> 又去哪裡找呢?這個是由Jsp2.0版本推薦的和核心標記庫的內容,例如 <c:if> 就對應在頁面中做if判斷的功能的一斷Java程式碼。它的class檔案在jstl.jar這個類庫裡面,往往還需要和一個standard.jar類庫 一起匯入,放在具體Web專案的WEB-INF的lib目錄下面就可以使用了。 

順便羅唆一句,Web Project的目錄結構是相對固定的,因為容器會按照固定的路徑去尋找它需要的配置檔案和資源,這個任何一本J2ee入門書上都有,這裡就不介紹了。了 解Tag的本質還要了解它的工作原理,所以大家去J2ee的API裡找到並研究這個包:javax.servlet.jsp.tagext。它有一些接 口,和一些實現類,專門用語開發Tag,只有自己親自寫出幾個不同功能的標籤,才算是真正理解了標籤的原理。別忘記了自己開發的標籤要自己去完成配置文 件,容器只是集成了去哪裡尋找jsp標籤對應class的路徑,自己寫的標籤庫當然要告訴容器去哪裡找啦。 

說了這麼多,我們為什麼要用標籤呢?完全在Jsp裡面來個 <% %> 就可以在裡面任意寫Java程式碼了,但是長期實踐發現頁面程式碼統一都是與html同風格的標記語言更加有助於美工人員進行開發前臺,它不需要懂Java, 只要Java程式設計師給個列表告訴美工什麼標籤可以完成什麼邏輯功能,他就可以專注於美工,也算是進一步隔離了前後臺的工作吧!

3. 成就Web框架 
框架是什麼?曾經看過這樣的定義:與模式類似,框架也是解決特定問題的可重用方法,框架是一個描述性的構建塊和服務集合,開發人員可以用來達成某個目標。 一般來說,框架提供瞭解決某類問題的基礎設施,是用來建立解決方案的工具,而不是問題的解決方案。 

正是由於Tag的出現,成就了以後出現的那麼多Web框架,它們都開發了自己成熟實用的一套標籤,然後由特定的Xml檔案來配置載入資訊,力圖使得Web 應用的開發變得更加高效。下面這些標籤相應對很多人來說相當熟悉了: 
<html:password> 
<logic:equal> 
<bean:write> 
<f:view> 
<h:form> 
<h:message> 

它們分別來自Struts和JSF框架,最強大的功能在於控制轉發,就是MVC三層模型中間完成控制器的工作。Struts-1實際上並未做到真正的三層 隔離,這一點在Struts-2上得到了很大的改進。而Jsf向來以比較完善合理的標籤庫受到人們推崇。 

今天就大概講這麼多吧,再次需要強調的是Servlet/Jsp是學習J2ee必經之路,也是最基礎的知識,希望大家給與足夠的重視!




Java雜談(九)--Struts

J2ee的開源框架很多,筆者只能介紹自己熟悉的幾個,其他的目前在中國IT行業應用得不是很多。希望大家對新出的框架不要盲目的推崇,首先一定要熟悉它比舊的到底好在哪裡,新的理念和特性是什麼?然後再決定是否要使用它。

這期的主題是Struts,直譯過來是支架。Struts的第一個版本是在2001年5月釋出的,它提供了一個Web應用的解決方案,如何讓Jsp和 servlet共存去提供清晰的分離檢視和業務應用邏輯的架構。在Struts之前,通常的做法是在Jsp中加入業務邏輯,或者在Servlet中生成視 圖轉發到前臺去。Struts帶著MVC的新理念當時退出幾乎成為業界公認的Web應用標準,於是當代IT市場上也出現了眾多熟悉Struts的程式設計師。 即使有新的框架再出來不用,而繼續用Struts的理由也加上了一條低風險,因為中途如果開發人員變動,很容易的招進新的會Struts的IT民工啊, ^_^! 

筆者之前說的都是Struts-1,因為新出了Struts-2,使得每次談到Struts都必須註明它是Struts-1還是2。筆者先談比較熟悉的 Struts-1,下次再介紹一下與Struts-2的區別: 

1. Struts框架整體結構 
Struts-1的核心功能是前端控制器,程式設計師需要關注的是後端控制器。前端控制器是是一個Servlet,在Web.xml中間配置所有 Request都必須經過前端控制器,它的名字是ActionServlet,由框架來實現和管理。所有的檢視和業務邏輯隔離都是應為這個 ActionServlet, 它就像一個交通警察,所有過往的車輛必須經過它的法眼,然後被送往特定的通道。所有,對它的理解就是分發器,我們也可以叫做Dispatcher,其實了 解Servlet程式設計的人自己也可以寫一個分發器,加上攔截request的Filter,其實自己實現一個struts框架並不是很困難。主要目的就是 讓編寫檢視的和後臺邏輯的可以脫離緊耦合,各自同步的完成自己的工作。 

那麼有了ActionServlet在中間負責轉發,前端的檢視比如說是Jsp,只需要把所有的資料Submit,這些資料就會到達適合處理它的後端控制 器Action,然後在裡面進行處理,處理完畢之後轉發到前臺的同一個或者不同的檢視Jsp中間,返回前臺利用的也是Servlet裡面的forward 和redirect兩種方式。所以到目前為止,一切都只是借用了Servlet的API搭建起了一個方便的框架而已。這也是Struts最顯著的特性?? 控制器。 

那麼另外一個特性,可以說也是Struts-1帶來的一個比較成功的理念,就是以xml配置代替硬編碼配置資訊。以往決定Jsp往哪個servlet提 交,是要寫進Jsp程式碼中的,也就是說一旦這個提交路徑要改,我們必須改寫程式碼再重新編譯。而Struts提出來的思路是,編碼的只是一個邏輯名字,它對 應哪個class檔案寫進了xml配置檔案中,這個配置檔案記錄著所有的對映關係,一旦需要改變路徑,改變xml檔案比改變程式碼要容易得多。這個理念可以 說相當成功,以致於後來的框架都延續著這個思路,xml所起的作用也越來越大。 

大致上來說Struts當初給我們帶來的新鮮感就這麼多了,其他的所有特性都是基於方便的控制轉發和可擴充套件的xml配置的基礎之上來完成它們的功能的。 
下面將分別介紹Action和FormBean, 這兩個是Struts中最核心的兩個元件。 

2. 後端控制器Action 
Action就是我們說的後端控制器,它必須繼承自一個Action父類,Struts設計了很多種Action,例如DispatchAction、 DynaValidationAction。它們都有一個處理業務邏輯的方法execute(),傳入的request, response, formBean和actionMapping四個物件,返回actionForward物件。到達Action之前先會經過一個 RequestProcessor來初始化配置檔案的對映關係,這裡需要大家注意幾點: 

1) 為了確保執行緒安全,在一個應用的生命週期中,Struts框架只會為每個Action類建立一個Action例項,所有的客戶請求共享同一個Action 例項,並且所有執行緒可以同時執行它的execute()方法。所以當你繼承父類Action,並添加了private成員變數的時候,請記住這個變數可以 被多個執行緒訪問,它的同步必須由程式設計師負責。(所有我們不推薦這樣做)。在使用Action的時候,保證執行緒安全的重要原則是在Action類中僅僅使用 區域性變數,謹慎的使用例項變數。區域性變數是對每個執行緒來說私有的,execute方法結束就被銷燬,而例項變數相當於被所有執行緒共享。 

2) 當ActionServlet例項接收到Http請求後,在doGet()或者doPost()方法中都會呼叫process()方法來處理請求。 RequestProcessor類包含一個HashMap,作為存放所有Action例項的快取,每個Action例項在快取中存放的屬性key為 Action類名。在RequestProcessor類的processActionCreate()方法中,首先檢查在HashMap中是否存在 Action例項。建立Action例項的程式碼位於同步程式碼塊中,以保證只有一個執行緒建立Action例項。一旦執行緒建立了Action例項並把它存放到 HashMap中,以後所有的執行緒會直接使用這個快取中的例項。 

3) <action> 元素的 <roles> 屬性指定訪問這個Action使用者必須具備的安全形色,多個角色之間逗號隔開。RequestProcessor類在預處理請求時會呼叫自身的 processRoles()方法,檢查配置檔案中是否為Action配置了安全形色,如果有,就呼叫HttpServletRequest的 isUserInRole()方法來判斷使用者是否具備了必要的安全性角色,如果不具備,就直接向客戶端返回錯誤。(返回的檢視通過 <input> 屬性來指定) 

3. 資料傳輸物件FormBean 
Struts並沒有把模型層的業務物件直接傳遞到檢視層,而是採用DTO(Data Transfer Object)來傳輸資料,這樣可以減少傳輸資料的冗餘,提高傳輸效率;還有助於實現各層之間的獨立,使每個層分工明確。Struts的DTO就是 ActionForm,即formBean。由於模型層應該和Web應用層保持獨立。由於ActionForm類中使用了Servlet API, 因此不提倡把ActionForm傳遞給模型層, 而應該在控制層把ActionForm Bean的資料重新組裝到自定義的DTO中, 再把它傳遞給模型層。它只有兩個scope,分別是session和request。(預設是session)一個ActionForm標準的生命週期 是: 
1) 控制器收到請求 -> 
2) 從request或session中取出ActionForm例項,如不存在就建立一個 -> 
3) 呼叫ActionForm的reset()方法 -> 
4) 把例項放入session或者request中 -> 
5) 將使用者輸入表達資料組裝到ActionForm中 -> 
6) 如眼張方法配置了就呼叫validate()方法 -> 
7) 如驗證錯誤就轉發給 <input> 屬性指定的地方,否則呼叫execute()方法 

validate()方法呼叫必須滿足兩個條件: 
1) ActionForm 配置了Action對映而且name屬性匹配 
2) <aciton> 元素的validate屬性為true 

如果ActionForm在request範圍內,那麼對於每個新的請求都會建立新的ActionForm例項,屬性被初始化為預設值,那麼reset ()方法就顯得沒有必要;但如果ActionForm在session範圍內,同一個ActionForm例項會被多個請求共享,reset()方法在這 種情況下極為有用。