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

时间:2014-10-24 12:05:25

标签: linux spring spring-boot

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

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

如果我运行nohup java -jar ProApplicaton.jar &之类的命令,我就无法使用ctrl+c正常关闭它。

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

18 个答案:

答案 0 :(得分:53)

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

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

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

来自Spring boot documentation

答案 1 :(得分:44)

关于@ Jean-Philippe Bond的回答,

这是maven用户的maven快速示例,用于配置HTTP端点以使用spring-boot-starter-actuator关闭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

列出所有端点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

答案 2 :(得分:34)

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

<强> 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'

答案 3 :(得分:25)

您可以使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

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

答案 4 :(得分:5)

我不公开任何终结点,而是启动(在后台使用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

答案 5 :(得分: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);  

答案 6 :(得分:4)

SpringApplication隐式地向JVM注册一个关闭钩子,以确保在退出时正常关闭ApplicationContext。这也将调用所有使用@PreDestroy注释的bean方法。这意味着我们不必在启动应用程序中明确使用registerShutdownHook() ConfigurableApplicationContext方法,就像我们在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");
        }
    }
}

答案 7 :(得分:4)

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

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

答案 8 :(得分:3)

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

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

    号。 2和3或多或少基于Andy Wilkinson的this post。他们单向工作 - 一旦被触发,他们最终会关闭背景。

答案 9 :(得分:2)

有很多方法可以关闭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上,您的外壳程序将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

答案 10 :(得分:2)

Spring Boot现在支持正常关闭(当前在预发行版本2.3.0.BUILD-SNAPSHOT中)

启用后,关闭应用程序将包括宽限期 可配置的持续时间。在此宽限期内,现有请求 将被允许​​完成,但不允许新的请求

您可以通过以下方式启用它:

server.shutdown.grace-period=30s

https://docs.spring.io/spring-boot/docs/2.3.0.BUILD-SNAPSHOT/reference/html/spring-boot-features.html#boot-features-graceful-shutdown

答案 11 :(得分: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命令正常关闭。

答案 12 :(得分:0)

很多执行器的答案大多是正确的。不幸的是,配置和端点信息已更改,因此它们不是 100% 正确的。要启用执行器,请为 Maven 添加

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

或用于 Gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

对于配置,将以下内容添加到 application.properties。这将暴露执行器中的所有端点:

management.endpoints.web.exposure.include=*
management.endpoint.shutdown.enabled=true

要仅公开关闭端点,请更改为:

management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true

最后,使用 GET 无法使用关闭端点 - 仅使用 POST。所以你必须使用类似的东西:

curl -X POST localhost:8080/actuator/shutdown

答案 13 :(得分:0)

如果您使用的是 spring boot 2.3 n up 版本,有一种内置方式可以优雅地关闭应用程序。在 application.properties 中添加以下内容

server.shutdown=优雅 spring.lifecycle.timeout-per-shutdown-phase=20s

如果您使用的是较低的 spring boot 版本,您可以编写自定义关闭钩子并处理不同的 bean,它们应该如何关闭或它们应该以何种顺序关闭。下面的示例代码。

@Component
public class AppShutdownHook implements ApplicationListener<ContextClosedEvent> {

    private static final Logger logger = LoggerFactory.getLogger(AppShutdownHook.class);


    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        logger.info("shutdown requested !!!");
        try {
            //TODO Add logic to shutdown, diff elements of your application
            } catch (Exception e) {
            logger.error("Exception occcured while shutting down Application:", e);
        }

    }


}

答案 14 :(得分:0)

对于 Spring boot Web 应用程序,Spring boot 提供了开箱即用的解决方案,用于从 2.3.0.RELEASE 版本正常关闭。

摘自Spring doc

参考此答案Code Snippet

答案 15 :(得分:0)

使用SpringApplication类中的静态exit()方法来正常关闭Spring Boot应用程序。

public class SomeClass {
    @Autowire
    private ApplicationContext context

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

答案 16 :(得分:0)

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

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

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

答案 17 :(得分:-1)

使用Spring Boot Maven插件启动/运行部署

mvn spring-boot:run

并使用

停止/关闭
Ctrl+C