JNDI データソースの使い方

目次

はじめに

JNDI データソースの設定については、JNDI-Resources-HOWTO で詳しく説明されています。ただし、tomcat-user からのフィードバックによると、個々の設定の詳細はかなり難しい場合があります。

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

これらのメモは tomcat-user に投稿された設定やフィードバックに基づいているため、結果は異なる場合があることに注意してください。より幅広いユーザーに役立つと思われるテスト済みの設定がある場合、またはこのセクションを改善できると思われる場合は、お知らせください。

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

また、JNDI DataSource の設定全般、特にこのチュートリアルでは、後者のリファレンスの自動アプリケーションデプロイメントに関するセクションを含む、コンテキスト および ホスト の設定リファレンスを読んで理解していることを前提としています。

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 アプリケーションで障害が発生することに注意してください。

したがって、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 アプリケーションがこれらのリソースを閉じないと、再利用できなくなり、データベース接続プールが「リーク」する可能性があります。これにより、最終的に使用可能な接続がなくなった場合に、Web アプリケーションのデータベース接続が失敗する可能性があります。

この問題の解決策があります。Apache Commons DBCP 2 は、これらの放棄されたデータベース接続を追跡して回復するように設定できます。それらを回復できるだけでなく、これらのリソースを開いて閉じなかったコードのスタックトレースを生成することもできます。

放棄されたデータベース接続が削除されてリサイクルされるように DBCP 2 データソースを設定するには、DBCP 2 データソースの Resource 設定に次の属性のいずれかまたは両方を追加します

removeAbandonedOnBorrow=true
removeAbandonedOnMaintenance=true

これらの属性のデフォルトはどちらも false です。removeAbandonedOnMaintenance は、timeBetweenEvictionRunsMillis を正の値に設定してプールメンテナンスを有効にしない限り効果がないことに注意してください。これらの属性の完全なドキュメントについては、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. コンテキストの設定

リソースの宣言を コンテキスト に追加することで、Tomcat で JNDI データソースを設定します。

例えば

<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://localhost: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 タグライブラリとコアタグライブラリを使用しています。Apache Tomcat Taglibs - Standard Tag Library プロジェクトから入手できます。1.1.x 以降のリリースを入手してください。JSTL を入手したら、jstl.jarstandard.jar を Web アプリの WEB-INF/lib ディレクトリにコピーします。

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

デプロイしたら、ブラウザで http://localhost: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. コンテキスト設定

上記のMySQLの設定と同様に、コンテキストにデータソースを定義する必要があります。ここでは、thinドライバを使用して、ユーザーscott、パスワードtigerで、mysidというSIDに接続するmyoracleというデータソースを定義します。(注:thinドライバでは、この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のクラスローダーがjarファイルを見つけるためには、このディレクトリに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>

<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"は、上記のresource定義ファイルでも変更されていれば、任意の値に置き換えることができます。

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.*クラスを削除する必要がある場合もあります。

すべてをまとめる

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

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

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

ここで、databaseはhost:port:SIDの形式です。ここで、テストサーブレット/ JSPのURLにアクセスしようとすると、根本原因がjava.lang.UnsatisfiedLinkError:get_env_handleであるServletExceptionが発生します。

まず、UnsatisfiedLinkErrorは、

  • JDBCクラスファイルとOracleクライアントのバージョンが一致していないことを示しています。必要なライブラリファイルが見つからないというメッセージが、その手がかりとなります。たとえば、バージョン8.1.5のOracleクライアントで、Oracleバージョン8.1.6のclasses12.zipファイルを使用している可能性があります。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を起動する際に、CATALINA_OPTS環境変数に-verbose:gc引数を追加します。詳細GCが有効になっている場合、$CATALINA_BASE/logs/catalina.outログファイルには、かかった時間を含むすべてのガベージコレクションのデータが含まれます。

JVMが正しく調整されている場合、GCは99%の確率で1秒未満で完了します。残りは数秒しかかかりません。GCが10秒以上かかることは、めったにありません。

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

ランダムな接続が閉じられた例外

これは、1つのリクエストが接続プールから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;
    }
  }

コンテキストと GlobalNamingResources

上記の説明では、JNDI宣言をContext要素に配置していますが、これらの宣言をサーバー設定ファイルのGlobalNamingResourcesセクションに配置することも可能であり、場合によっては望ましい場合があります。GlobalNamingResourcesセクションに配置されたリソースは、サーバーのコンテキスト間で共有されます。

JNDI リソースの命名とレルムの相互作用

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