JNDIデータソースのハウツー

目次

はじめに

JNDIデータソースの設定はJNDIリソースハウツーで詳細に説明されています。しかし、tomcat-userからのフィードバックでは、個々の設定の具体例がかなり難しいことが示されています。

ここでは、一般的なデータベースのためにtomcat-userに投稿されたいくつかの設定例と、DB使用に関する一般的なヒントを紹介します。

これらのメモはtomcat-userに投稿された設定やフィードバックから得られたものであるため、結果は異なる場合があります:-)。より多くの読者に役立つと思われる、他のテスト済みの設定がある場合、またはこのセクションを改善できると思われる場合は、お知らせください。

Tomcat 7.xとTomcat 8.xの間でJNDIリソースの設定が多少変更されていることに注意してください。これらは異なるバージョンのApache Commons DBCPライブラリを使用しているためです。 Tomcat 11で動作させるには、古いJNDIリソース設定を以下の例の構文に合わせて変更する必要があるでしょう。詳細はTomcat移行ガイドを参照してください。

また、JNDIデータソースの設定全般、特にこのチュートリアルは、ContextおよびHostの設定リファレンス、特に後者のリファレンスの自動アプリケーションデプロイに関するセクションを読み、理解していることを前提としています。

DriverManager、サービスプロバイダメカニズム、およびメモリリーク

java.sql.DriverManagerサービスプロバイダメカニズムをサポートしています。この機能により、META-INF/services/java.sql.Driverファイルを提供することで自身を宣言する利用可能なすべてのJDBCドライバが自動的に発見、ロード、登録され、JDBC接続を作成する前に明示的にデータベースドライバをロードする必要がなくなります。しかし、この実装はサーブレットコンテナ環境のすべてのJavaバージョンにおいて根本的に問題があります。問題は、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アプリケーションが停止するときに登録解除されなければなりません。Tomcatは、Webアプリケーションが停止するときに、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 ドキュメントを参照してください。

データベース接続プールリークの防止

データベース接続プールは、データベースへの接続のプールを作成および管理します。データベースへの既存の接続をリサイクルおよび再利用することは、新しい接続を開くよりも効率的です。

接続プールには1つの問題があります。Webアプリケーションは、ResultSet、Statement、およびConnectionを明示的に閉じなければなりません。これらのリソースを閉じないと、それらが再利用のために利用できなくなり、データベース接続プールの「リーク」が発生する可能性があります。これにより、利用可能な接続がなくなった場合、Webアプリケーションのデータベース接続が最終的に失敗する可能性があります。

この問題には解決策があります。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設定

TomcatでJNDIデータソースを設定するには、リソースの宣言をContextに追加します。

例:

<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://: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_1.xsd"
  version="6.1">
  <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 Taglibs - Standard Tag Libraryプロジェクトから入手できます。1.1.x以降のリリースであることを確認してください。JSTLを入手したら、jstl.jarstandard.jarをWebアプリケーションのWEB-INF/libディレクトリにコピーします。

最後に、Webアプリケーションを$CATALINA_BASE/webappsDBTest.warというwarファイルとして、またはDBTestというサブディレクトリにデプロイします。

デプロイ後、ブラウザでhttps://:8080/DBTest/test.jspにアクセスして、これまでの努力の成果を確認してください。

Oracle 8i、9i、および10g

0. はじめに

Oracleは、通常の注意点を除けば、MySQLの設定から最小限の変更で済みます :-)

古いOracleバージョンのドライバは、*.jarファイルではなく*.zipファイルとして配布されている場合があります。Tomcatは$CATALINA_HOME/libにインストールされた*.jarファイルのみを使用します。したがって、classes111.zipまたはclasses12.zip.jar拡張子にリネームする必要があります。jarファイルはzipファイルであるため、これらのファイルを解凍してjarにする必要はなく、単純なリネームで十分です。

Oracle 9i以降では、oracle.jdbc.driver.OracleDriverではなくoracle.jdbc.OracleDriverを使用してください。Oracleはoracle.jdbc.driver.OracleDriverが非推奨であり、このドライバクラスのサポートは次のメジャーリリースで終了すると述べています。

1. Context設定

上記のMySQL設定と同様に、Contextでデータソースを定義する必要があります。ここでは、myoracleというデータソースを定義し、シン・ドライバを使用してユーザーscott、パスワードtigerでmysidというSIDに接続します。(注: シン・ドライバでは、このSIDはtnsnameと同じではありません)。使用されるスキーマは、ユーザーscottのデフォルトスキーマになります。

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. コード例

上記と同じサンプルアプリケーションを使用できます(必要なDBインスタンスやテーブルなどを作成することを前提として)、データソースコードを次のように置き換えます。

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と同様に、DBCP 2のClassLoaderがそれらを見つけるには、jarがこのディレクトリにある必要があります。これは、次にどの設定手順を行うかに関わらず行う必要があります。

2. リソース設定

ここには2つの選択肢があります。すべてのTomcatアプリケーションで共有されるデータソースを定義するか、1つのアプリケーションに特化したデータソースを定義するかです。

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データソースの作成に厳密には対応していませんが、これらのメモは上記のOracleとDBCP 2のソリューションと組み合わせることができます。

OCIドライバを使用するには、Oracleクライアントがインストールされている必要があります。CDからOracle8i(8.1.7)クライアントをインストールし、otn.oracle.comから適切なJDBC/OCIドライバ(Oracle8i 8.1.7.1 JDBC/OCI Driver)をダウンロードする必要があります。

Tomcat用にclasses12.zipファイルをclasses12.jarにリネームした後、$CATALINA_HOME/libにコピーします。使用しているTomcatとJDKのバージョンによっては、このファイルからjavax.sql.*クラスを削除する必要があるかもしれません。

全てをまとめる

$PATHまたはLD_LIBRARY_PATH(おそらく$ORAHOME\bin内)にocijdbc8.dllまたは.soがあることを確認し、System.loadLibrary("ocijdbc8");を使用する簡単なテストプログラムでネイティブライブラリがロードできることも確認してください。

次に、これらの重要な行を含む簡単なテストサーブレットまたはJSPを作成する必要があります。

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

ここでデータベースはhost:port:SIDの形式です。次に、テストサーブレット/JSPのURLにアクセスしようとすると、java.lang.UnsatisfiedLinkError:get_env_handleをルート原因とするServletExceptionが表示されます。

まず、UnsatisfiedLinkErrorは次のことを示しています。

  • JDBCクラスファイルとOracleクライアントのバージョンとの間に不一致があります。ここで注目すべきは、必要なライブラリファイルが見つからないというメッセージです。例えば、Oracleバージョン8.1.6のclasses12.zipファイルをバージョン8.1.5のOracleクライアントで使用している可能性があります。classesXXX.zipファイルとOracleクライアントソフトウェアのバージョンは一致している必要があります。
  • $PATHLD_LIBRARY_PATHの問題。
  • otnからダウンロードしたドライバを無視し、ディレクトリ$ORAHOME\jdbc\libにあるclasses12.zipファイルを使用しても動作すると報告されています。

次に、ORA-06401 NETCMN: invalid driver designatorというエラーが発生する可能性があります。

Oracleドキュメントには次のように記載されています:「原因:ログイン(接続)文字列に無効なドライバ指定子が含まれています。対策:文字列を修正して再送信してください。」データベース接続文字列(host:port:SIDの形式)をこれに置き換えてください: (description=(address=(host=myhost)(protocol=tcp)(port=1521))(connect_data=(sid=orcl)))

編集者注: うーん、TNSNamesを整理すれば、これは本当に必要ないと思いますが、私はOracle DBAではありません :-)

よくある問題

ここでは、データベースを使用するWebアプリケーションで遭遇するいくつかの一般的な問題と、それらを解決するためのヒントを紹介します。

間欠的なデータベース接続障害

TomcatはJVM内で動作します。JVMは、使用されなくなったJavaオブジェクトを削除するために定期的にガベージコレクション(GC)を実行します。JVMがGCを実行すると、Tomcat内のコードの実行は一時停止します。データベース接続確立のために設定された最大時間がガベージコレクションにかかった時間よりも短い場合、データベース接続障害が発生する可能性があります。

ガベージコレクションにどれくらいの時間がかかっているかデータを収集するには、Tomcat起動時に-verbose:gc引数をCATALINA_OPTS環境変数に追加します。verbose gcが有効になっている場合、$CATALINA_BASE/logs/catalina.outログファイルには、各ガベージコレクションのデータ(かかった時間を含む)が含まれます。

JVMが正しくチューニングされていれば、GCは99%の確率で1秒未満で完了します。残りは数秒しかかかりません。GCが10秒以上かかることは稀です。

DB接続のタイムアウトが10〜15秒に設定されていることを確認してください。DBCP 2では、maxWaitMillisパラメータを使用してこれを設定します。

ランダムな接続クローズ例外

これらは、あるリクエストが接続プールからDB接続を取得し、それを2回閉じた場合に発生します。接続プールを使用している場合、接続を閉じてもそれは別のリクエストによる再利用のためにプールに戻されるだけで、接続自体は閉じられません。そして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リソース命名とRealmの相互作用

Realmを機能させるには、Realmは<ResourceLink>を使用してリネームされたデータソースではなく、<GlobalNamingResources>または<Context>セクションで定義されたデータソースを参照する必要があります。