首页 > 解决方案 > Oracle 的 ucp.jar 应该驻留在 Tomcat 的 lib 还是应用程序的 war 中?缺少 ResultSetMetaData。使用 Oracle 实现 Tomcat 应用程序的干净重新部署?

问题描述

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

有以下最新的稳定版本:

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

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

缺失的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(ie 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类,怎么找不到呢?是卸载了吗?至少经过一些实验,我确切地知道重现它所需的最少步骤:

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

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

此 Web 应用程序实例已停止

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

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

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

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

破碎的反射

不幸的是,通过在 Maven 中对其进行设置或范围ucp.jar来进行战争可能会导致另一个问题:compileruntime

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)

@EnableTransactionManagement在您添加Spring Java 配置或<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问题的风险,应该解决。

我忘了说:您可能会遇到堆栈跟踪抱怨在ResultSetMetaData以前版本的 Tomcat:7.x 中丢失。但它并没有破坏可观察到的行为。与 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(如此处建议那样)都没有帮助。如果您将 ojdbc 替换为 postgres 数据库和驱动程序,您将不会看到类似的警告,并且您的日志将是干净的。


源代码

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

标签: oracletomcatjdbcnoclassdeffounderrorucp

解决方案


我知道您提到我使用 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>

该示例来自描述时为 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 查找可以解决该问题。

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

PS:好问题,非常有据可查!


推荐阅读