首页 文章

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

提问于
浏览
83

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

当我在shell命令上单击 ctrl+c 时,可以正常关闭应用程序 . 如果我在 生产环境 机器中运行应用程序,我必须使用命令 java -jar ProApplicaton.jar . 但是我无法关闭shell终端,否则会关闭进程 .

如果我运行像 nohup java -jar ProApplicaton.jar & 这样的命令,我不能使用 ctrl+c 正常关闭它 .

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

12 回答

  • 0

    如果您正在使用 Actuator 模块,则可以通过 JMXHTTP 关闭应用程序(如果 endpoints 已启用)(将 endpoints.shutdown.enabled=true 添加到 application.properties 文件中) .

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

    根据 endpoints 的暴露方式,敏感参数可用作安全提示 . 例如,敏感 endpoints 在通过 HTTP 访问时需要用户名/密码(如果未启用Web安全性,则只需禁用) .

    来自Spring boot documentation

  • 30

    关于@ Jean-Philippe Bond的回答,

    这是一个maven快速示例,供maven用户配置HTTP endpoints ,使用spring-boot-starter-actuator关闭 spring 启动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
    

    列出所有 endpoints here

    3.发送一个post方法来关闭app:

    curl -X POST localhost:port/shutdown
    

    安全注意事项:

    如果您需要auth保护的关闭方法,您可能还需要

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

    configure details

  • 0

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

    start.sh

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

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

    stop.sh

    #!/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可以让shell挂起 .

    例如 . 重新部署您的应用程序,您可以使用以下命令重新启动

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

    您可以使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应用程序 . Reference .

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

  • 5

    我没有公开任何 endpoints 并启动( with nohup in background and without out files created through nohup )并使用shell脚本停止(使用 KILL PID gracefully and force kill if app is still running after 3 mins ) . 我只是创建可执行jar并使用PID文件编写器来编写PID文件并将Jar和Pid存储在与应用程序名称相同的文件夹中,并且shell脚本也具有相同的名称,最后是start和stop . 我称这些停止脚本并通过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
    
  • 4

    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);
    
  • 0

    SpringApplication隐式地向JVM注册一个关闭钩子,以确保在退出时正常关闭ApplicationContext . 这也将调用所有使用 @PreDestroy 注释的bean方法 . 这意味着我们不必在启动应用程序中明确使用 ConfigurableApplicationContextregisterShutdownHook() 方法,就像我们在spring核心应用程序中所做的那样 .

    @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");
            }
        }
    }
    
  • 25

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

    @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();
         }
    }
    
  • 4

    从Spring Boot 1.5开始,没有开箱即用的优雅关机制 . 一些spring-boot启动器提供此功能:

    我是nr的作者 . 1.首发名为"Hiatus for Spring Boot" . 它适用于负载均衡器级别,即只是将服务标记为OUT_OF_SERVICE,而不会以任何方式干扰应用程序上下文 . 这允许进行正常关闭,并且意味着,如果需要,可以将服务停止服务一段时间,然后恢复生命 . 缺点是它不会停止JVM,你必须使用 kill 命令 . 当我在容器中运行所有东西时,这对我来说没什么大不了的,因为无论如何我将不得不停下来移除容器 .

    第2和第3款或多或少基于Andy Wilkinson的this post . 它们单向工作 - 一旦触发,它们最终会关闭上下文 .

  • 50

    如果您使用的是maven,则可以使用Maven App assembler plugin .

    The daemon mojo(嵌入JSW)将输出带有start / stop参数的shell脚本 . stop 将正常关闭/杀死您的Spring应用程序 .

    可以使用相同的脚本将您的maven应用程序用作Linux服务 .

  • 42

    如果你在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命令正常关闭 .

  • 3

    它们有很多方法可以关闭spring应用程序 . 一种是在 ApplicationContext 上调用close():

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

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

    使用 endpoints.shutdown.enabled=true 不是最佳配方 . 这意味着您公开一个终点来终止您的应用程序 . 因此,根据您的使用案例和您的环境,您必须确保它...

    Ctrl+C 应该在你的情况下运作良好 . 我认为你的问题是由&符号引起的(&)更多解释:

    Spring Application Context可能已经注册了JVM运行时的关闭钩子 . 见ApplicationContext documentation .

    我不知道Spring Boot是否自动配置了这个钩子 . 我认为是 .

    Ctrl+C 上,shell会向前台应用程序发送 INT 信号 . 这意味着"please interrupt your execution" . 应用程序可以捕获此信号并在终止之前进行清理(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 上 .

相关问题