Oracle 的 ucp.jar 应该驻留在 Tomcat 的 lib 还是应用程序的 war 中?缺少 ResultSetMetaData。使用 Oracle 实现 Tomcat 应用程序的干净重新

Posted

技术标签:

【中文标题】Oracle 的 ucp.jar 应该驻留在 Tomcat 的 lib 还是应用程序的 war 中?缺少 ResultSetMetaData。使用 Oracle 实现 Tomcat 应用程序的干净重新部署?【英文标题】:Should Oracle's ucp.jar reside in Tomcat's lib or application's war? Missing ResultSetMetaData. Achieving clean redeploy of Tomcat app with Oracle? 【发布时间】:2021-08-22 01:04:21 【问题描述】:

假设现在是 2016 年。我正在构建一个非常简单的 Java EE 应用程序,其中包含用于 DI 的 Spring、jdbc 模板和 Web、用于持久性的 Oracle,并将其部署到 Tomcat。听起来很简单,不知道是否可以更简单。

有以下最新的稳定版本:

Tomcat 8.5 Oracle jdbc 驱动程序 v 12.x 和 Spring 4.3.x

Tomcat recommends 把 jdbc 驱动放到$CATALINA_BASE/lib,所以我遵循这个建议。 Oracle recommends 使用他们的 UCP 池和 oracle.com 上的教程还建议将 ucp.jar 与 ojdbc.jar 放在一起(到 Tomcat 的 lib 文件夹)。我使用 Spring 管理 UCP 池的生命周期并将其作为数据源传递给 JdbcTemplate

我在生产中使用单个专用服务器,为了让我的用户获得最佳体验,我使用了 Tomcat 的 Parallel deployment 功能。此功能没有什么特别之处,它允许在不停机的情况下部署新版本,并在没有活动会话时自动(并且优雅地)取消部署旧版本。

ResultSetMetaData 缺失问题

通过如此简单的设置部署新版本的应用程序后我可能遇到的意外问题:

INFO [http-nio-8080-exec-6] org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading Illegal access: this web application instance has been stopped already. Could not load [java.sql.ResultSetMetaData]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
java.lang.IllegalStateException: Illegal access: this web application instance has been stopped already. Could not load [java.sql.ResultSetMetaData]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
  at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1427)
  at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1415)
  at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1254)
  at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1215)
  at com.sun.proxy.$Proxy31.getMetaData(Unknown Source)
  at org.springframework.jdbc.core.SingleColumnRowMapper.mapRow(SingleColumnRowMapper.java:89)
  at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:93)
  at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:60)
  at org.springframework.jdbc.core.JdbcTemplate$1QueryStatementCallback.doInStatement(JdbcTemplate.java:465)
  at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:407)
  at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:477)
  at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:487)
  at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:497)
  at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:503)
  at example.App.rsMetadataTest(App.java:82)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:498)
  at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:204)
  at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
  at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:854)
  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:765)
  at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
  at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
  at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
  at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
  at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
  at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687)
  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)
  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
  at java.lang.Thread.run(Thread.java:748)

现在应用程序坏了。任何后续尝试拨打涉及ResultSetMetaData(即jdbcTemplate.queryForObject("select 'hello' from dual", String.class))的电话都将失败,并显示:

java.lang.NoClassDefFoundError: java/sql/ResultSetMetaData
  com.sun.proxy.$Proxy31.getMetaData(Unknown Source)
  org.springframework.jdbc.core.SingleColumnRowMapper.mapRow(SingleColumnRowMapper.java:89)
  org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:93)
  org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:60)
  org.springframework.jdbc.core.JdbcTemplate$1QueryStatementCallback.doInStatement(JdbcTemplate.java:465)
  org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:407)
  org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:477)
  org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:487)
  org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:497)
  org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:503)
  example.App.rsMetadataTest(App.java:82)
  sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  java.lang.reflect.Method.invoke(Method.java:498)
  org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:204)
  org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
  org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
  org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:854)
  org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:765)
  org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
  org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
  org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
  org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
  org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
  javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
  org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
  javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
  org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)

如何重现

很遗憾,我不了解异常的根本原因。 ResultSetMetaData是一个JDK类,怎么找不到呢?是卸载了吗?至少经过一些实验,我确切地知道重现它所需的最少步骤:

部署第一个版本的应用程序和初始化数据库池(即使用简单连接,但涉及ResulstSetMetaData,即jdbcTempalte.query())。 部署应用的第二个版本 等待第一个版本取消部署(尽可能优雅) 并拨打涉及ResultSetMetaData的电话。 轰隆隆!再次找不到ResultSetMetaData,应用已损坏。

此错误不依赖于 Tomcat 的并行部署功能。您可以拥有最新的 (9.x) Tomcat 和股票配置,2 个不同的 webapps 使用相同的 Oracle jdbc 驱动程序,按照我上面描述的顺序和相同的条件部署它并得到相同的错误。

另外我想补充一点,Tomcat的以下陈述是不正确的:

此 Web 应用程序实例已停止

我确切地知道第二个(刚刚部署的)应用程序被调用(不是卸载的应用程序),它是活动的并且无法停止。但它无法在途中到达ResultSetMetaData

在 docker-compose 的帮助下,我做了很多实验来隔离问题,看看有什么可以解决的。解决问题的一件事是将ucp.jar 放入.war,而不是放入Tomcat 的库中。 这就是标题中问题的原因:

Oracle 的 ucp.jar 应该驻留在 Tomcat 的 lib 中还是捆绑到应用程序的 war 中?

ucp.jar 本身不是一个 jdbc 驱动程序,它在全球服务提供商处注册。您是否将 HikariCP 放入 Tomcat 的库中?我不这么认为。将 ucp 捆绑到 webapp 可以解决 ResultSetMetaData 问题。 ucp.jar 放到Tomcat的lib还有其他原因吗?

破碎的反射

不幸的是,通过在 Maven 中设置 compileruntime 范围将 ucp.jar 移动到战争可能会导致另一个问题:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'oracleDataSource' defined in example.App: Initialization of bean failed; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
  ....
  ... 64 more
Caused by: java.lang.ArrayStoreException: sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy
  at sun.reflect.annotation.AnnotationParser.parseEnumArray(AnnotationParser.java:744)
  ...
  at java.lang.Class.getAnnotations(Class.java:3446)
  at org.springframework.transaction.annotation.AnnotationTransactionAttributeSource.determineTransactionAttribute(AnnotationTransactionAttributeSource.java:152)

在您的 Spring Java 配置中添加 @EnableTransactionManagement 或添加 <tx:annotation-driven/> 如果您更喜欢 XML,上下文不会立即启动。但我确实想在我的应用程序中使用 @Transactional 注释。所以我又被困住了。在这里至少我能够理解这个问题。 Spring 4 尝试读取 PoolDataSourceImpl 上的注解,以查看 bean 是否需要代理以支持基于注解的事务控制。 Class#getAnnotations() 无法读取PoolDataSourceImpl 类的注释,因为oracle.jdbc.logging.annotations.Feature 存在于两个jar(ucp 和jdbc)中。并且有 2 个类加载器具有不同的 Class<oracle.jdbc.logging.annotations.Feature> 实例。 PoolDataSourceImpl 的内省功能部分被奇怪的 ArrayStoreExceotion 破坏了! 出现此类错误是为了将两个 Oracle jar 保存在同一类路径中。


如果你在 2016 年(当时还没有更高版本的 Oracle 驱动)遇到上述问题,你会怎么做?我问这个,因为我从事的项目在过去有点卡住了。早些时候,升级 Oracle 驱动程序在生产中导致了意想不到的和不明显的问题,所以在最近的版本中,我们对更新 jdbc 驱动程序犹豫不决。但是由于最近项目从Tomcat 7升级到Tomcat 8,现在有面临ResultSetMetaData缺失问题的风险,应该解决。

我忘了说:您可能会遇到堆栈跟踪,抱怨在以前版本的 Tomcat:7.x 中缺少 ResultSetMetaData。但它并没有破坏可观察到的行为。与 Tomcat 9.x 和 8.x 不同,Tomcat 7.x 打印了一次异常,但不知何故设法执行了查询并成功处理了请求。 Tomcat 7.x 没有破坏应用程序。是不是说现代Tomcat有Tomcat 7.x没有的回归?


Tomcat 潜在内存泄漏警告

在重新部署时我也不喜欢日志中的以下几行:

WARNING [Catalina-utility-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [app##1] appears to have started a thread named [Timer-0] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.lang.Object.wait(Native Method)
 java.lang.Object.wait(Object.java:502)
 java.util.TimerThread.mainLoop(Timer.java:526)
 java.util.TimerThread.run(Timer.java:505)

WARNING [Catalina-utility-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [app##1] appears to have started a thread named [oracle.jdbc.driver.BlockSource.ThreadedCachingBlockSource.BlockReleaser] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.lang.Object.wait(Native Method)
 oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource$BlockReleaser.run(BlockSource.java:329)

WARNING [Catalina-utility-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [app##1] appears to have started a thread named [InterruptTimer] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.lang.Object.wait(Native Method)
 java.lang.Object.wait(Object.java:502)
 java.util.TimerThread.mainLoop(Timer.java:526)
 java.util.TimerThread.run(Timer.java:505)

有可能修复它们吗?根据我的测试,它们不是由 UCP 引起的,而是来自 ojdbc.jar。我在这里没有找到任何解决方案。无论是最新版本的 ojdbc8(或 ojdbc11),还是使用 Oracle 的 UniversalConnectionPoolManager(如建议的 here)的其他池或生命周期方法都没有帮助。 如果您将 ojdbc 替换为 postgres 数据库和驱动程序,您将不会看到类似的警告,并且您的日志将是干净的。


源代码

我没有在帖子中提供任何代码,它已经很长了,但我创建了一个repo,其中包含最小的应用程序示例和参数化的 docker-compose 测试。因此,您可以轻松地使用它并通过一个命令重现我提到的所有问题:docker-compose rm -fs && docker-compose up --build

【问题讨论】:

【参考方案1】:

我知道您提到 我使用 Spring 来管理 UCP 池的生命周期并将其作为数据源传递给 JdbcTemplate,但我的建议是将您的数据源创建为 tomcat 资源(即,在上下文级别):

<Resource
   name="tomcat/UCPPool"
   auth="Container"
<!-- Defines UCP or JDBC factory for connections -->
   factory="oracle.ucp.jdbc.PoolDataSourceImpl"
<!-- Defines type of the datasource instance -->
   type="oracle.ucp.jdbc.PoolDataSource"
   description="UCP Pool in Tomcat"
<!-- Defines the Connection Factory to get the physical connections -->
   connectionFactoryClassName="oracle.jdbc.pool.OracleDataSource”
   minPoolSize="2"
   maxPoolSize="60"
   initialPoolSize="15"
   autoCommit="false"
   user="scott"
   password="tiger"
<!-- FCF is auto-enabled in 12.2.  Use this property only if you are using Pre 12.2 UCP
   fastConnectionFailoverEnabled=”true”  -->
<!-- Database URL -->
url="jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(HOST=proddbclust
er-scan)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=proddb)))"
</Resource>

例子是从guide provided for Oracle描述Configure Tomcat for UCP时得到的。

并尝试通过 JNDI 获取对该数据源的引用:

@Bean
public DataSource dataSource() 
  final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
  dsLookup.setResourceRef(false);
  DataSource dataSource = dsLookup.getDataSource("tomcat/UCPPool");
  return dataSource;

您很可能面临类加载问题,将ucp.jarojdbc.jar 放在您的$CATALINA_BASE/lib 中并配置此JNDI 查找可以解决问题。

关于您的警告,请考虑阅读this related SO question,尤其是this answer:Oracle JDBC 驱动器中似乎存在错误,更新驱动程序版本 12.2 应该可以解决问题。

P.S.:好问题,有据可查!!

【讨论】:

感谢您的建议!我以前没有尝试过。但不幸的是,它并没有解决任何问题。对于 v12 驱动程序,您仍然会遇到ResultSetMetaData 问题,对于任何其他驱动程序,您都会看到来自 Tomcat 的有关潜在内存泄漏的警告。 非常感谢@Kirill 的反馈,很遗憾听到它没有解决问题。我会深入研究它。关于警告,请参阅更新的答案,它看起来像是 12.2 版中解决的 JDBC 驱动程序中的错误。请问,你可以试试吗? 已经在使用 12.2.0.1(存在 ResultSetMetaData 问题)。所有更高版本的驱动都没有这个问题,但是目前没有足够的资源进行测试,以确保jdbc驱动升级不会导致其他意外问题(这在应用历史上已经发生过多次) )。关于潜在内存泄漏的警告:我无法使用 Maven 中心提供的任何版本的 jdbc 驱动程序(从 12.x 到 21.x)来摆脱它们。你可以自己轻松尝试一下,docker-compose文件参数化很大。 我看到@Kirill。我会尝试测试提供的回购。同时,请您扩展 noclassdeffoundexception 错误堆栈跟踪吗? 添加了ResultSetMetaData相关的堆栈跟踪

以上是关于Oracle 的 ucp.jar 应该驻留在 Tomcat 的 lib 还是应用程序的 war 中?缺少 ResultSetMetaData。使用 Oracle 实现 Tomcat 应用程序的干净重新的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Windows 批处理执行驻留在 Oracle APEX 上的 SQL 脚本

JDBC 驱动程序 JAR 文件应该驻留在具有数据源的 Tomcat 部署的啥位置?

虚拟环境应该驻留在envs文件夹中吗?

是否可以调用驻留在 exe 中的非导出函数?

Cuda 内核代码驻留在英伟达 GPU 上的啥位置?

如何判断我的 NSManagedObject 是不是驻留在只读 NSPersistentStore 中?