假设是 2016 年。我正在构建一个非常简单的 Java EE 应用程序,其中包含用于 DI 的 Spring、jdbc 模板和 Web、用于持久性的 Oracle,并将其部署到 Tomcat。听起来很简单,不知道它是否可以更微不足道。
有以下最新的稳定版本:
- 雄猫 8.5
- Oracle jdbc 驱动程序 v 12.x
- 和 Spring 4.3.x
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类,怎么找不到呢?是卸载了吗?至少经过一些实验,我确切地知道重现它所需的最少步骤:
- 部署第一个版本的应用程序和初始化数据库池(即使用简单的连接,但不涉及
ResulstSetMetaData
,即jdbcTempalte.query()
)。 - 部署应用程序的第二个版本
- 等待第一个版本取消部署(尽可能优雅)
- 并拨打涉及 的电话
ResultSetMetaData
。 - 繁荣!
ResultSetMetaData
再次找不到,应用程序已损坏。
此错误不依赖于 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
来进行战争可能会导致另一个问题:compile
runtime
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