JNDI 數(shù)據(jù)源配置的相關(guān)內(nèi)容已經(jīng)在 JNDI 資源文檔中詳細介紹過。但從 Tomcat 用戶的反饋意見來看,有些配置的細節(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ū)動的負擔。但在 servlet 容器環(huán)境的所有 Java 版本中,卻根本沒法實現(xiàn)這種功能。問題在于 java.sql.DriverManager 只會掃描一次驅(qū)動。
Tomcat 自帶的阻止 JRE 內(nèi)存泄漏偵聽器可以在一定程度上解決這個問題,它會在 Tomcat 啟動時觸發(fā)驅(qū)動掃描。該偵聽器默認是啟用的。只有可見于該偵聽器的庫(比如 $CATALINA_BASE/lib 中的庫)才能被數(shù)據(jù)庫驅(qū)動所掃描。如果你想禁用該功能,那么一定要記?。菏紫仁褂?JDBC 的 Web 應(yīng)用會觸發(fā)掃描,從而當該應(yīng)用重新加載時會出錯;對于其他依賴該功能的 Web 應(yīng)用來說也會導致出錯。
所以,假如應(yīng)用的 WEB-INF/lib 目錄中存在數(shù)據(jù)庫驅(qū)動,那么這些應(yīng)用就不能依賴服務(wù)提供者機制,而應(yīng)該顯式地注冊驅(qū)動。
java.sql.DriverManager 中的驅(qū)動已經(jīng)被認為是內(nèi)存泄露之源。當 Web 應(yīng)用停止運行時,它所注冊的任何驅(qū)動都必須重新注冊。當 Web 應(yīng)用停止運行時,Tomcat 會嘗試自動尋找并重新注冊任何由 Web 應(yīng)用類加載器所加載的 JDBC 驅(qū)動。但最好是由應(yīng)用通過 ServletContextListener 來實現(xiàn)這一點。
Apache Tomcat 的默認數(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)閉這些資源時,會導致這些資源再也無法被重用,從而造成了數(shù)據(jù)庫連接池“泄露”。如果再也沒有可用連接時,最終這將導致 Web 應(yīng)用數(shù)據(jù)庫連接失敗。
針對該問題,有一個解決辦法:通過配置 Apache Commons DBCP,記錄并恢復這些廢棄的數(shù)據(jù)庫連接。它不僅能恢復這些連接,而且還能針對打開這些連接而又永遠不關(guān)閉它們的代碼生成堆棧跟蹤。
為了配置 DBCP 數(shù)據(jù)源來移除并回收廢棄的數(shù)據(jù)庫連接,將下列屬性(一個或全部)添加到你的 DBCP 數(shù)據(jù)源中的 Resource 配置中:
removeAbandonedOnBorrow=true
removeAbandonedOnMaintenance=true
以上屬性默認都為 false。注意,只有當 timeBetweenEvictionRunsMillis 為正值,從而啟用池維護時,removeAbandonedOnMaintenance 才能生效。關(guān)于這些屬性的詳情,可查看 DBCP 文檔 。
使用 removeAbandonedTimeout 屬性設(shè)置某個數(shù)據(jù)庫連接閑置的秒數(shù),超過此時段,即被認為是廢棄連接。
removeAbandonedTimeout="60"
默認的去除廢棄連接的超時為 300 秒。
將 logAbandoned 設(shè)為 true,可以讓 DBCP 針對那些拋棄數(shù)據(jù)庫連接資源的代碼,記錄堆棧跟蹤信息。
logAbandoned="true"
默認為 false。
已報告的能夠正常運作的 MySQL 與 JDBC 驅(qū)動的版本號為:
在繼續(xù)下一步的操作之前,千萬不要忘了將 JDBC 驅(qū)動的 JAR 文件復制到 $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 復制到 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 格式)進行分發(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,標識一個數(shù)據(jù)庫的唯一標示符)。 用戶 scott 使用的 Schema 就是默認的 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 文件復制到 $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ù)。
雖然并不能嚴格地解決如何使用 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 后,將其復制到 $CATALINA_HOME/lib 中。根據(jù) Tomcat 的版本以及你所使用的 JDK,你可能還必須該文件中的刪除 javax.sql.* 類。
確保在 $PATH 或 LD_LIBRARY_PATH(可能在 $ORAHOME\bin)目錄下存在 ocijdbc8.dll 或 .so 文件,另外還要確認能否使用 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ū)動標識符。解決方法:修改字符串,重新提交?!彼?,如下面這樣來修改數(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 對象。當 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è)置。
當某一請求從連接池中獲取了一個數(shù)據(jù)庫連接,然后關(guān)閉了它兩次時,往往會出現(xiàn)這樣的異常消息。使用連接池時,關(guān)閉連接,就會把它歸還給連接池,以便之后其他的請求能夠重用該連接,而并不會關(guān)閉連接。Tomcat 使用多個線程來處理并發(fā)請求。下面這個范例就演示了,在 Tomcat 中,一系列事件導致了這種錯誤。
運行在線程 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ù)源。