1、什么是优雅关机?简单来说就是向应用进程发送停止命令后,可以保证正在执行的业务操作不会受到影响,直到操作完成才会停止服务。应用程序收到停止命令后,会执行以下操作:1.停止接收新的访问请求2.正在处理的请求,等待请求被处理;对于其他内部正在执行的任务,比如定时任务、mq消费等,也是等待当前正在执行的任务执行完毕,不会再启动新的任务。3.当应用即将关闭时,根据需要发出信号,通知其他应用服务准备接管,保证服务的高可用。例如直接通过kill-9命令强行关闭应用进程,可能导致正在执行的任务数据丢失或混乱,也可能导致任务持有的全局资源得不到释放,比如当前任务持有redis锁,没有设置过期时间。当任务突然终止,没有主动释放锁时,其他进程会因为获取不到锁而无法处理业务。那么如何在不影响正在执行的业务的情况下安全关闭应用呢?2.程序实践在SpringBoot官方文档中,已经告诉开发者只需要实现一个特定的接口来监听项目成功启动和关闭时的事件即可。相关接口如下:CommandLineRunner接口:当应用启动成功但还未开始接受流量时,会回调该接口的实现类,也可以实现ApplicationRunner接口。工作方式类似于CommandLineRunner和DisposableBean接口:当应用程序即将销毁时,会回调该接口的实现类,也可以使用@PreDestroy注解。标记的方法也将被调用。基于这个流程,我们可以创建一个服务监控类,用于监控项目成功启动和关闭时的回调服务。示例代码如下:@ComponentpublicclassAppListenerimplementsCommandLineRunner,DisposableBean{@Overridepublicvoidrun(String...args)throwsException{System.out.println("应用启动成功,预加载相关数据");}@Overridepublicvoiddestroy()throwsException{System.out.println("应用程序当前关闭,清理相关数据");}}每个SpringApplication在启用时,都会向JVM注册一个关闭钩子,保证ApplicationContext在退出时通过这个钩子通知JVM,从而实现服务的正常关闭,关闭的所有方法下面介绍的服务就是基于这个原则实现的。2.1.方法一:通过Actuator的Endpoint机制关闭服务。使用该方法需要先添加spring-boot-starter-actuator监控服务依赖包,org.springframework.bootspring-boot-starter-actuator在默认配置中,shutdown端点是关闭的,需要在application.properties配置中启用:management.endpoint.shutdown.enabled=true虽然是Actuator端点,但支持通过JMX或HTTP远程访问.shutdown默认配置不支持Web访问HTTP,所以使用HTTP请求shutdown时的配置也需要开启:management.endpoints.web.exposure.include=shutdown最后在SpringBoot服务启动后,使用POST请求类型调用如下接口关闭服务!http://127.0.0.1:8080/actuator/shutdown2.2,方法二:使用ApplicationContext的close方法关闭服务如果不想添加spring-boot-starter-actuator监控服务依赖包关闭关闭服务,也可以使用ApplicationContext的close方法关闭服务,它会自动销毁bean对象,关闭服务。只需要在应用启动时获取ApplicationContext对象,然后在相关位置调用close方法关闭服务即可。示例代码如下:@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(Application.class,args);尝试{TimeUnit.SECONDS.sleep(10);}catch(InterruptedExceptione){e.printStackTrace();}//启动10秒后,context.close()会自动关闭;}}当然我们也可以自己写一个Controller,获取对应的ApplicationContext对象,通过api操作调用close方法关闭服务,示例代码如下:@RestControllerpublicclassShutdownControllerimplementsApplicationContextAware{privateApplicationContextcontext;@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{this.context=applicationContext;}/***关闭服务*/@GetMapping("/shutdown")publicvoidshutdownContext(){((ConfigurableApplicationContext)context).close();}}2.3、方法三:监听服务pid,通过kill方式关闭服务通过api方式关闭服务,在很多人看来是不安全的,因为接口一旦泄露,意味着用户可以请求这个界面随意关闭服务,它的影响是不言而喻的,所以很多人建议在服务器端,应该使用其他方式关闭服务,比如通过进程命令关闭。springboot启动时,将应用进程ID写入一个app.pid文件生成,路径可以指定,然后通过脚本命令关闭服务。启动示例代码如下:@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplicationapplication=newSpringApplication(Application.class);application.addListeners(newApplicationPidFileWriter("/home/app/project1/app.pid"));应用程序运行();}}通过以下命令,您可以安全地关闭服务。猫/home/app/project1/app.pid|xargskill这种方法也是Linux操作系统中常用的解决方法。不同的是实现方式可能不同,有的不需要写文件。获取应用程序进程ID的其他方法。如果使用kill-9关闭服务,服务的监听器将收不到任何消息,类似于直接杀掉应用进程。这种方法不可取!2.4.方法四:使用SpringApplication的exit方法关闭服务。通过调用SpringApplication.exit()方法,您还可以安全地退出程序,同时返回一个退出代码,该代码可以传递给所有上下文。最后调用System.exit()也可以将这个错误码传递给JVM。示例代码如下:@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(Application.class,args);尝试{TimeUnit.SECONDS.sleep(5);}catch(InterruptedExceptione){e.printStackTrace();}//5秒后,关闭服务exitApplication(context);}publicstaticvoidexitApplication(ConfigurableApplicationContextcontext){//获取退出码intexitCode=SpringApplication.exit(context,(ExitCodeGenerator)()->0);//传递给jvm的退出代码,安全退出程序System.exit(exitCode);}}3.其他监视器介绍3.1.如果ApplicationListener有一些服务,比如定时任务,我们想在SpringBoot中关闭数据源在关闭连接池之前,可以通过实现ApplicationListener接口监听bean对象的变化,在bean关闭之前执行相关的关闭任务对象被销毁。示例代码如下:@ComponentpublicclassJobTaskListenerimplementsApplicationListener{@OverridepublicvoidonApplicationEvent(ApplicationEventapplicationEvent){//springbean容器销毁前执行的事件,防止数据库连接池在任务终止前被销毁if(applicationEventinstanceofContextClosedEvent){System.out.println("关闭相关定时任务");}}}3.2、PreDestroy上面我们提到了DisposableBean接口的实现,可以监听应用关闭前的回调处理。其实在自定义方法注解上加上@PreDestroy也可以达到同样的效果。示例代码如下:@ComponentpublicclassAppDestroyConfig{@PreDestroypublicvoidPreDestroy(){System.out.println("Theapplicationisshuttingdown...");}}4.总结本文主要围绕如何安全关闭SpringBoot服务展开。我介绍了一些程序操作。如有疏漏,欢迎网友批评指正!