介绍
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 配置一般性,特别是本教程, 假定已阅读并理解上下文和主机配置参考,包括后一个参考中有关自动应用程序部署的部分。
DriverManager 服务提供商机制和内存泄漏
java.sql.DriverManager 支持服务提供商机制。
此功能是,通过提供 META-INF/services/java.sql.Driver
文件来声明自己的所有可用 JDBC 驱动程序,
都会被自动发现、加载和注册,从而消除了在创建 JDBC 连接之前显式加载数据库驱动程序的需要。
但是,在 servlet 容器环境的所有 Java 版本中,该实现从根本上被破坏了。
问题是 java.sql.DriverManager 只会扫描一次驱动程序。
Apache Tomcat 附带的 JRE Memory Leak Prevention 侦听器,
通过在 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 DataSourc 的 Resource configuration (资源配置)。
-
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 驱动程序版本:
-
使用 InnoDB 的 MySQL 3.23.47、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 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://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.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 Taglibs - 标准标签库项目获取它 — 只需确保获得 1.1.x 或更高版本。 拥有 JSTL 后,将 jstl.jar 和 standard.jar 复制到 Web 应用程序的 WEB-INF/lib 目录。
最后,将 Web 应用程序部署到 $CATALINA_BASE/webapps 中, 作为名为 DBTest.war 的 warfile 或名为 DBTest 的子目录。
部署后,将浏览器指向 http://localhost:8080/DBTest/test.jsp 以查看辛勤工作成果。
Oracle 8i, 9i & 10g
0. 简介
Oracle 需要对 MySQL 配置进行最少的更改,除了通常的问题。
旧版 Oracle 的驱动程序可能作为 *.zip 文件而不是 *.jar 文件分发。 Tomcat 将仅使用安装在 $CATALINA_HOME/lib 中的 *.jar 文件。 因此,需要使用 .jar 扩展名重命名 classes111.zip 或 classes12.zip。 由于 jarfile 是 zip 文件,因此无需解压缩和 jar 这些文件 - 简单的重命名就足够了。
对于 Oracle 9i 及更高版本,应该使用 oracle.jdbc.OracleDriver 而不是 oracle.jdbc.driver.OracleDriver, 因为 Oracle 已声明 oracle.jdbc.driver.OracleDriver 已弃用, 并且对该驱动程序类的支持将在下一个主要版本中停止。
1. 上下文配置
与上面的 mysql 配置类似,需要在 Context 中定义 Datasource。 在这里,定义了一个名为 myoracle 的数据源,它使用 thin 驱动程序以用户 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. 代码示例
可以使用与上述相同的示例应用程序(假设创建了所需的数据库实例、表等),将数据源代码替换为类似。
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 驱动程序)。
将classes12.zip文件重命名为 classes12.jar for Tomcat 后, 将其复制到 $CATALINA_HOME/lib 中。可能还必须从此文件中删除 javax.sql.* 类, 具体取决于使用的 Tomcat 和 JDK 版本。
把它们放在一起
确保 $PATH 或 LD_LIBRARY_PATH 中包含 ocijdbc8.dll 或 .so(可能在 $ORAHOMEbin 中), 并确认本机库可以通过使用 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 下载的驱动程序并使用 $ORAHOMEjdbclib 目录中的 classes12.zip 文件也可以。
接下来,可能会遇到错误 ORA-06401 NETCMN:驱动程序指示符无效
Oracle 文档显示:“原因:登录 (连接) 字符串包含无效的驱动程序指示符。
操作:更正字符串并重新提交。将数据库连接字符串(
格式为 host:port:SID
)更改为以下字符串:(
description=(address=(host=myhost)(protocol=tcp)(port=1521))(connect_data=(sid=orcl)))
常见问题
以下是使用数据库的 Web 应用程序遇到的一些常见问题以及解决这些问题的提示。
间歇性数据库连接失败
Tomcat 在 JVM 中运行。JVM 会定期执行垃圾回收 (GC) 以删除不再使用的 java 对象。 当 JVM 执行 GC 时,Tomcat 中的代码执行会冻结。 如果为建立数据库连接配置的最长时间小于垃圾回收所花费的时间,则可能会出现数据库连接失败。
要收集有关垃圾回收所花费多长时间的数据, 请在启动 Tomcat 时将 -verbose:gc 参数添加到 CATALINA_OPTS 环境变量中。 启用 verbose gc 后,$CATALINA_BASE/logs/catalina.out 日志文件将包含每个垃圾回收的数据, 包括它所花费的时间。
当 JVM 被正确调优时,99% 的时间 GC 将花费不到 1 秒的时间。 其余的只需几秒钟。极少数情况下,GC 需要超过 10 秒。
确保 db connection timeout (数据库连接超时) 设置为 10-15 秒。 对于 DBCP 2,可以使用参数 maxWaitMillis 进行设置。
随机连接已关闭异常
当一个请求从连接池获取数据库连接并关闭它两次时,可能会发生这种情况。 使用连接池时,关闭连接只会将其返回到池中以供另一个请求重用,它不会关闭连接。 Tomcat 使用多个线程来处理并发请求。以下是可能导致 Tomcat 中出现此错误的事件序列示例:
-
在线程 1 中运行的请求 1 获得 db 连接。
-
请求 1 关闭数据库连接。
-
JVM 将正在运行的线程切换到线程 2
-
在线程 2 中运行的请求 2 获得 db 连接(请求 1 刚刚关闭的同一数据库连接)。
-
JVM 将正在运行的线程切换回线程 1
-
请求 1 在 finally 块中再次关闭数据库连接。
-
JVM 将正在运行的线程切换回线程 2
-
请求 2 线程 2 尝试使用 db 连接,但失败,因为请求 1 关闭了它。
下面是一个正确编写的代码示例,以使用从连接池获取的数据库连接:
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>。