Disable JSESSIONID URL Rewriting in JSF

最近在工作上用 JSF 寫程式時,發現如果用 <h:outputLink /> 這個 tag 來輸出 <a href=”xxxx” /> 之類的連結,在瀏覽器首次瀏覽時,JSF 產生的連結會自動將 jsessionid 附在 URL 的後面。

例如下圖,我用 <h:outputLink value=”#{bundle[‘link.hinet’]}” /> 產生一個 Hinet 的連結(link.hinet 定義於 ResourceBundle 中)。首次執行時,可以看出在 Hinet 連結後面 JSF 自動幫忙加上了 jsessionid 的參數。

如果目的地不是一個 J2EE Web 應用程式,直接點下去,很容易會出現 404 Not Found 的錯誤訊息。

初步猜想應該是首次瀏覽時,JSF 針對 jsessionid 做了 URL Rewriting。因為首次瀏覽時,JSF 還不知道瀏覽器是否有將 Cookie enable,所以除了嘗試要將 jsessionid 設定到 Cookie 之外,另外在 URL 上面也針對 session id 做了一次重寫,以便保證 session 可以傳遞到下一個動作。

實際驗證的結果,在首次瀏覽之後,再將 URL 中的 jsessionid 參數移除,在功能上也是完全不受影響,因此我可以確認應該是這個問題。找了很久,從書上跟 Google 找來找去,似乎也沒看到 JSF 上有可以將這個自動重寫機制關掉的參數。所以看起來只能從 J2EE Web Context 來想辦法了。

從 Google 上找到了一篇文章 禁用JavaWeb應用中URL上包含的jsessionid ,裡面提到的方式就是先繼承 HttpServletResponse,實作一個自己的 HttpServletResponseWrapper,並且將所有相關的 encodeUrl 函式全部改寫,直接回傳 URL,不再加以處理。再搭配 Filter 來處理每一個 Request / Response。

1
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

public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {
	// TODO Auto-generated method stub
	if (!(request instanceof HttpServletRequest)) {  
           chain.doFilter(request, response);  
		return;  
	}  

	HttpServletRequest httpRequest = (HttpServletRequest) request;  
	HttpServletResponse httpResponse = (HttpServletResponse) response;  		
	if (httpRequest.isRequestedSessionIdFromURL()) {  
	    HttpSession session = httpRequest.getSession();  
		if (session != null) session.invalidate();  
	}  		
	
	HttpServletResponseWrapper wrappedResponse  
	    = new HttpServletResponseWrapper(httpResponse) 
	{  
	    public String encodeRedirectUrl(String url) { return url; }  
	    public String encodeRedirectURL(String url) { return url; }  
	    public String encodeUrl(String url) { return url; }  
	    public String encodeURL(String url) { return url; }  
	 };
	 
	 chain.doFilter(request, wrappedResponse);
}

所以後續 JSF 呼叫 encodeUrl 來進行 rewriting 時,自然 jsessionid 也不會被加上去了。

實際部署後,果然首次瀏覽時,URL 就不會再出現 jsessionid 了。不過如此的結果就是 Client 端必須啟動 Cookie 了。

下載 DisableUrlSessionFilter.java

JFreeChart 在 Linux 下中文亂碼的問題

使用 Jakarta JMeter 來自動量測網銀效能已經一段時間了,直到今年五月前都是請工讀生將量測的數據用 Excel 整理成報表。不過由於工讀生人力縮減,這項整理報表工作勢必得想辦法變成自動化,於是我便找時間將報表分析作業用 Struts / Spring,弄了一個小網站方便查詢整理分析的效能數據。

其中有項報表是以曲線圖表顯示每項量測個案歷史區間的效能起伏變化,原本 Excel 拿來作圖表非常簡單,但是換成用程式自動產生圖表就有點麻煩了。後來找到一個 OpenSource 繪製圖表的 Library – JFreeChart,這個函式庫支援非常多的圖表,舉凡柱狀圖、圓餅圖、曲線圖、DashBoard…,實在是不勝枚舉。研究了一下,加上股溝上面找來的 sample code,七拼八湊終於也完成了這個圖表。

不過等到部署到 Broso Fedora 上面後,才發現怎麼原本在 Windows Desktop 上顯示正常的中文字體,在 Linux 執行後全部變成了小方塊。

 

查了一下才知道,原來必須將中文字體加入 JRE Library 的 font path 中。稍微記錄一下。

  1. 察看 /usr/share/fonts/zh_TW/TrueType,下面有沒有中文字型檔,如果沒有,將 Windows 下的細明體檔案複製到此。
  2. 察看 $JAVA_HOME/jre/lib 下面有沒有 fontconfig.RedHat.properties 這個檔案。如果沒有,複製 fontconfig.RedHat.properties.src 成為 fontconfig.RedHat.properties。
  3. 修改 fontconfig.RedHat.properties ,新增一組設定。filename.-misc-zysong18030-medium-r-normal–*-%d-*-*-c-*-iso10646-1=/usr/share/fonts/zh_TW/TrueType/bsmi00lp.ttf。
  4. 重新啟動 Application Server,搞定。

GoogleDesktopProxy

自從使用了 GoogleDesktop 之後,它強大的搜尋信件文件能力,讓我減少了許多尋找資料的時間。尤其是在辦公室裡,常常記得某個資料曾經看過,找了很久但就是找不到放在哪兒。這個時候,GoogleDesktop 就發揮了很大的效用,輸入關鍵字後,馬上就幫你將所有相關的文件通通找出來列給你。

最近公司同事想把一些文件整理以後放在一台公用電腦上面,然後利用 GoogleDesktop 讓大家可以利用關鍵字搜尋來尋找有用的資料。經過實驗之後,假設公用電腦的 IP 是 192.168.1.100,由於 GoogleDesktop 只會 Listen 127.0.0.1 這個 loopback 的 IP,因此其他同網段(192.168.1.*) 的電腦無法連結到 GoogleDesktop 的搜尋網址。

碰到這種問題,最直接的想法就是搞一個 proxy server,讓 proxy server listen 192.168.1.100:4664,然後將所有連往 192.168.1.100:4664 的 packet 全部 forward 到 127.0.0.1:4664,如此一來應該就可以連到 GoogleDesktop 的搜尋網址。不過很遺憾的,經過實驗結果,會出現下面情形..

我初步猜想是 Browser 送過來的 HTTP header 中,GoogleDesktop 只接受 Host: 127.0.0.1:4664。所以馬上用 telnet 動手實驗了一下

[root@broso shell]# telnet 192.168.1.12 4664
Trying 192.168.1.12…
Connected to 192.168.1.12.
Escape character is '^]'.
GET /&s=Gcp35yLaRKNq-qK-MNo25UNQjO0 HTTP/1.1
Host: 192.168.1.12:4664
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Big5,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Proxy-Connection: keep-alive

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Pragma: no-cache
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Cache-control: no-cache, no-store, must-revalidate
Connection: close

<!–
Content-type: fix-mhtml

–><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><title>錄無效的要求</title>
<style>

後面省略

把上行的 Host: 192.168.1.12:4664 改為 Host: 127.0.0.1:4664 之後,結果真的就成功了,GoogleDesktop 就會回覆正確的搜尋頁面了..

[root@broso shell]# telnet 192.168.1.12 4664
Trying 192.168.1.12…
Connected to 192.168.1.12.
Escape character is '^]'.
GET /&s=Gcp35yLaRKNq-qK-MNo25UNQjO0 HTTP/1.1
Host: 127.0.0.1:4664
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Big5,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Proxy-Connection: keep-alive

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Pragma: no-cache
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Cache-control: no-cache, no-store, must-revalidate
Connection: close

<!–
Content-type: fix-mhtml

–><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><title>Google 桌面</title>

後面省略

知道 GoogleDesktop 原理之後,馬上動手來改寫個小型 Proxy Server。先前為了能夠從公司內部自由穿梭到外部 internet 世界,曾經寫了一個小型 SSL 加密的 Proxy Server(這個以後有機會再來寫),因此我的想法很簡單,是只要將中間穿梭的封包加以處理一下,碰到 Host: 192.168.1.100:4664 的 HTTP header,就置換為 Host: 127.0.0.1:4664。理論上應該就沒問題才是。

改完之後,馬上試驗一下,果然頁面很成功出現了。

不過再試下去,又發現一些問題…

咦,怎麼網址又被改為 http://localhost 了,看起來除了上行的 HTTP Header 要置換之外,連下行的 HTTP Header 及 HTML content 都得做一下過濾跟置換。所以又修改了一下程式,碰到 http://localhost 或是 http://127.0.0.1 都置換為 http://192.168.1.100:4664 。修改完畢後,馬上又試驗了一下… 

果然馬上就成功了。有興趣的人可以在這裡下載 GoogleDesktopProxy

簡單說明一下安裝方法:

  1. 將下載回來的 GoogleDesktopProxy.rar 解開放在同一個目錄下。
  2. 修改 start_proxy.cmd,將 "C:\Program Files\Java\jre1.6.0_03\bin\java" 改為你所使用的 JRE 或 JDK 路徑
  3. 修改 proxy.properties 中,我標為黃色字體的部分

參數 Host 指的是安裝 GoogleDesktop 那台電腦的 IP
參數 AllowList 指的是允許連接 GoogleDesktopProxy 的 IP 清單,所以沒列在裡面的 IP 都會被檔掉唷
參數 ReplaceLineFrom 跟 ReplaceLineTo 這兩個參數是成對的,GoogleDesktopProxy 會過濾所有經過的封包,是不是包含了 ReplaceLineFrom 所指定的字串,如果有,就置換為 ReplaceLineTo 所指定的字串。
所以 ReplaceLineFrom_1 請同樣修改為安裝 GoogleDesktop 那台電腦的 IP。ReplaceLineFrom_2 跟 ReplaceLineTo_3 亦同。


Host=192.168.1.12
Port=4664
TargetHost=127.0.0.1
TargetPort=4664
LeftSSL=0
RightSSL=0

AllowList=127.0.0.1,192.168.1.*,10.23.4.*
LogDir=log

SSLTimeOut=300
SSLKeystore=KEYSTORE.jks
SSLKeystorePassword=broso.net
SSLTruststore=TRUSTSTORE.jks
SSLTruststorePassword=broso.net

ReplaceLineFrom_1=Host: 192.168.1.12:4664
ReplaceLineTo_1=Host: 127.0.0.1:4664

ReplaceLineFrom_2=http://127.0.0.1:4664
ReplaceLineTo_2=http://192.168.1.12:4664

ReplaceLineFrom_3=http://localhost:4664
ReplaceLineTo_3=http://192.168.1.12:4664

之後如果有機會的話,再來說明一下上述 SSL 相關參數的使用方法。

有興趣研究原始碼的人,可以留言跟我索取。

DBUtils 如何處理 oracle.sql.TIMESTAMP

接續上篇 java.sql.Date v.s. java.sql.Timestamp in Oracle 9i

Oracle JDBC 的 FAQ 中提到,在 Oracle 9i 中如果要取得時間部分的資料,最好也最標準的方法是將資料型態由 DATE 改為 TIMESTAMP。經過實際測試將資料型態改為 TIMESTAMP 之後,Oracle 9i JDBC Driver 並非如預期地將 TIMESTAMP 轉為 java.sql.Timestamp,而是轉型為 oracle.sql.TIMESTAMP,應用程式必須再呼叫一次 oracle.sql.TIMESTAMP.timestampValue(),才能正確轉回 java.sql.Timestamp,這對我原先已經撰寫完成的許多程式來說,必須一一修改,相當麻煩。

所幸原先程式係透過一個共用元件來使用 DBUtils,經過檢視 Jakarta Commons DBUtils 的原始碼後,問題出在 org.apache.commons.dbutils.BasicRowProcessor 的 toMap() 函式

1
2
3
4
5
6
7
8
9
10
11
12
13


    public Map toMap(ResultSet rs) throws SQLException {
        Map result = new CaseInsensitiveHashMap();
        ResultSetMetaData rsmd = rs.getMetaData();
        int cols = rsmd.getColumnCount();

        for (int i = 1; i <= cols; i++) {
            result.put(rsmd.getColumnName(i), rs.getObject(i));
        }

        return result;
    }

裡面使用 ResultSet.getObject() 來直接取得 JDBC Driver 回傳的型態,而 Oracle 9i JDBC Driver 遇到資料型態為 DATE,則回傳 java.sql.Date,資料型態為 TIMESTAMP,則回傳 oracle.sql.TIMESTAMP。真不知道是 Oracle 的 bug 還是故意這麼搞的,所以 DBUtils 得到的就絕對不會是 java.sql.Timestamp

思考之後,最好的方式應該是寫一個 Wrapper 去繼承 org.apache.commons.dbutils.BasicRowProcessor,然後只改寫 toMap()函式如下

1
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


public class BasicRowProcessorWrapper extends BasicRowProcessor {
    /**
     * 繼承自 org.apache.commons.dbutils.BasicRowProcessor
     * 由於 Oracle 9i JDBC Driver 遇到資料型態為 DATE 會轉為 java.sql.Date
     * 導致僅留下日期而遺失時間的部分,遇到資料型態為 TIMESTAMP 會轉為 oracle.sql.TIMESTAMP
     * 必須自行再轉為 java.sql.Timestamp,因此改寫 toMap() 函式
     * @param rs ResultSet
     * @return Map
     * @throws SQLException
     */
    public Map toMap(ResultSet rs) throws SQLException {
        Map result = new CaseInsensitiveHashMap();
        ResultSetMetaData rsmd = rs.getMetaData();
        int cols = rsmd.getColumnCount();

        for (int i = 1; i <= cols; i++) {
            Object obj = rs.getObject(i);
            if ("oracle.sql.TIMESTAMP".equals(obj.getClass().getName())) {
                result.put(rsmd.getColumnName(i), rs.getTimestamp(i));
            } else if (obj instanceof java.sql.Date) {
                result.put(rsmd.getColumnName(i), rs.getTimestamp(i));
            } else  {
                result.put(rsmd.getColumnName(i), obj);
            }
        }

        return result;
    }
}

遇到 JDBC Driver 型態為 oracle.sql.TIMESTAMP 或是 java.sql.Date,則強迫使用 getTimestamp() 函式來轉型為 java.sql.Timestamp,如此一來,即使未來 DBUtils 改版更新,也無須更動舊有程式

下載相關原始碼

java.sql.Date v.s. java.sql.Timestamp in Oracle 9i

我們都知道 JDBC 中 java.sql.Date 與 java.sql.Timestamp 的差異在於java.sql.Date 是不包含時間的部分

而最近在工作上碰到一個問題…
程式使用

Oracle 9i JDBC thin driver
Jakarta commons DBCP
Jakarta commons DBUtils

等元件進行資料庫的存取..
而使用的資料庫版本為 Oracle9i Enterprise Edition Release 9.2.0.7.0 – 64bit Production

如果撈取 Oracle 資料型態為 DATE 的欄位時,我希望撈出例如 2007/01/24 13:14: 20格式的資料(用 sqlplus 撈出來是有 時間 部分的)
但抓到的資料卻只有 2007/01/24 ,也就是說 時間 的部分不見了

查了很久的問題,加上股溝大神的協助,終於發現原因…
可以參考這個網址

http://www.oracle.com/technology/tech/java/sqlj_jdbc/htdocs/jdbc_faq.htm#08_01

What is going on with DATE and TIMESTAMP?

原來 Oracle 在 9.2 版以前,資料庫資料型態只有 DATE (含有日期與時間),
而 JDBC 9.2 版以前的 drvier 查完資料 mapping 回來會是 java.sql.Timestamp,所以查出來的資料會是包含日期與時間的值

但 Oracle 9.2 後,資料庫資料型態除了 DATE ,還新增了 TIMESTAMP 型態
如果 table schema 建立時使用 DATE 型態建立 (雖然是 DATE , 但是實際上日期時間也都會保留在欄位裡, 用 sqlplus 可以驗證)
但 9.2 版以後的 jdbc driver 查完資料 DATE 型態 mapping 回來會是 java.sql.Date, 而不是 java.sql.Timestamp
所以自然程式取出來的時間部分就不見了

除非 table schema 建立時,欄位型態就是 TIMESTAMP,那麼 jdbc driver 就會幫你轉回 java.sql.Timestamp
但這種情形太少了,原有的 table 我們大部分都是用 DATE 去建立欄位的…

在上述網址中提到四個解法中,

1. 建議我們建立 table 時就要使用 TIMESTAMP,這是最標準的作法
但這不可能,因為目前 table 已經建立了
因此以後如要建立新的 Table , 時間欄位型態請記得改用 TIMESTAMP.

2. 與 3 在如果搭配 DBCP 與 DBUtils 元件的情況下,我們也無法介入調整 (因為根本碰不到 JDBC ResultSet)

4. 方式最簡單,定義一個系統變數 -Doracle.jdbc.V8Compatibility=”true” 即可,看來最快也最簡單,但經我測試的結果,撈出來的資料值會是類似下面降子

2007/01/24 00:00: 00

也就是說,雖然時間的部分出現了,但是值還是 00:00:00 ,所以還是沒用

最後我直接把 JDBC Driver downgrade 到 8.1.7 版,雖然使用舊版,但 DATE 型態就會正確的轉回 java.sql.Timestamp

ps. 需注意的是用 8.1.7 的 jdbc driver 來連 Oracle 9i , Oracle 官方說法是必須需上一個 patch 後才不會有問題
雖然我沒上 patch 也是正常…@@

ROME: RSS/ATOM Java Utilities.

昨天用 RSS4j 替相簿產生了 RSS Feed 後,再仔細研究下去才發現 RSS4j 目前只支援 RSS 0.90/0.91/1.0,
並不支援最新的 RSS 2.0 與 Google 的ATOM 0.3/1.0,於是又找了另一個 RSS java library : ROME

ROME 目前支援的 RSS 版本相當齊全,列舉如下:

RSS 0.90, RSS 0.91 Netscape, RSS 0.91 Userland, RSS 0.92, RSS 0.93, RSS 0.94, RSS 1.0, RSS 2.0, Atom 0.3, and Atom 1.0

所以用 ROME 來製作各種格式的 RSS Feed 當然不是問題,甚至可以用來做各種格式間的轉換,功能相當強大。
於是我就利用 ROME 的 API ,稍微包裝一下後,弄了一個簡單使用 ROME 的類別 broso.rss.ROMERSSGenerator

包裝起來以後,用法比昨天的 RSS4j 就更簡單了
把昨天的範例程式用 ROMERSSGenerator 修改一下..

1
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
`

    String photo_root = this.getServletContext().getRealPath("/");
String rss1_file = photo_root + File.separator + "rss1.xml";
String rss2_file = photo_root + File.separator + "rss2.xml";
String atom03_file = photo_root + File.separator + "atom03.xml";
String atom10_file = photo_root + File.separator + "atom10.xml";

// 產生 RSS XML Document
SimpleDateFormat dateParser = new SimpleDateFormat("yyyy/MM/dd");

ROMERSSGenerator rssGenerator = new ROMERSSGenerator();
rssGenerator.setRSS1(rss1_file); // 設定寫出 RSS 1.0 格式
rssGenerator.setRSS2(rss2_file); // 設定寫出 RSS 2.0 格式
rssGenerator.setATOM03(atom03_file); // 設定寫出 ATOM 0.3 格式
rssGenerator.setATOM10(atom10_file); // 設定寫出 ATOM 1.0 格式

// 產生相簿這個 Channel
rssGenerator.setFeedTitle("我的相簿");
rssGenerator.setFeedLink("http://broso.twbbs.org/photo");
rssGenerator.setFeedDescription("隨想意誌 我的相簿");


for (int i=0;i<photo_album.size();i++) {
....
....
// 將每個相簿加入 RSS 中
String link = "http://broso.twbbs.org/photo/"+DirName;
rssGenerator.addEntry(Title,link,dateParser.parse( Date),"text/html");
}

// 產生 RSS XML
rssGenerator.writeFeed();

需注意的是 ROME 需使用 J2SE 1.4 與 JDOM 1.0

下載
ROMERSSGenerator,
ROME,
JDOM 1.0

相簿即日起提供 RSS 訂閱服務

最近想幫相簿製作 RSS Feed,研究了一下,找到一個 java 的 Open Source RSS4j
提供 RSS 製作與剖析,參考作者的 Sample Code,很快地就整合到相簿的管理介面裡了。以後每次 Publish 一個相簿,RSS 都會自動更新了。

簡單說明一下如何利用 RSS4j 來製作 RSS

1.先去下載 RSS4j 回來,解開後把 rss4j.jar 放到 WEB-INF/lib 下面。
2.再到 Apache Xerces 下載最新的 xerces2-j ,解開後將裡面的 jar 檔都一併放到 WEB-INF/lib 下面。
3.剩下的就是寫 code 整合到相簿的管理介面中了。

1
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
`

    String photo_root = this.getServletContext().getRealPath("/");
String rss_file = photo_root + File.separator + "rss.xml";

// 產生 RSS XML Document
RssDocument doc = new RssDocument();
doc.setVersion(RssDocument.VERSION_10);

// 產生相簿這個 Channel
RssChannel channel = new RssChannel();
channel.setChannelTitle("我的相簿");
channel.setChannelLink("http://broso.twbbs.org/photo");
channel.setChannelDescription("隨想意誌 我的相簿");
channel.setChannelUri("http://broso.twbbs.org/photo");
doc.addChannel(channel);


for (int i=0;i<photo_album.size();i++) {
....
....
// 將每個相簿加入 RSS 中
RssChannelItem item1 = new RssChannelItem();
item1.setItemTitle(Title);
item1.setItemLink("http://broso.twbbs.org/photo/"+DirName);
item1.setItemDescription( Date);
channel.addItem(item1);
}

// 產生 RSS XML
File file = new File(rss_file);
try {
RssGenerator.generateRss(doc, file);
System.out.println("RSS file written.");
} catch (RssGenerationException e) {
System.out.println(e.getMessage());
e.printStackTrace();
out.println(e.getMessage());
return;
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
out.println(e.getMessage());
return;
}

4.搞定!!

小心勿觸 BEA WLS 8.1 sp3 OOM 地雷

經過一段時間的壓力測試以及實驗證明,前端 AP 透過 Bea WebLogic 8.1 sp3 的 Connection Pool 取得 JDBC Connection,這種方式來存取 DB 時,會有 Memory Leak 的現象。這種情形發生在 javax.sql.DataSource.getConnection 之後,取得之 Connection 結束連線後,卻無法將佔用之記憶體全部釋放,於是時間一久,JVM Heap Memory 被吃光,便產生了 java.lang.OutOfMemoryError。

為了證明是 Bea 的問題,我特地改寫程式,透過 Jakarta Commons DBCP 元件來取得 JDBC Connection,以便與原先程式透過 Bea ConnPool 取得連線的方式作為對照組。

下面是使用 Quest JProbe Memory Debugger 分別取得的 DBCP 與 WLS ConnPool 的 Heap Memory 曲線圖:

測試個案為持續發送 Online 簡訊,經過一個小時後觀察 Heap Memory 曲線。
OnlineService 使用 WLS Connection Pool
OnlineService with WLS Connection Pool

可以看得出來 Heap memory 耗用量逐漸的上升

再來看看 OnlineService 使用 Jakarta DBCP 的情形
OnlineService with Jakarta DBCP

看得出來 Heap Memory 經過 JVM GC 皆可以正確的釋放出來

再來看看 BatchService 的情形,測試個案是兩個批次 Job 各同時處理 10 萬筆簡訊
BatchService 使用 WLS Connection Pool
BatchService with WLS Connection Pool

Heap 耗用量一下子就飆到 80MB,之後持續上升,直到 12x MB 後,JVM 做了一次 Full GC,耗用量下降至 50MB,但馬上又飆到 12x MB 後,OOM 就發生了,兩個 Job 都無法完成。

BatchService 換成使用 Jakarta DBCP 的情形又如何呢?
BatchService with Jakarta DBCP

跟 OnlineService 一樣平穩,至此我就確定 WLS 8.1 sp3 內部應該有 Memory Leak 的問題存在,於是求助 BEA。BEA Consultant 才給了一帖大補丸,要求上 Patch 後再觀察看看。

上了 Patch 之後,果然曲線就變得大不相同
OnlineService 使用 WLS Connection Pool + WLS8.1 sp3 patch
OnlineService with WLS Connection Pool + WLS 8.1 sp3 patch

BatchService 使用 WLS Connection Pool + WLS8.1 sp3 patch
BatchService with WLS Connection Pool + WLS 8.1 sp3 patch

BatchService 換上 Patch 後,兩個批次 Job 各同時處理 10 萬筆簡訊已可正確處理完畢沒問題。

所以如果使用 WLS 8.1 sp3 的人,請小心勿觸 OOM 的地雷