如何以正确的方式关闭Spring Boot应用程序?


120

他们在Spring Boot Document中说:“每个SpringApplication都会向JVM注册一个关闭钩子,以确保退出时优雅地关闭ApplicationContext。”

当我单击ctrl+cshell命令时,可以正常关闭应用程序。如果我在生产计算机上运行该应用程序,则必须使用命令 java -jar ProApplicaton.jar。但是我无法关闭shell终端,否则它将关闭进程。

如果我运行像这样的命令nohup java -jar ProApplicaton.jar &,则无法ctrl+c正常关闭它。

在生产环境中启动和停止Spring Boot Application的正确方法是什么?


根据您的配置,将应用程序的PID写入文件。您可以向该PID发送终止信号。另请参阅本期中的评论。
M. Deinum 2014年

我应该使用哪个信号,我认为kill -9不是一个好主意,对吗?
克里斯(Chris)

5
这就是为什么我向您指出了该线索的原因kill -SIGTERM <PID>
M. Deinum 2014年

用pid杀死,否-9
Dapeng

1
杀死$(lsof -ti tcp:<port>)-如果您不想使用执行器并需要快速杀死
Alex Nolasco

Answers:


61

如果使用执行器模块,则可以通过JMXHTTP启用端点来关闭应用程序。

添加到application.properties

endpoints.shutdown.enabled = true

以下网址将可用:

/actuator/shutdown -允许正常关闭应用程序(默认情况下未启用)。

根据端点公开的方式,敏感参数可以用作安全提示。

例如,敏感端点在通过访问时将需要用户名/密码HTTP(如果未启用Web安全,则将其禁用)。

Spring引导文档


1
我不想包括执行器模块。但是我发现,在我的控制台日志中,Spring Boot在第一行中打印了PID。有什么方法可以让Spring Boot在其他文件中打印PID而不添加执行器模块(ApplicationPidListener)?
克里斯

2
请注意,这必须是此端点的http帖子。您可以启用它endpoints.shutdown.enabled =真
sparkyspider

54

这是另一个选项,不需要您更改代码或公开关闭端点。创建以下脚本,并使用它们来启动和停止您的应用程序。

start.sh

#!/bin/bash
java -jar myapp.jar & echo $! > ./pid.file &

启动您的应用并将进程ID保存在文件中

停止

#!/bin/bash
kill $(cat ./pid.file)

使用保存的进程ID停止您的应用

start_silent.sh

#!/bin/bash
nohup ./start.sh > foo.out 2> foo.err < /dev/null &

如果需要从远程计算机或CI管道使用ssh启动应用程序,请使用此脚本来启动应用程序。直接使用start.sh可以使外壳挂起。

之后例如。重新/部署您的应用程序,您可以使用以下方法重新启动它:

sshpass -p password ssh -oStrictHostKeyChecking=no userName@www.domain.com 'cd /home/user/pathToApp; ./stop.sh; ./start_silent.sh'

这应该是答案。我刚刚确认信号15关机告诉Spring正常关闭。
乍得

1
为什么不在start.sh内调用java-使用nohup执行jar,而不是在start.sh内调用java-执行在另一个外壳脚本中使用nohup的jar执行?
Anand Varkey Philips飞利浦

2
@AnandVarkeyPhilips唯一的原因是出于测试目的,我有时会从命令行调用start.sh,但是如果您始终需要,nohup则可以合并命令
Jens

@Jens,感谢您提供信息。你能告诉我你这样做吗:foo.out 2> foo.err </ dev / null&
Anand Varkey Philips Philips

1
@jens,谢谢,我已成功完成,并在下面发布了我的启动和停止脚本。(stackoverflow.com/questions/26547532/...
阿南德VARKEY飞利浦

52

对于@ Jean-Philippe Bond的回答,

这是一个maven快速示例,供maven用户使用spring-boot-starter-actuator配置HTTP端点以关闭Spring Boot Web应用程序,以便您可以复制和粘贴:

1.Maven pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.application.properties:

#No auth  protected 
endpoints.shutdown.sensitive=false

#Enable shutdown endpoint
endpoints.shutdown.enabled=true

此处列出所有端点:

3.发送发布方法以关闭应用程序:

curl -X POST localhost:port/shutdown

安全说明:

如果您需要关机方法auth保护,则可能还需要

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置详细信息


第二步之后,尝试进行部署时,遇到此错误消息:说明:org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter中的方法setAuthenticationConfiguration的参数0需要一个类型为org.springframework.security.config的bean。找不到“ .annotation.authentication.configuration.AuthenticationConfiguration”。行动:考虑在您的配置中定义类型为“ org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration”的bean。
罗伯托

2
请注意:如果我server.contextPath=/appName在application.properties中包含类似内容,那么现在关闭命令将变为:curl -X POST localhost:8080/appName/shutdown 希望它可以对某人有所帮助。由于这个错误,我不得不付出很多努力。
Naveen Kumar,

如果没有安全性,建议在Spring Boot 1.5.8中使用application.properties endpoints.shutdown.enabled=true management.security.enabled=false
Stefano Scarpanti,

如果你不想暴露终点,并使用PID文件停止并通过shell脚本开始,试试这个: stackoverflow.com/questions/26547532/...
阿南德VARKEY飞利浦

27

您可以使springboot应用程序将PID写入文件,并且可以使用pid文件来停止或重新启动或使用bash脚本获取状态。要将PID写入文件,请使用ApplicationPidFileWriter将侦听器注册到SpringApplication,如下所示:

SpringApplication application = new SpringApplication(Application.class);
application.addListeners(new ApplicationPidFileWriter("./bin/app.pid"));
application.run();

然后编写一个bash脚本来运行spring boot应用程序。参考

现在,您可以使用脚本启动,停止或重新启动。


通用和兼容詹金斯全面启动和停止shell脚本可以在这里找到(stackoverflow.com/questions/26547532/...
阿南德VARKEY飞利浦


14

所有答案似乎都缺少一个事实,即您可能需要在正常关机期间(例如,在企业应用程序中)以协调的方式完成某些工作。

@PreDestroy允许您在单个bean中执行关闭代码。更加复杂的东西看起来像这样:

@Component
public class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {
     @Autowired ... //various components and services

     @Override
     public void onApplicationEvent(ContextClosedEvent event) {
         service1.changeHeartBeatMessage(); // allows loadbalancers & clusters to prepare for the impending shutdown
         service2.deregisterQueueListeners();
         service3.finishProcessingTasksAtHand();
         service2.reportFailedTasks();
         service4.gracefullyShutdownNativeSystemProcessesThatMayHaveBeenLaunched(); 
         service1.eventLogGracefulShutdownComplete();
     }
}

这正是我所需要的。执行完应用程序后,按ctrl-c谢谢@Michal
Claudio Moscoso

8

我不公开任何终结点并启动(在后台运行nohup,并且没有通过nohup创建的文件),并使用shell脚本停止运行(优雅地使用KILL PID,如果应用在3分钟后仍在运行,则强制终止)。我只是创建可执行jar,然后使用PID文件编写器写入PID文件,然后将Jar和Pid存储在与应用程序名称相同的名称的文件夹中,并且shell脚本的名称也以相同的名称开头和结尾。我也通过jenkins管道调用这些停止脚本和启动脚本。到目前为止没有问题。完美适用于8个应用程序(非常通用的脚本,并且易于适用于任何应用程序)。

主班

@SpringBootApplication
public class MyApplication {

    public static final void main(String[] args) {
        SpringApplicationBuilder app = new SpringApplicationBuilder(MyApplication.class);
        app.build().addListeners(new ApplicationPidFileWriter());
        app.run();
    }
}

YML文件

spring.pid.fail-on-write-error: true
spring.pid.file: /server-path-with-folder-as-app-name-for-ID/appName/appName.pid

这是启动脚本(start-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
# JVM Parameters and Spring boot initialization parameters
JVM_PARAM="-Xms512m -Xmx1024m -Dspring.profiles.active=${ACTIVE_PROFILE} -Dcom.webmethods.jms.clientIDSharing=true"
# Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.sh}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT#start-}

PIDS=`ps aux |grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "No instances of $APP_NAME with profile:$ACTIVE_PROFILE is running..." 1>&2
else
  for PROCESS_ID in $PIDS; do
        echo "Please stop the process($PROCESS_ID) using the shell script: stop-$APP_NAME.sh"
  done
  exit 1
fi

# Preparing the java home path for execution
JAVA_EXEC='/usr/bin/java'
# Java Executable - Jar Path Obtained from latest file in directory
JAVA_APP=$(ls -t $BASE_PACKAGE/apps/$APP_NAME/$APP_NAME*.jar | head -n1)
# To execute the application.
FINAL_EXEC="$JAVA_EXEC $JVM_PARAM -jar $JAVA_APP"
# Making executable command using tilde symbol and running completely detached from terminal
`nohup $FINAL_EXEC  </dev/null >/dev/null 2>&1 &`
echo "$APP_NAME start script is  completed."

这是停止脚本(stop-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
#Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.*}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT:5}

# Script to stop the application
PID_PATH="$BASE_PACKAGE/config/$APP_NAME/$APP_NAME.pid"

if [ ! -f "$PID_PATH" ]; then
   echo "Process Id FilePath($PID_PATH) Not found"
else
    PROCESS_ID=`cat $PID_PATH`
    if [ ! -e /proc/$PROCESS_ID -a /proc/$PROCESS_ID/exe ]; then
        echo "$APP_NAME was not running with PROCESS_ID:$PROCESS_ID.";
    else
        kill $PROCESS_ID;
        echo "Gracefully stopping $APP_NAME with PROCESS_ID:$PROCESS_ID..."
        sleep 5s
    fi
fi
PIDS=`/bin/ps aux |/bin/grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | /bin/awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "All instances of $APP_NAME with profile:$ACTIVE_PROFILE has has been successfully stopped now..." 1>&2
else
  for PROCESS_ID in $PIDS; do
    counter=1
    until [ $counter -gt 150 ]
        do
            if ps -p $PROCESS_ID > /dev/null; then
                echo "Waiting for the process($PROCESS_ID) to finish on it's own for $(( 300 - $(( $counter*5)) ))seconds..."
                sleep 2s
                ((counter++))
            else
                echo "$APP_NAME with PROCESS_ID:$PROCESS_ID is stopped now.."
                exit 0;
            fi
    done
    echo "Forcefully Killing $APP_NAME with PROCESS_ID:$PROCESS_ID."
    kill -9 $PROCESS_ID
  done
fi

7

Spring Boot在尝试创建应用程序上下文时提供了几个应用程序侦听器,其中之一是ApplicationFailedEvent。我们可以用来了解应用程序上下文是否已初始化。

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.context.event.ApplicationFailedEvent; 
    import org.springframework.context.ApplicationListener;

    public class ApplicationErrorListener implements 
                    ApplicationListener<ApplicationFailedEvent> {

        private static final Logger LOGGER = 
        LoggerFactory.getLogger(ApplicationErrorListener.class);

        @Override
        public void onApplicationEvent(ApplicationFailedEvent event) {
           if (event.getException() != null) {
                LOGGER.info("!!!!!!Looks like something not working as 
                                expected so stoping application.!!!!!!");
                         event.getApplicationContext().close();
                  System.exit(-1);
           } 
        }
    }

将上述侦听器类添加到SpringApplication。

    new SpringApplicationBuilder(Application.class)
            .listeners(new ApplicationErrorListener())
            .run(args);  

我找到的最好答案!谢谢!
Vagif

[@ user3137438],与在内部销毁注释中记录日志有何不同?
Anand Varkey Philips飞利浦

6

从Spring Boot 2.3及更高版本开始,内置了正常关闭功能机制。

在Spring Boot 2.3之前,没有开箱即用的正常关机机制。一些春季启动启动器提供此功能:

  1. https://github.com/jihor/hiatus-spring-boot
  2. https://github.com/gesellix/graceful-shutdown-spring-boot
  3. https://github.com/corentin59/spring-boot-graceful-shutdown

我是nr的作者。1.该启动器名为“ Hiatus for Spring Boot”。它在负载均衡器级别上起作用,即仅将服务标记为OUT_OF_SERVICE,而不以任何方式干扰应用程序上下文。这样可以进行正常关机,这意味着,如果需要,可以在一段时间内停止使用该服务,然后使其恢复正常工作。缺点是它不会停止JVM,您必须这样做kill命令。当我在容器中运行所有内容时,这对我来说没什么大不了的,因为无论如何我都必须停止并卸下容器。

2和3号或多或少地基于安迪·威尔金森(Andy Wilkinson)的这篇文章。它们是单向工作的-一旦触发,它们最终将关闭上下文。


5

SpringApplication向JVM隐式注册一个关闭钩子,以确保ApplicationContext在退出时正常关闭。这还将调用所有带有注释的bean方法@PreDestroy。这意味着我们不必像在Spring核心应用程序中那样显式地在引导应用程序中使用a的 registerShutdownHook()方法ConfigurableApplicationContext

@SpringBootConfiguration
public class ExampleMain {
    @Bean
    MyBean myBean() {
        return new MyBean();
    }

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(ExampleMain.class, args);
        MyBean myBean = context.getBean(MyBean.class);
        myBean.doSomething();

        //no need to call context.registerShutdownHook();
    }

    private static class MyBean {

        @PostConstruct
        public void init() {
            System.out.println("init");
        }

        public void doSomething() {
            System.out.println("in doSomething()");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("destroy");
        }
    }
}

作为和的替代@PostConstruct@PreDestroy我在批注中使用initMethoddestroyMethod属性@Bean。因此,对于此示例:@Bean(initMethod="init", destroyMethod="destroy")
acker9'9

@PreDestroy几个开发人员可能不知道的一个陷阱是,仅针对具有Singleton范围的bean调用此类方法。开发人员必须管理Bean生命周期中其他范围的清理部分
ass

2

它们是关闭Spring应用程序的多种方法。一种是在上调用close()ApplicationContext

ApplicationContext ctx =
    SpringApplication.run(HelloWorldApplication.class, args);
// ...
ctx.close()

您的问题建议您通过执行来关闭您的应用程序Ctrl+C,这通常用于终止命令。在这种情况下...

使用endpoints.shutdown.enabled=true不是最好的食谱。这意味着您需要公开一个端点来终止您的应用程序。因此,根据您的用例和环境,您必须对其进行保护...

Ctrl+C应该适合您的情况。我认为您的问题是由&号引起的。

Spring Application Context可能已经向JVM运行时注册了一个关闭挂钩。请参阅ApplicationContext文档

我不知道Spring Boot是否像您所说的那样自动配置了这个钩子。我认为是。

在上Ctrl+C,您的外壳将INT信号发送到前台应用程序。这意味着“请中断您的执行”。应用程序可以捕获该信号并在其终止(由Spring注册的钩子)之前进行清理,或者干脆忽略它(错误)。

nohup是使用陷阱执行以下程序以忽略HUP信号的命令。当您挂断电话时,HUP用于终止程序(例如,关闭ssh连接)。此外,它重定向输出,以避免您的程序在消失的TTY上阻塞。nohup不忽略INT信号。所以它不能阻止Ctrl+C工作。

我认为您的问题是由&号引起的,而不是由nohup引起的。Ctrl+C向前台进程发送信号。“&”号使您的应用程序在后台运行。一种解决方案:做

kill -INT pid

使用kill -9kill -KILL不好,因为应用程序(在此为JVM)无法捕获它以正常终止。

另一个解决方案是使应用程序恢复到前台。然后Ctrl+C将工作。看一下Bash Job控制,更确切地说看fg


2

exit()在SpringApplication类中使用static 方法可以正常关闭Spring Boot应用程序。

public class SomeClass {
    @Autowire
    private ApplicationContext context

    public void close() {
        SpringApplication.exit(context);
    }
}

这对我有用。非常感谢。
Khachornchit Songsaen



0

如果您在Linux环境中,那么您要做的就是从/etc/init.d/内部创建指向.jar文件的符号链接。

sudo ln -s /path/to/your/myboot-app.jar /etc/init.d/myboot-app

然后,您可以像其他任何服务一样启动该应用程序

sudo /etc/init.d/myboot-app start

关闭应用程序

sudo /etc/init.d/myboot-app stop

这样,退出终端时应用程序不会终止。应用程序将使用stop命令正常关闭。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.