JNDI 数据源使用指南
目录
简介
JNDI 数据源配置在 JNDI-Resources-HOWTO 中有详细介绍。然而,来自 tomcat-user
的反馈表明,针对单个配置的具体操作可能相当棘手。
因此,这里列出了一些已发布到 tomcat-user 的流行数据库的示例配置,以及一些数据库使用的一般性提示。
您应该注意,由于这些说明来源于发布到 tomcat-user
的配置和/或反馈,所以您的情况可能有所不同 (YMMV :-))。如果您有任何其他经过测试的配置,认为可能对更广泛的受众有用,或者您觉得我们可以以任何方式改进此部分,请告知我们。
请注意,JNDI 资源配置在 Tomcat 7.x 和 Tomcat 8.x 之间有所改变,因为它们使用了不同版本的 Apache Commons DBCP 库。您很可能需要修改旧的 JNDI 资源配置以匹配以下示例中的语法,才能使其在 Tomcat 11 中工作。详情请参阅Tomcat 迁移指南。
此外,请注意,JNDI 数据源配置(一般而言,特别是本教程)假设您已经阅读并理解了Context和Host配置参考,包括后者中关于自动应用程序部署的部分。
DriverManager、服务提供者机制和内存泄漏
java.sql.DriverManager
支持服务提供者机制。此功能意味着所有通过提供 META-INF/services/java.sql.Driver
文件来宣布自己的可用 JDBC 驱动程序都会被自动发现、加载和注册,从而免除了您在创建 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 数据源以删除和回收被遗弃的数据库连接,请在 DBCP 2 数据源的 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 数据源。
例如:
<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_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.jar
和 standard.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.zip
或 classes12.zip
需要重命名为 .jar
扩展名。由于 jar 文件就是 zip 文件,因此无需解压和重新打包这些文件——简单的重命名即可。
对于 Oracle 9i 及更高版本,您应该使用 oracle.jdbc.OracleDriver
而不是 oracle.jdbc.driver.OracleDriver
,因为 Oracle 已声明 oracle.jdbc.driver.OracleDriver
已弃用,并将在下一个主要版本中停止支持此驱动程序类。
1. Context 配置
与上面的 mysql 配置类似,您需要在 Context 中定义您的数据源。这里我们定义了一个名为 myoracle 的数据源,使用 thin 驱动程序以用户 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 的 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 数据源,但这些说明可以与上面的 Oracle 和 DBCP 2 解决方案结合使用。
要使用 OCI 驱动程序,您应该安装 Oracle 客户端。您应该从 CD 安装 Oracle8i(8.1.7) 客户端,并从 otn.oracle.com 下载合适的 JDBC/OCI 驱动程序 (Oracle8i 8.1.7.1 JDBC/OCI Driver)。
将 classes12.zip
文件重命名为 classes12.jar
以便 Tomcat 使用后,将其复制到 $CATALINA_HOME/lib
中。您可能还需要根据您使用的 Tomcat 和 JDK 版本,从该文件中删除 javax.sql.*
类。
整合所有内容
确保您的 $PATH
或 LD_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,您会得到一个 ServletException
,其根本原因是 java.lang.UnsatisfiedLinkError:get_env_handle
。
首先,UnsatisfiedLinkError
表明您存在
- 您的 JDBC 类文件与您的 Oracle 客户端版本不匹配。这里的关键信息是提示找不到所需库文件。例如,您可能正在使用 Oracle 8.1.6 版本的 classes12.zip 文件,而您的 Oracle 客户端是 8.1.5 版本。classesXXX.zip 文件和 Oracle 客户端软件版本必须匹配。
- 一个
$PATH
、LD_LIBRARY_PATH
问题。 - 据报告,忽略您从 otn 下载的驱动程序,而使用目录
$ORAHOME\jdbc\lib
中的 classes12.zip 文件也会奏效。
接下来您可能会遇到错误 ORA-06401 NETCMN: 无效的驱动程序指示符
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
环境变量中。当启用 verbose 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 部分的资源将在服务器的 Contexts 之间共享。
JNDI 资源命名和领域交互
为了使 Realms 生效,Realm 必须引用在 <GlobalNamingResources> 或 <Context> 部分中定义的数据源,而不是使用 <ResourceLink> 重命名的数据源。