1.1 现象

关闭tomcat时,报无法加载类的错误。

非法访问:此Web应用程序实例已停止。无法加载[io.netty.util.concurrent.DefaultPromise$1]java.lang.NoClassDefFoundError

1.2 线程关闭

导致无法加载加载的原因就是tomcat已经关闭了类加载器,但是部分线程还在运行。

简单理解就是tomcat关闭的线程和用户其他线程没有串行执行。

比如:

把springboot+netty项目发布到外置tomcat,netty的关闭就是异步提交到线程

public class StartEventListener implements ApplicationListener<ContextClosedEvent> {
    @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.<init>(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)

1.3 springboot外置tomcat分析

//通常我们要实现 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
我们在这两个位置来释放资源即可,注意要串行。

*/

1.4 异常原因

原因:关闭tomcat时,netty是异步执行,导致tomcat关闭了类加载器,而netty仍然在执行(需要加载类)。
解决:将netty关闭串行话,保证netty先关闭然后tomcat继续往下走。
     
总结:异步线程关闭在外置tomcat中建议都做串行处理。

1.5 正确关闭

1、如果使用springboot内置tomcat,也是多线程为什么不出错,答:springboot已经做串行处理
2、针对本文的错误只需 
  将bossGroup.shutdownGracefully(); 修改为 bossGroup.shutdownGracefully().sync();串行执行。
  

1.6 springboot内外置tomcat

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()进入容器关闭生命周期阶段
    

补充:

1、 SpringBoot 模式的优雅关闭

	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释放的资源】报错

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐