J2EE能在大規模項目中得到廣泛應用,我覺得主要有以下幾方面原因:
1、JSP、Servlet的方式能夠很容易的將模塊進行劃分,而且模塊間很容易做到互不影響,幾個頁面有問題不會影響其它不相關的業務模塊運行,因此做到不同模塊不同時間上線的方式
2、容器已經處理好了很多基礎工作,一般程序員不用管socket通訊的并發、不用管如果使用線程池、不用管資源的同步死鎖問題、不用管數據庫連接池的實現、再加上Java的自動GC功能以及現在的硬件條件,即使不懂Java的程序員只要通過短期的項目經歷,一般都能很快參與業務代碼的堆積(也許說這話有點侮辱程序員,不過不得不承認現在絕大部分公司拉的項目無非就是基于某個行業業務的增、刪、改、查+統計報表)
3、web容器的reload功能大大提高了開發效率,修改了頁面或者業務類代碼不需要重新啟動JVM就能通過ClassLoader加載新的修改過得類進行測試,這點即使在上線運行時也起了很大的作用,由于現在我們的項目都是基于XMLHTTP的方式,在提交時用戶界面數據保持不變,如果提交出錯,打個電話過來改改后臺代碼,然后用戶再次點擊提交一切就OK了,這點相對于以前將業務代碼和UI界面綁定一塊,每次修改代碼得重新啟動JVM的CS程序是不可思議的方便
回到今天的話題,以上的第3點可是存在陷阱的地方,以下論述是基于WebLogic的測試結果:
當我們修改某個Java類保存編譯后你會發現console輸出控制臺似乎什么都沒發生,其實如果你在每個類加上 static{ System.out.println(“loading XXX.class”); },你會發現任何類的修改將會導致所有(甚至與改修改類沒有任何瓜葛的類)應用類被重新加載,當然weblogic是用lazy load的方式,你調用到那個類就重新加載哪個類。這樣如果系統中緩存的數據以及已經設置為某種狀態的static靜態屬性將會被重新初始化,這樣就很可能破壞某些正常的業務邏輯,出現奇怪的讓人覺得不可能發生的問題:“我明明初始化了這個實例了…,我明明通過跟蹤斷點發現某某屬性已經被我設置為…,怎么現在又成了….”。
還有一種出錯的情況,對于需要定時任務的系統(例如簡單的java.util.Timer或者功能強大的開源quartz)也許你會實現為類似如下方式:
1
public class Task extends TimerTask
{
2
public static Timer timer = new Timer();
3
static
{
4
System.out.println("loading Task.class " + timer);
5
}
6
public void run()
{
7
System.out.println("i am doing at " + new Date());
8
}
9
public static void start()
{
10
Task.timer.schedule(new Task(), 5000, 10000);
11
}
12
public static void stop()
{
13
Task.timer.cancel();
14
}
15
}
16
17
public class SchedulerManager
18
implements ServletContextListener
{
19
public void contextInitialized(ServletContextEvent arg0)
{
20
Task.start();
21
}
22
public void contextDestroyed(ServletContextEvent arg0)
{
23
Task.stop();
24
}
25
}
1
<listener>
2
<listener-class>test.SchedulerManager</listener-class>
3
</listener>
通過上下文監聽啟動和關閉定時器,正常運行是沒有問題的,但是如果你修改了類導致容器重新加載class那么問題出現了,例如你可以試著將 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();
你會發現在輸出日志種將出現“新老類共存”的效果:
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已經是“另一個空間”的東東了,你是觸及不到的了,如果你再調用Task.start()那系統將有兩個Timer在跑一個跑著老的類,一個跑著新的類,你可以調用Task.stop();關閉新的Timer但是老的Timer只有redeploy或者重啟整個JVM才能關閉了,終其原因是重新加載類和重新部署web工程效果是不一樣的,reload class并不調用監聽器的contextDestroyed和contextInitialized函數,所以這樣情況下用Servlet是個不錯的選擇:
1
public class TestServlet extends HttpServlet
{
2
public void init() throws ServletException
{
3
Task.start();
4
}
5
public void destroy()
{
6
Task.stop();
7
}
8
}
1
<servlet>
2
<servlet-name>TestServlet</servlet-name>
3
<servlet-class>test.TestServlet</servlet-class>
4
<load-on-startup>1</load-on-startup>
5
</servlet>
如果你修改了Task類,而且Task被調用到并且reload了,但是TestServlet還沒被調用到,所以還沒reload TestServlet那么也是會出現新老類并存的現象,但是一旦TestServlet被觸及那么在reload之前destroy將會被調用,接著init將會被調用,這樣系統將會恢復正常狀態,老類不再運行,只有新類在運行,用Servlet的方式至少不會出現老類永遠觸及不到無非關閉的情況。
按照這種說法Servlet似乎可以解決所有問題了,其實非也,如果你只用Servlet的“正統”用法操作不會有任何問題,但是如果你在某些情況下需要不通過Servlet的方式而是用普通類的方式直接操作Servlet的函數那么問題就來了。舉個例子:我們利用Servlet在init()時啟動quartz任務定制器,但是在系統運行中我們需要添加新的任務和修改任務參數,簡單的方式就是全部重新加載所有任務,為此你也許回在servlet中提供public static void reload()的函數,這就是問題的根源了,例如在調用reload之前有其他的類(或者就是這個Servlet本身)被修改過了,當你調用reload函數時這個Servlet需要reload class,但是由于你并非通過“正統”的Servlet訪問方式操作,而是用類的普通調用方式操作的,所以weblogic容器(其他容器我還沒測試過,也許會有不一樣的效果)在reload class時不再調用Servlet的public void destroy()函數,并且在reload class是也不再調用Servlet的public void init()函數,這樣的話以前啟動的org.quartz.Scheduler實例再也沒有機會調用scheduler.shutdown(false)進行關閉了。
以下代碼是個不優雅但是管用的方法:
1
<IMG style="display:none" id="SchedulerServlet" BORDER="1" WIDTH=0 HEIGHT=0/>
2
3
<SCRIPT LANGUAGE="JavaScript">
4
<!--
5
function reload()
{
6
document.all("SchedulerServlet").src = "/servlet/SchedulerServlet";
7
if(window.confirm("你確信要重新加載作業信息?"))
{
8
××× 通過XMLHTTP遠程調用SchedulerServlet.reload() ×××
9
}
10
}
11
//-->
12
</SCRIPT>
這樣在“非正統”的SchedulerServlet.reload()調用之間已經有個src = “/servlet/SchedulerServlet”的“正統”調用被除觸發了。不過以上解決方法純粹脫褲放屁,直接通過HTTP請求servlet的get、post函數進行處理不就得了,所以以下的方案才是正道:
1
function reload()
{
2
if(window.confirm("你確信要重新加載作業信息?"))
{
3
document.all("SchedulerManager").src = "/SchedulerManager?action=reload";
4
}
5
}
6
7
function shutdown()
{
8
if(window.confirm("你確信要定制所有作業?"))
{
9
document.all("SchedulerManager").src = "/SchedulerManager?action=shutdown";
10
}
11
}
最后讓我們來研究一下reload class對容器中HTTP會話session的影響,我們在開發時修改類并不需要重新登錄就能進行測試,說明reload class時session還保存著用戶數據,那么這些數據是完整的沒問題的嗎?眼見為實先讓我們做幾個測試:
1
// 不可串行化的普通NoSerial類
2
public class NoSerial
{
3
static
{
4
System.out.println("loading NoSerial.class
");
5
}
6
public NoSerial()
{
7
System.out.println("NoSerial構造中
");
8
}
9
public int prop = 1;
10
}
11
12
// 實現Serializable接口的可串行化Data類,writeObject與readObject可以不實現
13
// 對這兩個函數的實現只是簡單的調用了默認的操作,為了輸出日志方便監控過程
14
public class Data implements Serializable
{
15
static
{
16
System.out.println("loading Data.class
");
17
}
18
public Data()
{
19
System.out.println("Data構造中
");
20
}
21
public int prop = 1;
22
private void writeObject(ObjectOutputStream stream) throws IOException
{
23
System.out.println("writeObject
");
24
stream.defaultWriteObject();
25
}
26
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
{
27
System.out.println("readObject
");
28
stream.defaultReadObject();
29
}
30
}
31
32
// 實現Externalizable接口的可串行化ExterData類
33
public class ExterData implements Externalizable
{
34
static
{
35
System.out.println("loading ExterData.class
");
36
}
37
public ExterData()
{
38
System.out.println("ExterData構造中
");
39
}
40
public int prop = 1;
41
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
{
42
System.out.println("readExternal
");
43
prop = in.readInt();
44
}
45
public void writeExternal(ObjectOutput out) throws IOException
{
46
System.out.println("writeExternal
");
47
out.writeInt(prop);
48
}
49
}
50
51
//////////////// 在session中存入一下三個類的實例 //////////////////
52
System.out.println("【##############################】");
53
session.setAttribute("NoSerial", new test.NoSerial());
54
Object noSerial = session.getAttribute("NoSerial");
55
System.out.println((noSerial instanceof NoSerial) + " | " + noSerial);
56
System.out.println("【------------------------------】");
57
session.setAttribute("data", new test.Data());
58
Object data = session.getAttribute("data");
59
System.out.println((data instanceof Data) + " | " + data);
60
System.out.println("【******************************】");
61
session.setAttribute("ExterData", new test.ExterData());
62
Object exterData = session.getAttribute("ExterData");
63
System.out.println((exterData instanceof ExterData) + " | " + exterData);
輸出日志:
【##############################】
NoSerial構造中…
true | test.NoSerial@a2dbe8
【——————————】
Data構造中…
true | test.Data@138ce2
【******************************】
ExterData構造中…
true | test.ExterData@18654c0
1
// 屏蔽掉session.setAttribute的代碼,并且隨便修改一下其他的類,觸發一下容器的reload class //
2
System.out.println("【##############################】");
3
//session.setAttribute("NoSerial", new test.NoSerial());
4
Object noSerial = session.getAttribute("NoSerial");
5
System.out.println((noSerial instanceof NoSerial) + " | " + noSerial);
6
System.out.println("【------------------------------】");
7
//session.setAttribute("data", new test.Data());
8
Object data = session.getAttribute("data");
9
System.out.println((data instanceof Data) + " | " + data);
10
System.out.println("【******************************】");
11
//session.setAttribute("ExterData", new test.ExterData());
12
Object exterData = session.getAttribute("ExterData");
13
System.out.println((exterData instanceof ExterData) + " | " + exterData);
輸出日志:
【##############################】
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中的內容的影響:
對于非可串行話的實例NoSerial容器不進行任何操作,這樣這個NoSerial實例仍然為以前的classLoad加載進來的class創建的實例,與現在重新加載的class已經沒有關系了,所以進行instanceof的判斷時結果為false,因此如果你想NoSerial noSerial = (NoSerial)session.getAttribute(“NoSerial”);這樣獲取該實例的話將會拋出類ClassCastException異常
對于實現了Serializable或者Externalizable接口的串行化類容器在重新加載類前將會先串行化(writeObject和writeExternal)保存實例內容,然后加載類loading XXX.class,最后重新初始化實例(這里有點小細節,對于Serializable沒有調用不帶參數的構造函數,對于Externalizable進行調用了,所以有loading ExterData.class…日志的輸出)調用readObject和readExternal恢復原來的數據,因此進行instanceof的判斷時結果為true,可以安全的取出來操作。所以對于需要保存再session中的類最好實現為可串行化的,這不僅有利于容器在內存受限時將會話內容保存到磁盤避免outofmemory的危險,而且在reload class后還可以正常操作session中的對象內容。
對于實現串行化有有幾點需要說明一下, 如果你不設置private static final long serialVersionUID = XXXL;屬性,那么當你改動類的屬性、函數名(函數的實現修改不要緊,JVM計算serialVersionUID的哈西算法并不考慮函數內容,所以修改函數內容并不會影響serialVersionUID值)等信息時會造成JVM計算的serialVersionUID前后不一致,會出現java.io.InvalidClassException: zt.cims.utils.test.Data; local class incompatible: stream classdesc serialVersionUID = -8934056548762896729, local class serialVersionUID = -5363502546919843732之類的異常。如果你設置了serialVersionUID屬性那么默認的串行化讀取時會運用“最小化損失”原則進行屬性匹配進行賦值,也就是說如果以前有i、j屬性,現在添加了k屬性那么i、j屬性將會被填充而k不進行處理,至于函數部分你可以任意改動甚至是函數名和參數。
最后對于序列化方面TWaver Java的TWaverUtil上有幾個函數挺方便的,也許你用得上
1
public static byte[] toBytes(Object object)
2
public static Object toObject(byte[] bytes)
3
public static byte[] toByteByGZIP(Object object)
4
public static Object toObjectByGZIP(byte[] bytes)