基于kill信号优雅的关闭JAVA程序

本文介绍了在Linux环境下,如何通过捕捉USR2信号安全退出HttpServer程序,避免使用kill-9命令导致的数据丢失问题。

    linux下其他jar包

# java -jar program.jar &

   当要停止程序时很多人先会考虑使用 kill -9 $pid ,强制程序退出,这有可能造成程序处理进程被半路中断,造成写入数据不完整。为了能优雅的退出,考虑通过捕捉USR2信号安全退出,以HttpServer为例。

package com.uar.daemon;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import sun.misc.Signal;
import sun.misc.SignalHandler;

import com.sun.net.httpserver.HttpServer;
import com.uar.bean.ConfigSetting;

public class HttpServerTest implements SignalHandler {
	
	private static Logger logger = LogManager.getLogger(HttpServerTest.class);
	
	private HttpServer server;
	private ExecutorService httpThreadPool;
	
	@Override
	public void handle(Signal sn) {
		logger.info("Signal [" + sn.getName() + "] is received, stopServer soon...");
		stopServer();
		logger.info("Stop successfully.");
	}
	
	public static void main(String[] args){
		HttpServerTest main = new HttpServerTest();
		// 捕捉USR2信号
		Signal.handle(new Signal("USR2"), main);
		main.startServer();
	}
	
	public void startServer() {
		int port = 5555;
		String context = "/KillTest";
		int maxConnections = 50;
		try {
			InetSocketAddress addr = new InetSocketAddress(port);
			server = HttpServer.create(addr, maxConnections);
			
			server.createContext(context, new ServerHandler());
			httpThreadPool = Executors.newCachedThreadPool();
			server.setExecutor(httpThreadPool);
			server.start();
		} catch (IOException e) {
			logger.error(e);
		}
	}	
	
	/**
	 * 安全的关闭HttpServer监听服务
	 */
	private void stopServer() {
		server.stop(1);
		httpThreadPool.shutdown();
	}
}

 

server.stop()

stop

public abstract void stop(int delay)
stops this server by closing the listening socket and disallowing any new exchanges from being processed. The method will then block until all current exchange handlers have completed or else when approximately delay seconds have elapsed (whichever happens sooner). Then, all open TCP connections are closed, the background thread created by start() exits, and the method returns. Once stopped, a HttpServer cannot be re-used.
Parameters:
   delay - the maximum time in seconds to wait until exchanges have finished.
Throws:
   IllegalArgumentException - if delay is less than zero.

 

    

<think>我们正在讨论的是在SpringBoot中自定义ShutdownHook来实现优雅退出。用户的问题:如何在SpringBoot中自定义关闭钩子函数以优雅地退出程序?根据之前的引用[1][2][3],我们知道SpringBoot本身已经利用了Java的ShutdownHook机制来实现优雅关闭。但用户想要自定义,可能有额外的清理工作。思路:1.SpringBoot默认已经注册了一个ShutdownHook(通过SpringApplication的registerShutdownHook方法),用于关闭ApplicationContext。2.如果我们想要添加自定义的清理逻辑,有两种主要方式:a)利用Spring的生命周期回调(如@PreDestroy、DisposableBean、ContextClosedEvent事件监听等),这些会在Spring的上下文关闭过程中被触发。b)直接注册一个Java的ShutdownHook(通过Runtime.addShutdownHook),但要注意避免与SpringBoot自身的关闭机制冲突。然而,如果我们在SpringBoot中直接注册一个额外的ShutdownHook,需要小心:-多个ShutdownHook的执行顺序是不确定的(尽管通常按照注册顺序,但JVM不保证)。-如果我们在自定义的Hook中需要用到Spring的Bean(比如数据库连接池等),那么我们必须确保在Spring上下文关闭之前执行我们的逻辑?或者更常见的是,我们希望在Spring关闭过程中一起执行。因此,更推荐的方式是:利用Spring提供的扩展点,而不是直接使用Runtime.addShutdownHook。但根据用户的要求,他明确想要“自定义shutdownhook”,所以我们提供两种方案:方案1:通过Spring的事件监听机制(推荐)我们可以监听ContextClosedEvent事件,这样当Spring上下文关闭时,会触发我们的事件处理逻辑。示例代码:@ComponentpublicclassMyShutdownHook{@EventListenerpublicvoidonContextClosed(ContextClosedEventevent){//执行自定义清理逻辑}}方案2:直接注册一个JavaShutdownHook,但将其作为Spring的Bean,并在初始化时注册,同时确保该Hook在Spring上下文关闭之后执行?或者需要协调。但是注意:如果我们在自定义的ShutdownHook中调用了context.close(),那么SpringBoot自身注册的Hook可能会被触发两次(因为SpringBoot已经注册了一个Hook,它会调用context.close())。所以为了避免重复关闭,我们可能需要将自定义逻辑整合到Spring的关闭流程中。根据引用[3]中的代码示例,他们注册了一个ShutdownHook,并在其中调用context.close(),这样就会触发Spring的关闭流程。但是,SpringBoot应用本身已经注册了这样的Hook(通过SpringApplication),所以我们不应该重复注册同样的操作。所以,如果我们有额外的清理逻辑,应该将其放在Spring的关闭流程中(比如使用@PreDestroy或者监听ContextClosedEvent),而不是自己再注册一个调用context.close()的Hook。但是,如果用户确实想要使用Runtime.addShutdownHook来注册一个与Spring无关的清理逻辑(比如在Spring上下文之外创建的资源),那么可以这样:在main方法中,在启动SpringApplication之前或之后注册Hook,但要注意:-如果注册在SpringApplication.run之前,那么Hook的执行顺序在Spring的Hook之前(因为先注册的先执行?但JVM不保证,通常认为是按注册顺序执行,但文档指出不保证顺序)。-如果需要在Spring关闭之后执行,则无法保证,因为Spring的Hook也是Hook,多个Hook并发执行,但JVM会等待所有Hook结束。因此,如果自定义的Hook不依赖Spring的Bean,那么可以直接注册。如果依赖,那么必须在Spring上下文关闭前执行?但这样也不好,因为可能Bean已经被销毁了。所以,总结:推荐使用Spring的事件机制(ContextClosedEvent)来添加自定义关闭逻辑。另外,SpringBoot2.3及以上版本支持通过actuator端点(如/actuator/shutdown)实现优雅停机,或者通过配置server.shutdown=graceful(该配置会让Web服务器停止接受新请求,等待进行中的请求完成)。因此,针对用户问题,我们给出两种自定义关闭逻辑的方法:方法一(推荐):使用Spring生命周期回调步骤:1.创建一个SpringBean2.在Bean中通过@PreDestroy方法或实现DisposableBean接口或监听ContextClosedEvent事件示例(事件监听):@ComponentpublicclassCustomShutdownHandler{@EventListenerpublicvoidhandleContextClosed(ContextClosedEventevent){//执行清理逻辑System.out.println("执行自定义关闭逻辑...");}}方法二:在main方法中注册JavaShutdownHook(注意协调)如果使用此方法,确保自定义的Hook不会与Spring的Hook冲突,并且不要重复关闭Spring上下文。示例:publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(MyApplication.class,args);//注册自定义ShutdownHookRuntime.getRuntime().addShutdownHook(newThread(()->{//注意:这里Spring的Hook也会执行,所以不要在这里调用context.close(),否则可能重复关闭//只执行与Spring无关的清理System.out.println("自定义Hook执行...");//如果必须使用Spring的Bean,要确保该Hook在Spring的Hook之前执行?但无法保证顺序//因此不推荐在这里使用Spring的Bean}));}但是,如果我们在自定义的Hook中需要用到Spring的Bean,那么应该使用方法一(Spring的事件机制)。因此,回答的结构:1.介绍SpringBoot优雅关闭机制(基于JavaShutdownHook,已经默认注册)2.说明自定义关闭逻辑的两种方法,并比较推荐使用Spring的事件回调3.给出每种方法的代码示例4.注意事项:避免重复关闭,避免在Hook中调用已被销毁的Bean同时,根据要求,生成相关问题。注意:用户引用了[1][2][3],我们在回答中也要适当引用。最后,按照要求生成相关问题。由于是中文回答,全程使用中文。</think> ### 在 Spring Boot 中自定义 Shutdown Hook 实现优雅退出的方法 #### 一、Spring Boot 优雅关闭机制原理 Spring Boot 的优雅停机基于 Java 的 `Runtime.addShutdownHook()` 机制实现: 1. **默认注册钩子**:启动时自动注册 Shutdown Hook,在 JVM 收到关闭信号时触发 `ApplicationContext` 的关闭流程[^2]。 2. **关闭流程**: ```mermaid graph LR A[JVM关闭信号] --> B(执行Shutdown Hook) B --> C{是否Spring Boot应用?} C -->|是| D[关闭ApplicationContext] D --> E[停止Web服务器] D --> F[释放Spring Bean资源] D --> G[触发ContextClosedEvent事件] C -->|否| H[直接终止进程] ``` #### 二、自定义 Shutdown Hook 的两种方式 ##### 方法 1:扩展 Spring 生命周期回调(推荐) 在 Bean 中监听 Spring 的上下文关闭事件,与 Spring 生命周期无缝集成: ```java @Component public class CustomShutdownHandler { // 监听应用上下文关闭事件 @EventListener public void onContextClosed(ContextClosedEvent event) { // 自定义清理逻辑(如关闭专有连接池) closeCustomResources(); System.out.println("自定义资源已释放"); } private void closeCustomResources() { // 实现具体资源释放逻辑 } } ``` **优点**: - 天然整合 Spring 生命周期 - 避免多 Hook 执行顺序问题 - 可直接使用 `@Autowired` 注入 Spring Bean[^1] ##### 方法 2:直接注册 Java Shutdown Hook 在 main 方法中手动注册,需注意与 Spring 关闭流程的协调: ```java @SpringBootApplication public class MyApp { public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(MyApp.class, args); Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("自定义Hook启动"); // 1. 先执行自定义清理 cleanNonSpringResources(); // 2. 再关闭Spring上下文(避免重复关闭) if(ctx.isActive()) { ctx.close(); } })); } } ``` **注意事项**: 1. 调用 `ctx.close()` 前检查上下文状态,避免 `IllegalStateException` 2. 自定义逻辑应**先于** Spring 上下文关闭执行 3. 禁止在 Hook 中操作已销毁的 Spring Bean[^3] #### 三、关键实践建议 1. **资源释放顺序** 确保释放顺序:外部依赖连接 → 线程池 → 数据缓存 → Spring Bean ```java shutdownHookThread -> closeRedisPool() -> shutdownThreadPool() -> flushLocalCache() -> ctx.close() // Spring托管资源 ``` 2. **超时控制** 在 `application.properties` 中配置超时时间: ```properties # 等待进行中请求完成的超时时间 server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s ``` 3. **钩子冲突规避** 通过环境变量禁用 Spring 默认 Hook(必要时): ```java public static void main(String[] args) { SpringApplication app = new SpringApplication(MyApp.class); app.setRegisterShutdownHook(false); // 禁用默认钩子 ConfigurableApplicationContext ctx = app.run(args); // 注册统一管控的自定义钩子 registerMasterShutdownHook(ctx); } ``` #### 四、验证方式 1. 发送停机信号测试: ```bash # 正常关闭 kill -2 <PID> # SIGINT # 强制关闭(测试钩子失效场景) kill -9 <PID> ``` 2. 检查日志确保顺序: ``` [自定义Hook] 释放X资源 [Spring Hook] 关闭DataSource [Tomcat] 端口8080停止监听 ``` > 生产推荐:优先采用 Spring 生命周期扩展(方法1),仅在需要完全控制关闭流程时才使用方法2[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值