JNDI Datasource 使用指南

目錄

簡介

JNDI Datasource 設定在 JNDI-Resources-HOWTO 中有廣泛說明。然而,tomcat-user 的回饋顯示,個別設定的具體資訊可能相當棘手。

以下是一些已發布到 tomcat-user 的範例設定,適用於熱門資料庫和一些資料庫使用的一般提示。

您應該知道,由於這些注意事項是根據設定和/或發布到 tomcat-user 的回饋衍生而來的,因此您的情況可能有所不同 :-)。如果您有任何其他經過測試的設定,而您認為這些設定可能對廣大受眾有幫助,或者如果您認為我們可以改善本節的任何部分,請告訴我們。

請注意,由於 Tomcat 7.x 和 Tomcat 8.x 使用不同版本的 Apache Commons DBCP 函式庫,因此 JNDI 資源設定在兩者之間有些許不同。您很可能需要修改較舊的 JNDI 資源設定,以符合以下範例中的語法,才能讓它們在 Tomcat 10 中運作。有關詳細資訊,請參閱 Tomcat 移轉指南

此外,請注意,一般而言,JNDI 資料來源設定,以及特別是本教學課程,都假設您已閱讀並了解 ContextHost 設定參考,包括後者參考中關於自動應用程式部署的章節。

DriverManager、服務提供者機制和記憶體外洩

java.sql.DriverManager 支援 服務提供者 機制。此功能是所有可用的 JDBC 驅動程式,透過提供 META-INF/services/java.sql.Driver 檔案來宣告自己,都會自動被發現、載入和註冊,讓您在建立 JDBC 連線之前,不必明確載入資料庫驅動程式。但是,此實作在所有 Java 版本中,對於 servlet 容器環境來說,從根本上來說是有缺陷的。問題在於 java.sql.DriverManager 只會掃描一次驅動程式。

Apache Tomcat 附帶的 JRE 記憶體外洩防護監聽器 會在 Tomcat 啟動期間觸發驅動程式掃描來解決此問題。此功能預設啟用。這表示只會掃描共用類別載入器及其父項中可見的函式庫,以尋找資料庫驅動程式。這包括 $CATALINA_HOME/lib$CATALINA_BASE/lib、類別路徑和模組路徑中的驅動程式。封裝在 Web 應用程式 (在 WEB-INF/lib 中) 和共用類別載入器 (在設定的地方) 中的驅動程式將不可見,也不會自動載入。如果您考慮停用此功能,請注意,掃描會由第一個使用 JDBC 的 Web 應用程式觸發,導致此 Web 應用程式重新載入時發生失敗,以及依賴此功能的其他 Web 應用程式發生失敗。

因此,在 WEB-INF/lib 目錄中具有資料庫驅動程式的 Web 應用程式無法依賴服務提供者機制,應明確註冊驅動程式。

java.sql.DriverManager 中的驅動程式清單也是已知的記憶體外洩來源。Web 應用程式註冊的任何驅動程式都必須在 Web 應用程式停止時取消註冊。當 Web 應用程式停止時,Tomcat 會嘗試自動發現並取消註冊 Web 應用程式類別載入器載入的任何 JDBC 驅動程式。但是,預期應用程式會透過 ServletContextListener 自行執行此動作。

資料庫連線池 (DBCP 2) 設定

Apache Tomcat 中的預設資料庫連線池實作仰賴 Apache Commons 專案中的函式庫。下列函式庫會被使用

  • Commons DBCP 2
  • Commons Pool 2

這些函式庫位於 $CATALINA_HOME/lib/tomcat-dbcp.jar 中的單一 JAR 中。不過,只有連線池所需的類別會包含在內,且套件已重新命名以避免與應用程式產生衝突。

DBCP 2 提供對 JDBC 4.1 的支援。

安裝

請參閱 DBCP 2 文件 以取得組態參數的完整清單。

防止資料庫連線池外洩

資料庫連線池會建立並管理連線至資料庫的連線池。重複使用和再利用已存在的資料庫連線比開啟新連線更有效率。

連線池有一個問題。網頁應用程式必須明確關閉 ResultSet、Statement 和 Connection。網頁應用程式未關閉這些資源可能會導致它們無法再重複使用,也就是資料庫連線池「外洩」。最後可能會導致網頁應用程式資料庫連線失敗,因為沒有更多可用連線。

這個問題有一個解決方案。Apache Commons DBCP 2 可以設定為追蹤和復原這些被遺棄的資料庫連線。它不僅可以復原它們,還可以產生開啟這些資源但從未關閉它們的程式碼堆疊追蹤。

若要設定 DBCP 2 DataSource 以移除和重複使用被遺棄的資料庫連線,請將下列其中一個或兩個屬性新增至 DBCP 2 DataSource 的 Resource 組態

removeAbandonedOnBorrow=true
removeAbandonedOnMaintenance=true

這兩個屬性的預設值都是 false。請注意,除非將 timeBetweenEvictionRunsMillis 設定為正值以啟用池維護,否則 removeAbandonedOnMaintenance 不會產生任何作用。請參閱 DBCP 2 文件 以取得這些屬性的完整文件。

使用 removeAbandonedTimeout 屬性設定資料庫連線閒置多少秒後會被視為被遺棄。

removeAbandonedTimeout="60"

移除被遺棄連線的預設逾時時間為 300 秒。

如果要讓 DBCP 2 記錄遺棄資料庫連線資源的程式碼堆疊追蹤,可以將 logAbandoned 屬性設定為 true

logAbandoned="true"

預設值為 false

MySQL DBCP 2 範例

0. 簡介

已回報可使用的 MySQL 和 JDBC 驅動程式版本

  • MySQL 3.23.47、使用 InnoDB 的 MySQL 3.23.47、MySQL 3.23.58、MySQL 4.0.1alpha
  • Connector/J 3.0.11-stable(官方 JDBC 驅動程式)
  • mm.mysql 2.0.14(舊的第三方 JDBC 驅動程式)

在繼續之前,別忘了將 JDBC 驅動程式的 jar 複製到 $CATALINA_HOME/lib

1. MySQL 設定

請務必遵循這些說明,因為變更可能會造成問題。

建立新的測試使用者、新的資料庫和單一測試資料表。您的 MySQL 使用者必須指定密碼。如果您嘗試使用空白密碼進行連線,驅動程式會失敗。

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);
注意:測試完成後應移除上述使用者!

接著將一些測試資料插入 testdata 資料表中。

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>
2. Context 設定

透過將資源宣告新增到 Context,在 Tomcat 中設定 JNDI DataSource。

例如

<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 2 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://127.0.0.1:3306/javatest"/>

</Context>
3. web.xml 設定

現在為此測試應用程式建立 WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
  version="6.0">
  <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>
4. 測試程式碼

現在建立一個簡單的 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 標籤庫。您可以從 Apache Tomcat 標籤庫 - 標準標籤庫 專案取得,只要確定取得 1.1.x 或更新的版本即可。取得 JSTL 後,將 jstl.jarstandard.jar 複製到 Web 應用程式的 WEB-INF/lib 目錄中。

最後將您的 Web 應用程式部署到 $CATALINA_BASE/webapps,作為稱為 DBTest.war 的 war 檔或部署到稱為 DBTest 的子目錄中。

部署後,將瀏覽器指向 https://127.0.0.1:8080/DBTest/test.jsp,以檢視您辛勤工作的成果。

Oracle 8i、9i 和 10g

0. 簡介

Oracle 需要的變更與 MySQL 設定類似,除了常見的陷阱 :-)

較舊的 Oracle 版本的驅動程式可能會分發為 *.zip 檔案,而不是 *.jar 檔案。Tomcat 只會使用安裝在 $CATALINA_HOME/lib 中的 *.jar 檔案。因此 classes111.zipclasses12.zip 需要重新命名為 .jar 副檔名。由於 jar 檔是 zip 檔,因此不需要解壓縮和 jar 這些檔案,只要重新命名即可。

對於 Oracle 9i 之後,您應該使用 oracle.jdbc.OracleDriver 而不是 oracle.jdbc.driver.OracleDriver,因為 Oracle 已聲明 oracle.jdbc.driver.OracleDriver 已過時,且此驅動程式類別的支援將在下次主要版本中終止。

1. Context 設定

與上述的 mysql 設定類似,您需要在 Context 中定義資料來源。在此,我們使用 thin 驅動程式定義一個稱為 myoracle 的資料來源,以 scott 使用者、密碼 tiger 連線到稱為 mysid 的 sid。(注意:使用 thin 驅動程式時,此 sid 與 tnsname 不同)。使用的 schema 會是 scott 使用者的預設 schema。

使用 OCI 驅動程式時,只需要在 URL 字串中將 thin 變更為 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"/>
2. web.xml 設定

建立應用程式的 web.xml 檔案時,應確保遵守 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>
3. 程式碼範例

您可以使用與上述相同的範例應用程式(假設您已建立所需的資料庫執行個體、資料表等),並將資料來源程式碼替換為類似下列的程式碼

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

0. 簡介

PostgreSQL 的設定方式類似於 Oracle。

1. 所需檔案

將 Postgres JDBC jar 複製到 $CATALINA_HOME/lib。與 Oracle 一樣,jar 必須放置在此目錄中,才能讓 DBCP 2 的 Classloader 找到它們。無論您接下來採取哪個設定步驟,都必須執行此步驟。

2. 資源設定

您有兩種選擇:定義所有 Tomcat 應用程式共用的資料來源,或定義專門給一個應用程式的資料來源。

2a. 共享資源設定

如果您希望定義所有 Tomcat 應用程式共用的資料來源,或只是偏好在此檔案中定義資料來源,請使用此選項。

作者在此並未成功,但其他人有回報成功。在此處提供說明將會受到歡迎。

<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"/>
2b. 應用程式特定資源設定

如果您希望定義專門給您的應用程式使用的資料來源,不會讓其他 Tomcat 應用程式看到,請使用此選項。此方法對您的 Tomcat 安裝較不具侵入性。

為您的 Context 建立資源定義。Context 元素應類似於下列內容。

<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>
3. web.xml 設定
<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>
4. 存取資料來源

以程式方式存取資料來源時,請記得在您的 JNDI 查詢中加上 java:/comp/env,如下列程式碼片段所示。另外請注意,可以將「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!");
}

非 DBCP 解決方案

這些解決方案使用與資料庫的單一連線(不建議用於測試以外的任何用途!)或其他一些連線池技術。

使用 OCI 軟體用戶端程式介面的 Oracle 8i

簡介

儘管沒有嚴格說明如何使用 OCI 用戶端建立 JNDI DataSource,但這些注意事項可以與上述 Oracle 和 DBCP 2 的解決方案結合使用。

要使用 OCI 驅動程式,您應該安裝 Oracle 軟體用戶端。您應該從光碟安裝 Oracle8i(8.1.7) 軟體用戶端,並從 otn.oracle.com 下載適當的 JDBC/OCI 驅動程式 (Oracle8i 8.1.7.1 JDBC/OCI 驅動程式)。

classes12.zip 檔案重新命名為 classes12.jar 以供 Tomcat 使用後,將其複製到 $CATALINA_HOME/lib 中。您可能還必須從此檔案中移除 javax.sql.* 類別,具體取決於您使用的 Tomcat 和 JDK 版本。

整合所有內容

請確保您的 $PATHLD_LIBRARY_PATH (可能在 $ORAHOME\bin 中) 中有 ocijdbc8.dll.so,並確認可以使用 System.loadLibrary("ocijdbc8"); 簡單測試程式載入本機程式庫。

您接下來應該建立一個簡單的測試 servlet 或 JSP,其中包含這些關鍵行

DriverManager.registerDriver(new
oracle.jdbc.driver.OracleDriver());
conn =
DriverManager.getConnection("jdbc:oracle:oci8:@database","username","password");

其中資料庫的格式為 主機:埠:SID 現在,如果您嘗試存取測試 servlet/JSP 的 URL,而您取得的結果是 ServletException,其根本原因為 java.lang.UnsatisfiedLinkError:get_env_handle

首先,UnsatisfiedLinkError 表示您有

  • JDBC 類別檔案和 Oracle 軟體用戶端版本之間不匹配。這裡的提示訊息指出找不到所需的程式庫檔案。例如,您可能使用的是 Oracle 版本 8.1.6 的 classes12.zip 檔案,而 Oracle 軟體用戶端版本為 8.1.5。classesXXX.zip 檔案和 Oracle 軟體用戶端軟體版本必須相符。
  • $PATHLD_LIBRARY_PATH 問題。
  • 據報導,忽略您從 otn 下載的驅動程式,並使用目錄 $ORAHOME\jdbc\lib 中的 classes12.zip 檔案也可以正常運作。

接下來您可能會遇到錯誤 ORA-06401 NETCMN: 無效的驅動程式標示

Oracle 文件說明:「原因:登入 (連線) 字串包含無效的驅動程式標示。動作:更正字串並重新提交。」將資料庫連線字串 (格式為 主機:埠:SID) 變更為這個:(description=(address=(host=myhost)(protocol=tcp)(port=1521))(connect_data=(sid=orcl)))

編輯。嗯,我不認為如果您整理好您的 TNSNames,這真的有必要 - 但我不是 Oracle DBA :-)

常見問題

以下是使用資料庫的 Web 應用程式常見問題,以及如何解決這些問題的提示。

間歇性資料庫連線失敗

Tomcat 在 JVM 中執行。JVM 會定期執行垃圾回收 (GC) 來移除不再使用的 Java 物件。當 JVM 執行 GC 時,Tomcat 內的程式碼執行會凍結。如果為建立資料庫連線所設定的最大時間小於垃圾回收所花的時間,則可能會導致資料庫連線失敗。

若要收集垃圾回收花費時間的資料,請在啟動 Tomcat 時將 -verbose:gc 參數加入 CATALINA_OPTS 環境變數。當詳細 GC 已啟用時,$CATALINA_BASE/logs/catalina.out 日誌檔將包含每個垃圾回收的資料,包括花費時間。

當 JVM 調整正確時,99% 的時間 GC 將花費不到一秒。其餘時間只會花費幾秒。GC 花費超過 10 秒的情況很少見,甚至沒有。

請確定資料庫連線逾時已設定為 10-15 秒。對於 DBCP 2,請使用參數 maxWaitMillis 設定。

隨機連線已關閉例外

當一個要求從連線池取得資料庫連線並關閉它兩次時,可能會發生這些情況。使用連線池時,關閉連線只會將它傳回池中供其他要求重複使用,它並未關閉連線。而 Tomcat 使用多個執行緒來處理同時要求。以下是可能導致 Tomcat 中發生此錯誤的事件順序範例

  Request 1 running in Thread 1 gets a db connection.

  Request 1 closes the db connection.

  The JVM switches the running thread to Thread 2

  Request 2 running in Thread 2 gets a db connection
  (the same db connection just closed by Request 1).

  The JVM switches the running thread back to Thread 1

  Request 1 closes the db connection a second time in a finally block.

  The JVM switches the running thread back to Thread 2

  Request 2 Thread 2 tries to use the db connection but fails
  because Request 1 closed it.

以下是使用從連線池取得的資料庫連線的正確撰寫程式碼範例

  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;
    }
  }

Context 與 GlobalNamingResources

請注意,儘管上述說明將 JNDI 宣告置於 Context 元素中,但有時可能會將這些宣告置於伺服器設定檔的 GlobalNamingResources 區段中,而且有時這是可行的。置於 GlobalNamingResources 區段中的資源將在伺服器的 Context 中共用。

JNDI 資源命名和領域互動

為了讓 Realms 運作,realm 必須參照在 <GlobalNamingResources> 或 <Context> 區段中定義的資料來源,而不是使用 <ResourceLink> 重新命名的資料來源。