讓不同的servlet在一個(gè)session內共享數據庫連接 引自: http://blog.ccidnet.com/blog-htm-do-showone-uid-49210-type-blog-itemid-147894.html
2007年05月28日
讓不同的servlet在一個(gè)session內共享數據庫連接
==== 問(wèn)題所在 ====
如果要編寫(xiě)一個(gè)購物車(chē),通常需要寫(xiě)很多個(gè)不同功能的servlet。例如用戶(hù)登錄、添加商品、查詢(xún)購物車(chē)、結帳等。
在這些 servlet 中都需要讀寫(xiě)數據庫。如果我們在每個(gè) servlet 中都進(jìn)行連接 -> 讀寫(xiě) -> 斷開(kāi)連接的操作,就會(huì )消耗大量的服務(wù)器資源,不僅程序響應速度減緩,而且會(huì )加重服務(wù)器和數據庫的負擔。
==== 把希望寄托于 HttpSession ====
如我們所學(xué),Servlet API 提供了一些方法和類(lèi)來(lái)專(zhuān)門(mén)處理短期的會(huì )話(huà)跟蹤。網(wǎng)站的每個(gè)用戶(hù)都和 javax.servlet.http.HttpSession 對象有關(guān),servlet使用這個(gè)對象來(lái)記錄和檢索每個(gè)用戶(hù)的信息。
幸運的是,我們可以在會(huì )話(huà)對象中存儲任意的 java 對象。存儲的方法大家都已經(jīng)很熟悉,就是使用 setAttribute()方法。代表數據庫連接的Connection也不例外。
這就為我們讓不同的servlet在一個(gè)session內共享鏈接帶來(lái)的希望。
==== 安全問(wèn)題 ====
那么,僅僅像下面這樣做就可以了么?
1、在Servlet1中,向session中設置一個(gè)屬性:
session.setAttribute("connection", connection);
2、在Servlet2中,取出這個(gè)屬性:
Connection connection = (Connection) session.getAttribute ("connection");
理論上,沒(méi)有問(wèn)題。在 Servlet1 中產(chǎn)生的 Connection 對象,到了 Servlet2 中可以繼續使用。
但 是如果 Servlet2 不小心改變了 connection 的引用,例如 connection = null; 那么,當它再次把這個(gè)connection放入session的屬性當中,其它的 servlet 就會(huì )得到一個(gè)指向 null 的 connection!
==== 解決之道 ====
把 connection 直接在 session 中傳來(lái)傳去,看來(lái)不怎么安全。
解決思路是,我們找一個(gè)專(zhuān)門(mén)的人來(lái)保管這個(gè) connection,在得到請求的時(shí)候,由這個(gè)人把 connection 的引用返回給調用者。這樣,即使調用者不小心把它得到的那份 connection 搞壞了,保管著(zhù)手里也總還有一個(gè)備份。
相應的,在 session 的屬性中,我們不再保存 connection 本身,而是把這個(gè)保管者存進(jìn)去。因為他能隨時(shí)給我們一個(gè)可用的 connection。
這個(gè)類(lèi)的具體寫(xiě)法是:
public class ConnectionHolder {
public ConnectionHolder(Connection con) {
// 保存連接
this.con = con;
try {
// 禁用自動(dòng)提交,以隔離不同session之間的操作。
con.setAutoCommit(false);
}
catch(SQLException e) {
// 錯誤處理代碼
}
}
public Connection getConnection() {
// 通過(guò)這個(gè)getter方法獲取連接
return con;
}
private Connection con = null; // 設置為私有變量,這很重要,以確保變量安全。
}
==== 使用方法 ====
每個(gè) servlet 在希望取得數據庫連接的時(shí)候,先看看 session 中是否有這個(gè)“保管者”(即上面的ConnectionHolder)。
如果有的話(huà),直接調用它的get方法,取得數據庫連接。
如果沒(méi)有的話(huà),說(shuō)明這個(gè)session還沒(méi)有連接過(guò)數據庫,那么當前類(lèi)就立刻創(chuàng )建一個(gè)數據庫連接,并把這個(gè)連接交給保管者,然后再把保管者放入 session 中,以便后續的 servlet 使用。
下面是一個(gè)實(shí)例:
1 protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
2
3 // 同步代碼取得數據庫連接
4 synchronized (session) {
5 // 看看這個(gè)持有者是否已經(jīng)在 session 中了
6 ConnectionHolder holder = (ConnectionHolder) session.getAttribute("servletapp.connection");
7
8 // 如果不在,就創(chuàng )建一個(gè)數據庫連接,并把它交給持有者。
9 if (holder == null) {
10 try {
11 holder = new ConnectionHolder(DriverManager.getConnection("Connection URL"));
12 session.setAttribute("servletapp.connection", holder);
13 }
14 catch (SQLException sqle) {
15 // 錯誤處理代碼
16 }
17 }
18
19 // 從容器取得實(shí)際連接
20 conn = holder.getConnection();
21 }
.... // 別忘了commit
}
這段代碼看起來(lái)有那么幾行。但實(shí)際上,在每個(gè)session中,只有第一次執行的servlet需要進(jìn)行數據庫連接操作,此后的servlet只會(huì )執行第4、6、20這三行。
==== 誰(shuí)來(lái)負責斷開(kāi)連接? ====
三個(gè)和尚沒(méi)水吃。
當 servlet 們不必再為創(chuàng )建數據庫連接費心的時(shí)候,也就沒(méi)有人愿意管關(guān)閉連接這檔子事了。事實(shí)上,更重要的是,他們沒(méi)法管。因為這個(gè)連接是放在 session 中的,而沒(méi)有誰(shuí)能準確的預測,一個(gè) session 會(huì )何時(shí)終止。
好在有一種叫做“監聽(tīng)器”(Listener)的東西可以專(zhuān)門(mén)管這件事。Listener有很多方法,其中的兩個(gè)方法是:
public void valueBound(HttpSessionBingEvent event);
public void valueUnbound(HttpSessionBingEvent event);
這兩個(gè)方法可以在一個(gè) session 被創(chuàng )建/失效的時(shí)候分別自動(dòng)執行。我們就把關(guān)閉連接的代碼放在第二個(gè)方法中,這樣,當一個(gè) session 失效的時(shí)候,數據庫連接就會(huì )自動(dòng)關(guān)閉。
要想讓一個(gè)類(lèi)成為L(cháng)istener,只需讓它實(shí)現 HttpSessionBindingListener 接口。我們的 connection 是由 ConnectionHolder 這個(gè)類(lèi)來(lái)保管的,因此最方便的辦法就是把它注冊成一個(gè)監聽(tīng)器。
具體方法是:
public void valueUnbound(HttpSessionBindingEvent event) {
// 當從Session刪除或當Session結束時(shí),關(guān)閉數據連接。
try {
if (con != null) {
con.rollback(); // 放棄所有未提交的數據
con.close();
}
}
catch (SQLException e) {
// 錯誤處理代碼
}
}
==== 完整示例 ====
下面是一個(gè)完整的 ConnectionHolder:
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionBindingEvent;
import java.sql.Connection;
import java.sql.SQLException;
public class ConnectionHolder implements HttpSessionBindingListener {
public ConnectionHolder(Connection con) {
// 保存連接
this.con = con;
try {
con.setAutoCommit(false);
}
catch(SQLException e) {
// 錯誤處理代碼
}
}
public Connection getConnection() {
return con;
}
public void valueBound(HttpSessionBindingEvent event) {
// 當增加Session時(shí),什么也不做
}
public void valueUnbound(HttpSessionBindingEvent event) {
// 當從Session刪除或當Session結束時(shí),關(guān)閉數據連接。
try {
if (con != null) {
con.rollback(); // 放棄所有未發(fā)送數據
con.close();
}
}
catch (SQLException e) {
// 錯誤處理代碼
}
}
private Connection con = null;
}
--------------
參考書(shū)籍:
1.《深入體驗Java Web開(kāi)發(fā)內幕——核心基礎》, 張孝祥, 電子工業(yè)出版社, 2006
2.《Java Servlet編程》, H.Jason, G.William, O‘REILLY 2002