J2EE能在大規(guī)模項目中得到廣泛應用,我覺得主要有以下幾方面原因:
1、JSP、Servlet的方式能夠很容易的將模塊進行劃分,而且模塊間很容易做到互不影響,幾個頁面有問題不會影響其它不相關的業(yè)務模塊運行,因此做到不同模塊不同時間上線的方式
2、容器已經(jīng)處理好了很多基礎工作,一般程序員不用管socket通訊的并發(fā)、不用管如果使用線程池、不用管資源的同步死鎖問題、不用管數(shù)據(jù)庫連接池的實現(xiàn)、再加上Java的自動GC功能以及現(xiàn)在的硬件條件,即使不懂Java的程序員只要通過短期的項目經(jīng)歷,一般都能很快參與業(yè)務代碼的堆積(也許說這話有點侮辱程序員,不過不得不承認現(xiàn)在絕大部分公司拉的項目無非就是基于某個行業(yè)業(yè)務的增、刪、改、查+統(tǒng)計報表)
3、web容器的reload功能大大提高了開發(fā)效率,修改了頁面或者業(yè)務類代碼不需要重新啟動JVM就能通過ClassLoader加載新的修改過得類進行測試,這點即使在上線運行時也起了很大的作用,由于現(xiàn)在我們的項目都是基于XMLHTTP的方式,在提交時用戶界面數(shù)據(jù)保持不變,如果提交出錯,打個電話過來改改后臺代碼,然后用戶再次點擊提交一切就OK了,這點相對于以前將業(yè)務代碼和UI界面綁定一塊,每次修改代碼得重新啟動JVM的CS程序是不可思議的方便
回到今天的話題,以上的第3點可是存在陷阱的地方,以下論述是基于WebLogic的測試結果:
當我們修改某個Java類保存編譯后你會發(fā)現(xiàn)console輸出控制臺似乎什么都沒發(fā)生,其實如果你在每個類加上 static{ System.out.println(“loading XXX.class”); },你會發(fā)現(xiàn)任何類的修改將會導致所有(甚至與改修改類沒有任何瓜葛的類)應用類被重新加載,當然weblogic是用lazy load的方式,你調(diào)用到那個類就重新加載哪個類。這樣如果系統(tǒng)中緩存的數(shù)據(jù)以及已經(jīng)設置為某種狀態(tài)的static靜態(tài)屬性將會被重新初始化,這樣就很可能破壞某些正常的業(yè)務邏輯,出現(xiàn)奇怪的讓人覺得不可能發(fā)生的問題:“我明明初始化了這個實例了…,我明明通過跟蹤斷點發(fā)現(xiàn)某某屬性已經(jīng)被我設置為…,怎么現(xiàn)在又成了….”。
還有一種出錯的情況,對于需要定時任務的系統(tǒng)(例如簡單的java.util.Timer或者功能強大的開源quartz)也許你會實現(xiàn)為類似如下方式:

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25


2

3

通過上下文監(jiān)聽啟動和關閉定時器,正常運行是沒有問題的,但是如果你修改了類導致容器重新加載class那么問題出現(xiàn)了,例如你可以試著將 System.out.println(“i am doing at ” + new Date());
簡單修改為 System.out.println(“i am new doing at ” + new Date());接著隨便找個jsp運行這樣的代碼:
System.out.println(“=========================================”);
(new Task()).run();
你會發(fā)現(xiàn)在輸出日志種將出現(xiàn)“新老類共存”的效果:
i am doing at Sat Jun 25 22:45:27 CST 2005
i am doing at Sat Jun 25 22:45:37 CST 2005
i am doing at Sat Jun 25 22:46:01 CST 2005
=========================================
i am new doing at Sat Jun 25 22:46:02 CST 2005
i am doing at Sat Jun 25 22:46:11 CST 2005
i am doing at Sat Jun 25 22:46:21 CST 2005
i am doing at Sat Jun 25 22:46:31 CST 2005
而且這時如果你想通過Task.stop();停止定時器,對不起,那個第一次被啟動的Task已經(jīng)是“另一個空間”的東東了,你是觸及不到的了,如果你再調(diào)用Task.start()那系統(tǒng)將有兩個Timer在跑一個跑著老的類,一個跑著新的類,你可以調(diào)用Task.stop();關閉新的Timer但是老的Timer只有redeploy或者重啟整個JVM才能關閉了,終其原因是重新加載類和重新部署web工程效果是不一樣的,reload class并不調(diào)用監(jiān)聽器的contextDestroyed和contextInitialized函數(shù),所以這樣情況下用Servlet是個不錯的選擇:

2

3

4

5

6

7

8


2

3

4

5

如果你修改了Task類,而且Task被調(diào)用到并且reload了,但是TestServlet還沒被調(diào)用到,所以還沒reload TestServlet那么也是會出現(xiàn)新老類并存的現(xiàn)象,但是一旦TestServlet被觸及那么在reload之前destroy將會被調(diào)用,接著init將會被調(diào)用,這樣系統(tǒng)將會恢復正常狀態(tài),老類不再運行,只有新類在運行,用Servlet的方式至少不會出現(xiàn)老類永遠觸及不到無非關閉的情況。
按照這種說法Servlet似乎可以解決所有問題了,其實非也,如果你只用Servlet的“正統(tǒng)”用法操作不會有任何問題,但是如果你在某些情況下需要不通過Servlet的方式而是用普通類的方式直接操作Servlet的函數(shù)那么問題就來了。舉個例子:我們利用Servlet在init()時啟動quartz任務定制器,但是在系統(tǒng)運行中我們需要添加新的任務和修改任務參數(shù),簡單的方式就是全部重新加載所有任務,為此你也許回在servlet中提供public static void reload()的函數(shù),這就是問題的根源了,例如在調(diào)用reload之前有其他的類(或者就是這個Servlet本身)被修改過了,當你調(diào)用reload函數(shù)時這個Servlet需要reload class,但是由于你并非通過“正統(tǒng)”的Servlet訪問方式操作,而是用類的普通調(diào)用方式操作的,所以weblogic容器(其他容器我還沒測試過,也許會有不一樣的效果)在reload class時不再調(diào)用Servlet的public void destroy()函數(shù),并且在reload class是也不再調(diào)用Servlet的public void init()函數(shù),這樣的話以前啟動的org.quartz.Scheduler實例再也沒有機會調(diào)用scheduler.shutdown(false)進行關閉了。以下代碼是個不優(yōu)雅但是管用的方法:

2

3

4

5

6

7

8

9

10

11

12

這樣在“非正統(tǒng)”的SchedulerServlet.reload()調(diào)用之間已經(jīng)有個src = “/servlet/SchedulerServlet”的“正統(tǒng)”調(diào)用被除觸發(fā)了。不過以上解決方法純粹脫褲放屁,直接通過HTTP請求servlet的get、post函數(shù)進行處理不就得了,所以以下的方案才是正道:

2

3

4

5

6

7

8

9

10

11

最后讓我們來研究一下reload class對容器中HTTP會話session的影響,我們在開發(fā)時修改類并不需要重新登錄就能進行測試,說明reload class時session還保存著用戶數(shù)據(jù),那么這些數(shù)據(jù)是完整的沒問題的嗎?眼見為實先讓我們做幾個測試:

2

3

4


5

6

7


8

9

10

11

12

13

14

15

16


17

18

19


20

21

22

23


24

25

26

27


28

29

30

31

32

33

34

35


36

37

38


39

40

41

42


43

44

45

46


47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

輸出日志:
【##############################】
NoSerial構造中…
true | test.NoSerial@a2dbe8
【——————————】
Data構造中…
true | test.Data@138ce2
【******************************】
ExterData構造中…
true | test.ExterData@18654c0

2

3

4

5

6

7

8

9

10

11

12

13

輸出日志:
【##############################】
false | test.NoSerial@6b3836
【——————————】
writeObject…
loading Data.class…
readObject…
true | test.Data@12a14a6
【******************************】
writeExternal…
loading ExterData.class…
ExterData構造中…
readExternal…
true | test.ExterData@14ea478
通過以上的測試可知reload class對于容器session中的內(nèi)容的影響:
對于非可串行話的實例NoSerial容器不進行任何操作,這樣這個NoSerial實例仍然為以前的classLoad加載進來的class創(chuàng)建的實例,與現(xiàn)在重新加載的class已經(jīng)沒有關系了,所以進行instanceof的判斷時結果為false,因此如果你想NoSerial noSerial = (NoSerial)session.getAttribute(“NoSerial”);這樣獲取該實例的話將會拋出類ClassCastException異常
對于實現(xiàn)了Serializable或者Externalizable接口的串行化類容器在重新加載類前將會先串行化(writeObject和writeExternal)保存實例內(nèi)容,然后加載類loading XXX.class,最后重新初始化實例(這里有點小細節(jié),對于Serializable沒有調(diào)用不帶參數(shù)的構造函數(shù),對于Externalizable進行調(diào)用了,所以有l(wèi)oading ExterData.class…日志的輸出)調(diào)用readObject和readExternal恢復原來的數(shù)據(jù),因此進行instanceof的判斷時結果為true,可以安全的取出來操作。所以對于需要保存再session中的類最好實現(xiàn)為可串行化的,這不僅有利于容器在內(nèi)存受限時將會話內(nèi)容保存到磁盤避免outofmemory的危險,而且在reload class后還可以正常操作session中的對象內(nèi)容。
對于實現(xiàn)串行化有有幾點需要說明一下, 如果你不設置private static final long serialVersionUID = XXXL;屬性,那么當你改動類的屬性、函數(shù)名(函數(shù)的實現(xiàn)修改不要緊,JVM計算serialVersionUID的哈西算法并不考慮函數(shù)內(nèi)容,所以修改函數(shù)內(nèi)容并不會影響serialVersionUID值)等信息時會造成JVM計算的serialVersionUID前后不一致,會出現(xiàn)java.io.InvalidClassException: zt.cims.utils.test.Data; local class incompatible: stream classdesc serialVersionUID = -8934056548762896729, local class serialVersionUID = -5363502546919843732之類的異常。如果你設置了serialVersionUID屬性那么默認的串行化讀取時會運用“最小化損失”原則進行屬性匹配進行賦值,也就是說如果以前有i、j屬性,現(xiàn)在添加了k屬性那么i、j屬性將會被填充而k不進行處理,至于函數(shù)部分你可以任意改動甚至是函數(shù)名和參數(shù)。
最后對于序列化方面TWaver Java的TWaverUtil上有幾個函數(shù)挺方便的,也許你用得上

2

3

4
