关闭tomcat时,报无法加载类的错误。
非法访问:此Web应用程序实例已停止。无法加载[io.netty.util.concurrent.DefaultPromise$1]java.lang.NoClassDefFoundError
导致无法加载加载的原因就是tomcat已经关闭了类加载器,但是部分线程还在运行。
简单理解就是tomcat关闭的线程和用户其他线程没有串行执行。
比如:
把springboot+netty项目发布到外置tomcat,netty的关闭就是异步提交到线程
public class StartEventListener implements ApplicationListener {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("通过事件关闭"); //先event 后DisposableBean
bossGroup.shutdownGracefully();
}
}
public class StartEventListener implements DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("通过destroy关闭");
bossGroup.shutdownGracefully();
}
}
关闭tomcat发现报错:
23-Jun-2021 14:24:20.687 严重 [main] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaks The web application [ROOT] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal@23e84203]) and a value of type [io.netty.util.internal.InternalThreadLocalMap] (value [io.netty.util.internal.InternalThreadLocalMap@19932c16]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
23-Jun-2021 14:24:20.689 信息 [main] org.apache.coyote.AbstractProtocol.stop 正在停止ProtocolHandler ["http-nio-8080"]
23-Jun-2021 14:24:20.691 信息 [main] org.apache.coyote.AbstractProtocol.stop 正在停止ProtocolHandler ["ajp-nio-8009"]
23-Jun-2021 14:24:20.692 信息 [main] org.apache.coyote.AbstractProtocol.destroy 正在摧毁协议处理器 ["http-nio-8080"]
23-Jun-2021 14:24:20.692 信息 [main] org.apache.coyote.AbstractProtocol.destroy 正在摧毁协议处理器 ["ajp-nio-8009"]
23-Jun-2021 14:24:22.691 信息 [nioEventLoopGroup-3-1] org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading 非法访问:此Web应用程序实例已停止。无法加载[io.netty.util.concurrent.GlobalEventExecutor$2]。为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪。
java.lang.IllegalStateException: 非法访问:此Web应用程序实例已停止。无法加载[io.netty.util.concurrent.GlobalEventExecutor$2]。为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪。
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1385)
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1373)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1226)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1188)
at io.netty.util.concurrent.GlobalEventExecutor.startThread(GlobalEventExecutor.java:220)
at io.netty.util.concurrent.GlobalEventExecutor.execute(GlobalEventExecutor.java:208)
at io.netty.util.concurrent.DefaultPromise.safeExecute(DefaultPromise.java:842)
at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:499)
at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:616)
at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:605)
at io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:96)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:1051)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
还有找不到类的错误(类实际存在)
Exception in thread "nioEventLoopGroup-3-1" java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy
at ch.qos.logback.classic.spi.LoggingEvent.(LoggingEvent.java:119)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:419)
at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383)
at ch.qos.logback.classic.Logger.log(Logger.java:765)
at io.netty.util.internal.logging.LocationAwareSlf4JLogger.log(LocationAwareSlf4JLogger.java:46)
at io.netty.util.internal.logging.LocationAwareSlf4JLogger.error(LocationAwareSlf4JLogger.java:249)
at io.netty.util.concurrent.DefaultPromise.safeExecute(DefaultPromise.java:844)
at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:499)
at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:616)
at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:605)
at io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:96)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:1051)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
//通常我们要实现 SpringBootServletInitializer
public class ServletInitializer extends SpringBootServletInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(ServletInitializer.class);
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringbootApplication.class);
}
}
//而SpringBootServletInitializer 启动过程会注册ServletContextListener实现类
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
if (rootApplicationContext != null) {
servletContext.addListener(new SpringBootContextLoaderListener
(rootApplicationContext, servletContext));
}
}
}
//ServletContextListener实现类SpringBootContextLoaderListener中 关闭容器
public void contextDestroyed(ServletContextEvent event) {
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
/*
通过以上分析可知,关闭tocmat会先发送ContextClosedEvent然后调用DisposableBean
我们在这两个位置来释放资源即可,注意要串行。
*/
原因:关闭tomcat时,netty是异步执行,导致tomcat关闭了类加载器,而netty仍然在执行(需要加载类)。
解决:将netty关闭串行话,保证netty先关闭然后tomcat继续往下走。
总结:异步线程关闭在外置tomcat中建议都做串行处理。
1、如果使用springboot内置tomcat,也是多线程为什么不出错,答:springboot已经做串行处理
2、针对本文的错误只需
将bossGroup.shutdownGracefully(); 修改为 bossGroup.shutdownGracefully().sync();串行执行。
springboot内置tomcat启动方式,进程是springboot,
所以关闭逻辑入口是Runtime.getRuntime().addShutdownHook
特点:启动入口是@SpringBootApplication注解的main类且SpringApplication.run启动
springboot外置tomcat方式,进程是tomcat
tomcat提供了ServletContextListener入口,springboot实现此接口进入spring容器关闭
特点:需要手动实现SpringBootServletInitializer
比如public class ServletInitializer extends SpringBootServletInitializer
springMVC的核心servlet是FrameworkServlet其实现了 Servlet 的 destroy 方法 ,
destroy内部调用applicationContext.close()进入容器关闭生命周期阶段
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
refresh((ApplicationContext) context);
}
/*
springboot采用外置tomcat启动时会调用:
application.setRegisterShutdownHook(false);//关闭自动释放
从而不走 Runtime.getRuntime().addShutdownHook方式,走servlet周期
springboot采用内置tomcat启动时:
registerShutdownHook默认是true,所以采用addShutdownHook方式
印证了1.5
*/
2、内外置tomcat,spirng关闭入口
//SpringBoot 启动时会在 refreshContext 操作也注册一个 ShotdownHook 来关闭Spring容器。
private void refreshContext(ConfigurableApplicationContext context) {
this.refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
}
}
通过application.setRegisterShutdownHook(false); registerShutdownHook设置false
我们自己实现
Runtime.getRuntime().addShutdownHook(thread);
/*
内置tomcat,spirng registerShutdownHook true,自己监听系统消息来处理关闭
外置tomcat,spring false 则由servlet来传播关闭事件 servlet#destroy()->spirng closed event
*/
总结:(内外置tomcat,谁发送关闭通知)
1)外置tomcat,我们实现SpringBootServletInitializer ,则registerShutdownHook会被设置false,那么容器关闭由ServletContextListener通知的(即tomcat注册了addShutdownHook)。
2)内置tomcat,不走SpringBootServletInitializer,默认true.则由spring直接通过addShutdownHook拿到通知。
其实不管内外置tomcat,我们遇到多线程时,按串行释放即可。
/这样就避免一堆【因异步线程调用了一些被spring或tomcat释放的资源】报错