JNDI 數(shù)據(jù)源配置的相關(guān)內(nèi)容已經(jīng)在 JNDI 資源文檔中詳細(xì)介紹過。但從 Tomcat 用戶的反饋意見來看,有些配置的細(xì)節(jié)問題非常棘手。
針對常用的數(shù)據(jù)庫,我們已經(jīng)給 Tomcat 用戶提供了一些配置范例,以及關(guān)于數(shù)據(jù)庫使用的一些通用技巧。本章就將展示這些范例和技巧。
另外,雖然有些注意事項來自于用戶所提供的配置和反饋信息,但你可能也有不同的實踐。如果經(jīng)過試驗,你發(fā)現(xiàn)某些配置可能具有廣泛的助益作用,或者你覺得它們會使本章內(nèi)容更加完善,請務(wù)必不吝賜教。
請注意,對比 Tomcat 7.x 和 Tomcat 8.x,JNDI 資源配置多少有些不同,這是因為使用的 Apache Commons DBCP 庫的版本不同所致。所以,為了在 Tomcat 8 中使用,你最好修改老版本的 JNDI 資源配置,以便能夠匹配下文范例中的格式。詳情可參看Tomcat 遷移文檔。
另外還要提示的是,一般來說(特別是對于本教程而言),JNDI 數(shù)據(jù)源配置會假定你已經(jīng)理解了 Context 與 Host 的配置偏好,其中包括在后者配置偏好中的應(yīng)用自動部署的相關(guān)內(nèi)容。
java.sql.DriverManager
支持服務(wù)提供者機制。這項功能的實際作用在于:對于所有可用的 JDBC 驅(qū)動,只要它們聲明提供 META-INF/services/java.sql.Driver
文件,就會被自動發(fā)現(xiàn)、加載并注冊,從而減輕了我們在創(chuàng)建
JDBC 連接之前還需要顯式地加載數(shù)據(jù)庫驅(qū)動的負(fù)擔(dān)。但在 servlet 容器環(huán)境的所有 Java 版本中,卻根本沒法實現(xiàn)這種功能。問題在于 java.sql.DriverManager
只會掃描一次驅(qū)動。
Tomcat 自帶的阻止 JRE 內(nèi)存泄漏偵聽器可以在一定程度上解決這個問題,它會在 Tomcat 啟動時觸發(fā)驅(qū)動掃描。該偵聽器默認(rèn)是啟用的。只有可見于該偵聽器的庫(比如 $CATALINA_BASE/lib
中的庫)才能被數(shù)據(jù)庫驅(qū)動所掃描。如果你想禁用該功能,那么一定要記住:首先使用
JDBC 的 Web 應(yīng)用會觸發(fā)掃描,從而當(dāng)該應(yīng)用重新加載時會出錯;對于其他依賴該功能的 Web 應(yīng)用來說也會導(dǎo)致出錯。
所以,假如應(yīng)用的 WEB-INF/lib
目錄中存在數(shù)據(jù)庫驅(qū)動,那么這些應(yīng)用就不能依賴服務(wù)提供者機制,而應(yīng)該顯式地注冊驅(qū)動。
java.sql.DriverManager
中的驅(qū)動已經(jīng)被認(rèn)為是內(nèi)存泄露之源。當(dāng) Web 應(yīng)用停止運行時,它所注冊的任何驅(qū)動都必須重新注冊。當(dāng) Web 應(yīng)用停止運行時,Tomcat 會嘗試自動尋找并重新注冊任何由 Web 應(yīng)用類加載器所加載的 JDBC 驅(qū)動。但最好是由應(yīng)用通過 ServletContextListener
來實現(xiàn)這一點。
Apache Tomcat 的默認(rèn)數(shù)據(jù)庫連接池實現(xiàn)基于的是 Apache Commons 項目的庫,具體來說是這兩個庫:
這兩個庫都位于一個 JAR 文件中:$CATALINA_HOME/lib/tomcat-dbcp.jar
。但該文件只包括連接池所需要的類,包名也已經(jīng)改變了,以避免與應(yīng)用沖突。
DBCP 2.0 支持 JDBC 4.1。
可參閱 DBCP 文檔了解完整的配置參數(shù)。
數(shù)據(jù)庫連接池創(chuàng)建并管理著一些與數(shù)據(jù)庫的連接。與打開新的連接相比,回收或重用現(xiàn)有的數(shù)據(jù)庫連接要更為高效一些。
連接池化還存在一個問題。Web 應(yīng)用必須明確地關(guān)閉 ResultSet、Statement,以及 Connection。假如 Web 應(yīng)用無法關(guān)閉這些資源時,會導(dǎo)致這些資源再也無法被重用,從而造成了數(shù)據(jù)庫連接池“泄露”。如果再也沒有可用連接時,最終這將導(dǎo)致 Web 應(yīng)用數(shù)據(jù)庫連接失敗。
針對該問題,有一個解決辦法:通過配置 Apache Commons DBCP,記錄并恢復(fù)這些廢棄的數(shù)據(jù)庫連接。它不僅能恢復(fù)這些連接,而且還能針對打開這些連接而又永遠(yuǎn)不關(guān)閉它們的代碼生成堆棧跟蹤。
為了配置 DBCP 數(shù)據(jù)源來移除并回收廢棄的數(shù)據(jù)庫連接,將下列屬性(一個或全部)添加到你的 DBCP 數(shù)據(jù)源中的 Resource
配置中:
removeAbandonedOnBorrow=true
removeAbandonedOnMaintenance=true
以上屬性默認(rèn)都為 false
。注意,只有當(dāng) timeBetweenEvictionRunsMillis
為正值,從而啟用池維護(hù)時,removeAbandonedOnMaintenance
才能生效。關(guān)于這些屬性的詳情,可查看 DBCP 文檔 。
使用 removeAbandonedTimeout
屬性設(shè)置某個數(shù)據(jù)庫連接閑置的秒數(shù),超過此時段,即被認(rèn)為是廢棄連接。
removeAbandonedTimeout="60"
默認(rèn)的去除廢棄連接的超時為 300 秒。
將 logAbandoned
設(shè)為 true
,可以讓 DBCP 針對那些拋棄數(shù)據(jù)庫連接資源的代碼,記錄堆棧跟蹤信息。
logAbandoned="true"
默認(rèn)為 false
。
已報告的能夠正常運作的 MySQL 與 JDBC 驅(qū)動的版本號為:
在繼續(xù)下一步的操作之前,千萬不要忘了將 JDBC 驅(qū)動的 JAR 文件復(fù)制到 $CATALINA_HOME/lib
中。
一定要按照下面的說明去操作,否則會出現(xiàn)問題。
創(chuàng)建一個新的測試用戶、一個新的數(shù)據(jù)庫,以及一張新的測試表。必須為 MySQL 用戶指定一個密碼。如果密碼為空,那么在連接時,就會無法正常驅(qū)動。
mysql> GRANT ALL PRIVILEGES ON *.* TO javauser@localhost
-> IDENTIFIED BY 'javadude' WITH GRANT OPTION;
mysql> create database javatest;
mysql> use javatest;
mysql> create table testdata (
-> id int not null auto_increment primary key,
-> foo varchar(25),
-> bar int);
注意:一旦測試結(jié)束,就該把上例中的這個用戶刪除!
下面在 testdata
表中插入一些測試數(shù)據(jù):
mysql> insert into testdata values(null, 'hello', 12345);
Query OK, 1 row affected (0.00 sec)
mysql> select * from testdata;
+----+-------+-------+
| ID | FOO | BAR |
+----+-------+-------+
| 1 | hello | 12345 |
+----+-------+-------+
1 row in set (0.00 sec)
mysql>
在 Context 中添加資源聲明,以便在 Tomcat 中配置 JNDI 數(shù)據(jù)源。
范例如下:
<Context>
<!-- maxTotal: Maximum number of database connections in pool. Make sure you
configure your mysqld max_connections large enough to handle
all of your db connections. Set to -1 for no limit.
-->
<!-- maxIdle: Maximum number of idle database connections to retain in pool.
Set to -1 for no limit. See also the DBCP documentation on this
and the minEvictableIdleTimeMillis configuration parameter.
-->
<!-- maxWaitMillis: Maximum time to wait for a database connection to become available
in ms, in this example 10 seconds. An Exception is thrown if
this timeout is exceeded. Set to -1 to wait indefinitely.
-->
<!-- username and password: MySQL username and password for database connections -->
<!-- driverClassName: Class name for the old mm.mysql JDBC driver is
org.gjt.mm.mysql.Driver - we recommend using Connector/J though.
Class name for the official MySQL Connector/J driver is com.mysql.jdbc.Driver.
-->
<!-- url: The JDBC connection url for connecting to your MySQL database.
-->
<Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="javauser" password="javadude" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/javatest"/>
</Context>
為該測試應(yīng)用創(chuàng)建一個 WEB-INF/web.xml
文件:
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<description>MySQL Test App</description>
<resource-ref>
<description>DB Connection</description>
<res-ref-name>jdbc/TestDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</web-app>
創(chuàng)建一個簡單的 test.jsp
頁面,稍后將用到它。
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<sql:query var="rs" dataSource="jdbc/TestDB">
select id, foo, bar from testdata
</sql:query>
<html>
<head>
<title>DB Test</title>
</head>
<body>
<h2>Results</h2>
<c:forEach var="row" items="${rs.rows}">
Foo ${row.foo}<br/>
Bar ${row.bar}<br/>
</c:forEach>
</body>
</html>
JSP 頁面用到了 JSTL 的 SQL 和 Core taglibs。你可以從 Apache Tomcat Taglibs - Standard Tag Library 項目中獲取它,不過要注意應(yīng)該是
1.1.x 或之后的版本。下載 JSTL 后,將 jstl.jar
和 standard.jar
復(fù)制到 Web 應(yīng)用的 WEB-INF/lib
目錄中。
最后,將你的應(yīng)用部署到 $CATALINA_BASE/webapps
,可以采用兩種方式:或者將應(yīng)用以名叫 DBTest.war
的 WAR 文件形式部署;或者把應(yīng)用放入一個叫 DBTest
的子目錄中。
部署完畢后,就可以在瀏覽器輸入 http://localhost:8080/DBTest/test.jsp
,查看你的第一個勞動成果了。
Oracle 需要的配置和 MySQL 差不多,只不過也存在一些常見問題。
針對過去版本的 Oracle 的驅(qū)動可能以 .zip 格式(而不是 .jar 格式)進(jìn)行分發(fā)的。Tomcat 只使用 *.jar
文件,而且它們還必須安裝在 $CATALINA_HOME/lib
中。因此,classes111.zip
或 classes12.zip
這樣的文件后綴應(yīng)該改成 .jar
。因為 jar 文件本來就是一種 zip 文件,因此不需要將原 zip
文件解壓縮然后創(chuàng)建相應(yīng)的 jar 文件,只需改換后綴名即可。
對于 Oracle 9i 之后的版本,應(yīng)該使用 oracle.jdbc.OracleDriver
而不是 oracle.jdbc.driver.OracleDriver
,因為 Oracle 規(guī)定開始棄用 oracle.jdbc.driver.OracleDriver
,下一個重大版本將不再支持這一驅(qū)動類。
跟前文 MySql 的配置一樣,你也需要在 Context 中定義數(shù)據(jù)源。下面定義一個叫做 myoracle 的數(shù)據(jù)源,使用上文說的短驅(qū)動來連接(用戶名為 scott,密碼為 tiger)到名為 mysid 的SID(Oracle 系統(tǒng)ID,標(biāo)識一個數(shù)據(jù)庫的唯一標(biāo)示符)。 用戶 scott 使用的 Schema 就是默認(rèn)的 schema。
使用 OCI 驅(qū)動,只需在 URL 字符串中將 thin 變?yōu)?oci 即可。
<Resource name="jdbc/myoracle" auth="Container"
type="javax.sql.DataSource" driverClassName="oracle.jdbc.OracleDriver"
url="jdbc:oracle:thin:@127.0.0.1:1521:mysid"
username="scott" password="tiger" maxTotal="20" maxIdle="10"
maxWaitMillis="-1"/>
在創(chuàng)建 Web 應(yīng)用的 web.xml 文件時,一定要遵從 Web 應(yīng)用部署描述符文件中 DTD 所需要的元素順序。
<resource-ref>
<description>Oracle Datasource example</description>
<res-ref-name>jdbc/myoracle</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
可以使用上文所列的范例應(yīng)用(假如你創(chuàng)建了所需的 DB 實例和表,等等),將數(shù)據(jù)源代碼用下面的代碼替換:
Context initContext = new InitialContext();
Context envContext = (Context)initContext.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/myoracle");
Connection conn = ds.getConnection();
//etc.
PostgreSQL 配置與 Oracle 基本相似。
將 Postgres 的 JDBC jar 文件復(fù)制到 $CATALINA_HOME/lib
中。和 Oracle 配置一樣,jar 文件必須放在這個目錄中,DBCP 類加載器才能找到它們。不管接下來如何配置,這是首先必須要做的。
目前有兩種選擇:定義一個能夠被 Tomcat 所有應(yīng)用所共享的數(shù)據(jù)源,或者定義只能被單個應(yīng)用所使用的數(shù)據(jù)源。
如果想定義能夠被多個 Tomcat 應(yīng)用所共享的數(shù)據(jù)源,或者只想在文件中定義自己的數(shù)據(jù)源,則采用如下配置:
盡管有些用戶反饋說這樣可行,但本文檔作者卻沒有成功,希望有人能闡述清楚。
<Resource name="jdbc/postgres" auth="Container"
type="javax.sql.DataSource" driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://127.0.0.1:5432/mydb"
username="myuser" password="mypasswd" maxTotal="20" maxIdle="10" maxWaitMillis="-1"/>
如果希望專門為某一應(yīng)用定義數(shù)據(jù)源,其他 Tomcat 應(yīng)用無法使用,可以使用如下配置。這種方法對 Tomcat 安裝的損害性要小一些。
在你的應(yīng)用的 Context 中創(chuàng)建一個資源定義,如下所示:
<Context>
<Resource name="jdbc/postgres" auth="Container"
type="javax.sql.DataSource" driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://127.0.0.1:5432/mydb"
username="myuser" password="mypasswd" maxTotal="20" maxIdle="10"
maxWaitMillis="-1"/>
</Context>
<resource-ref>
<description>postgreSQL Datasource example</description>
<res-ref-name>jdbc/postgres</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
在利用程序訪問數(shù)據(jù)庫時,記住把 java:/comp/env
放在你的 JNDI lookup 方法參數(shù)的前部,如下面這段代碼所示。另外,可以用任何你想用的值來替換 jdbc/postgres
,不過記得也要用同樣的值來修改上面的資源定義文件。
InitialContext cxt = new InitialContext();
if ( cxt == null ) {
throw new Exception("Uh oh -- no context!");
}
DataSource ds = (DataSource) cxt.lookup( "java:/comp/env/jdbc/postgres" );
if ( ds == null ) {
throw new Exception("Data source not found!");
}
這些方案或者使用一個單獨的數(shù)據(jù)庫連接(建議僅作測試用?。?,或者使用其他一些池化技術(shù)。
雖然并不能嚴(yán)格地解決如何使用 OCI 客戶端來創(chuàng)建 JNDI 數(shù)據(jù)源的問題,但這些注意事項卻能和上文提到的 Oracle 與 DBCP 解決方案結(jié)合起來使用。
為了使用 OCI 驅(qū)動,應(yīng)該先安裝一個 Oracle 客戶。你應(yīng)該已經(jīng)通過光盤安裝好了 Oracle 8i(8.1.7)客戶端,并從 otn.oracle.com 下載了適用的 JDBC/OCI 驅(qū)動(Oracle8i 8.1.7.1 JDBC/OCI 驅(qū)動)。
將 classes12.zip
重命名為 classes12.jar
后,將其復(fù)制到 $CATALINA_HOME/lib
中。根據(jù) Tomcat 的版本以及你所使用的 JDK,你可能還必須該文件中的刪除 javax.sql.*
類。
確保在 $PATH
或 LD_LIBRARY_PATH
(可能在 $ORAHOME\bin
)目錄下存在 ocijdbc8.dll
或 .so
文件,另外還要確認(rèn)能否使用 System.loadLibrary("ocijdbc8");
這樣的簡單測試程序加載本地庫。
下面你應(yīng)該創(chuàng)建一個簡單測試用 servlet 或 jsp,其中應(yīng)該包含以下關(guān)鍵代碼:
DriverManager.registerDriver(new
oracle.jdbc.driver.OracleDriver());
conn =
DriverManager.getConnection("jdbc:oracle:oci8:@database","username","password");
目前數(shù)據(jù)庫是 host:port:SID
形式,如果你試圖訪問測試用servlet/jsp,那么你會得到一個 ServletException
異常,造成異常的根本原因在于 java.lang.UnsatisfiedLinkError:get_env_handle
。
分析一下,首先 UnsatisfiedLinkError
表明:
JDBC 類文件和 Oracle 客戶端版本不匹配。消息中透露出的意思是沒有找到需要的庫文件。比如,你可能使用 Oracle 8.1.6 的 class12.zip 文件,而 Oracle 客戶端版本則是 8.1.5。classeXXXs.zip 文件必須與 Oracle 客戶端文件版本相一致。
出現(xiàn)了一個 $PATH, LD_LIBRARY_PATH
問題。
$ORAHOME\jdbc\lib
目錄中的 class12.zip 文件,同樣能夠正常運作。接下來,你可能還會遇到另一個錯誤消息:ORA-06401 NETCMN: invalid driver designator
。
Oracle 文檔是這么說的:“異常原因:登錄(連接)字符串包含一個不合法的驅(qū)動標(biāo)識符。解決方法:修改字符串,重新提交。”所以,如下面這樣來修改數(shù)據(jù)庫(host:port:SID
)連接字符串:
(description=(address=(host=myhost)(protocol=tcp)(port=1521))(connect_data=(sid=orcl)))
下面是一些 Web 應(yīng)用在使用數(shù)據(jù)庫時經(jīng)常會遇到的問題,以及一些應(yīng)對技巧。
Tomcat 運行在 JVM 中。JVM 周期性地會執(zhí)行垃圾回收(GC),清除不再使用的 Java 對象。當(dāng) JVM 執(zhí)行 GC 時,Tomcat 中的代碼執(zhí)行就會終止。如果配置好的數(shù)據(jù)庫連接建立的最長時間小于垃圾回收的時間,數(shù)據(jù)庫連接就會失敗。
在啟動 Tomcat 時,將 -verbose:gc
參數(shù)添加到 CATALINA_OPTS
環(huán)境變量中,就能知道垃圾回收所占用的時間了。在啟用 verbose:gc
后, $CATALINA_BASE/logs/catalina.out
日志文件就能包含每次垃圾回收的數(shù)據(jù),其中也包括它所占用的時間。
正確調(diào)整 JVM 后,垃圾回收可以做到在 99% 的情況下占用時間不超過 1 秒。剩余的情況則只占用幾秒鐘的時間,只有極少數(shù)情況下 GC 會占用超過 10 秒鐘的時間。
保證讓數(shù)據(jù)庫連接超時設(shè)定在 10~15 秒。對于 DBCP,可以使用 maxWaitMillis
參數(shù)來設(shè)置。
當(dāng)某一請求從連接池中獲取了一個數(shù)據(jù)庫連接,然后關(guān)閉了它兩次時,往往會出現(xiàn)這樣的異常消息。使用連接池時,關(guān)閉連接,就會把它歸還給連接池,以便之后其他的請求能夠重用該連接,而并不會關(guān)閉連接。Tomcat 使用多個線程來處理并發(fā)請求。下面這個范例就演示了,在 Tomcat 中,一系列事件導(dǎo)致了這種錯誤。
運行在線程 1 中的請求 1 獲取了一個連接。
請求 1 關(guān)閉了數(shù)據(jù)庫連接。
JVM 將運行的線程切換為線程 2。
線程 2 中運行的請求 2 獲取了一個數(shù)據(jù)庫連接。
(同一個數(shù)據(jù)庫連接剛被請求 1 關(guān)閉)
JVM 又將運行的線程切換回為線程 1。
請求 1 第二次關(guān)閉了數(shù)據(jù)庫連接。
JVM 將運行的線程切換回線程 2。
請求 2 和線程 2 試圖使用數(shù)據(jù)庫連接,但卻失敗了。因為請求 1 已經(jīng)關(guān)閉了它。
Connection conn = null;
Statement stmt = null; // Or PreparedStatement if needed
ResultSet rs = null;
try {
conn = ... get connection from connection pool ...
stmt = conn.createStatement("select ...");
rs = stmt.executeQuery();
... iterate through the result set ...
rs.close();
rs = null;
stmt.close();
stmt = null;
conn.close(); // Return to connection pool
conn = null; // Make sure we don't close it twice
} catch (SQLException e) {
... deal with errors ...
} finally {
// Always make sure result sets and statements are closed,
// and the connection is returned to the pool
if (rs != null) {
try { rs.close(); } catch (SQLException e) { ; }
rs = null;
}
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { ; }
stmt = null;
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { ; }
conn = null;
}
}
注意,雖然在上面的說明中,把 JNDI 聲明放在一個 Context 元素里面,但還是有可能(而且有時更需要)把這些聲明放在服務(wù)器配置文件的 GlobalNamingResources 區(qū)域。被放置在 GlobalNamingResources 區(qū)域的資源將會被服務(wù)器的各個上下文所共享。
為了讓 Realm 能運作,realm 必須指向定義在 <GlobalNamingResources>
或 <Context>
區(qū)域中的數(shù)據(jù)源,而不是<ResourceLink>
重新命名的數(shù)據(jù)源。
更多建議: