JNDI 数据源操作指南

目录

简介

JNDI 数据源配置在 JNDI-Resources-HOWTO 中有详细介绍。但是,来自 tomcat-user 的反馈表明,针对特定配置的细节可能相当棘手。

以下是一些针对流行数据库的示例配置,这些配置已发布到 tomcat-user,以及一些关于数据库使用的通用技巧。

请注意,由于这些说明是从发布到 tomcat-user 的配置和/或反馈中得出的,因此可能存在差异 :-)。如果您有任何其他经过测试的配置,您认为可能对更广泛的受众有用,或者您认为我们可以以任何方式改进本节,请告知我们。

请注意,Tomcat 7.x 和 Tomcat 8.x 之间的 JNDI 资源配置有所不同,因为它们使用的是不同版本的 Apache Commons DBCP 库。 您很可能需要修改旧的 JNDI 资源配置以匹配以下示例中的语法,才能使它们在 Tomcat 10 中正常工作。有关详细信息,请参阅 Tomcat 迁移指南

此外,请注意,JNDI DataSource 配置(一般而言,本教程尤其如此)假设您已阅读并理解 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 文档

防止数据库连接池泄漏

数据库连接池创建和管理连接到数据库的池。回收和重用已存在的数据库连接比打开新的连接更有效。

连接池存在一个问题。Web 应用程序必须显式关闭 ResultSet、Statement 和 Connection。Web 应用程序未能关闭这些资源会导致它们永远无法再次使用,即数据库连接池“泄漏”。最终,如果不再有可用连接,这会导致您的 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>

    <!-- 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 和 Core 标签库。您可以从 Apache Tomcat 标签库 - 标准标签库 项目中获取它 - 只需确保您获取 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 版本的驱动程序可能以 *.zip 文件而不是 *.jar 文件的形式分发。Tomcat 仅使用安装在 $CATALINA_HOME/lib 中的 *.jar 文件。因此,classes111.zipclasses12.zip 需要重命名为 .jar 扩展名。由于 jar 文件是 zip 文件,因此无需解压缩和打包这些文件 - 只需简单地重命名即可。

对于 Oracle 9i 及更高版本,您应该使用 oracle.jdbc.OracleDriver 而不是 oracle.jdbc.driver.OracleDriver,因为 Oracle 已声明 oracle.jdbc.driver.OracleDriver 已过时,并且对该驱动程序类的支持将在下一个主要版本中停止。

1. 上下文配置

与上面的 mysql 配置类似,您需要在您的 Context 中定义您的数据源。这里我们使用 thin 驱动程序定义了一个名为 myoracle 的数据源,以用户 scott、密码 tiger 连接到名为 mysid 的 sid。(注意:使用 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. 代码示例

您可以使用与上面相同的示例应用程序(假设您创建了所需的数据库实例、表等),将数据源代码替换为类似以下内容

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 的类加载器找到它们。无论您接下来采取哪种配置步骤,都必须执行此操作。

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 数据源的问题,但这些注释可以与上面的 Oracle 和 DBCP 2 解决方案结合使用。

为了使用 OCI 驱动程序,您应该安装 Oracle 客户端。您应该从 cd 安装 Oracle8i(8.1.7) 客户端,并从 otn.oracle.com 下载合适的 JDBC/OCI 驱动程序(Oracle8i 8.1.7.1 JDBC/OCI 驱动程序)。

classes12.zip 文件重命名为 classes12.jar 后,将其复制到 $CATALINA_HOME/lib 中。您可能还需要根据您使用的 Tomcat 和 JDK 版本从该文件中删除 javax.sql.* 类。

综合示例

确保您的 $PATHLD_LIBRARY_PATH 中包含 ocijdbc8.dll.so 文件(可能位于 $ORAHOME\bin 目录下),并确认可以使用简单的测试程序(使用 System.loadLibrary("ocijdbc8");)加载本机库。

接下来,您应该创建一个简单的测试 servlet 或 JSP,其中包含以下 **关键行**

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

其中 database 的格式为 host:port:SID。现在,如果您尝试访问测试 servlet/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 定期执行垃圾回收 (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;
    }
  }

上下文与全局命名资源

请注意,虽然上述说明将 JNDI 声明放在 Context 元素中,但也可以将这些声明放在服务器配置文件的 GlobalNamingResources 部分。放在 GlobalNamingResources 部分的资源将在服务器的 Contexts 之间共享。

JNDI 资源命名和领域交互

为了使 Realms 正常工作,realm 必须引用在 <GlobalNamingResources> 或 <Context> 部分中定义的数据源,而不是使用 <ResourceLink> 重命名的数据库源。