Как правильно закрыть приложение Spring Boot?

В Spring Boot Document они сказали, что "Каждое SpringApplication регистрирует хук завершения работы с JVM, чтобы гарантировать, что ApplicationContext будет корректно закрыт при выходе".

Когда я нажимаю ctrl+c по команде оболочки приложение может быть корректно завершено. Если я запускаю приложение на производственном компьютере, я должен использовать команду java -jar ProApplicaton.jar, Но я не могу закрыть терминал оболочки, иначе он закроет процесс.

Если я запускаю команду как nohup java -jar ProApplicaton.jar &Я не могу использовать ctrl+c чтобы закрыть его изящно.

Как правильно запустить и остановить Spring Boot Application в производственной среде?

21 ответ

Если вы используете модуль привода, вы можете закрыть приложение через JMX или же HTTP если конечная точка включена (добавить endpoints.shutdown.enabled=true на ваш application.properties файл).

/shutdown - Позволяет корректно завершать работу приложения (по умолчанию не включено).

В зависимости от того, как отображается конечная точка, чувствительный параметр может использоваться в качестве подсказки безопасности. Например, конфиденциальные конечные точки будут требовать имя пользователя / пароль, когда они доступны через HTTP (или просто отключен, если веб-безопасность не включена).

Из загрузочной документации Spring

Вот еще один вариант, который не требует от вас изменения кода или предоставления конечной точки выключения. Создайте следующие сценарии и используйте их для запуска и остановки вашего приложения.

start.sh

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

Запускает ваше приложение и сохраняет идентификатор процесса в файле

stop.sh

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

Останавливает ваше приложение, используя сохраненный идентификатор процесса

start_silent.sh

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

Если вам нужно запустить приложение, используя ssh с удаленного компьютера или конвейера CI, используйте этот скрипт вместо этого, чтобы запустить ваше приложение. Использование start.sh напрямую может привести к зависанию оболочки.

После например Для повторного развертывания приложения вы можете перезапустить его, используя:

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

Что касается ответа @Jean-Philippe Bond,

Вот быстрый пример maven для пользователя maven для настройки конечной точки HTTP для отключения веб-приложения с пружинной загрузкой с помощью spring-boot-starter-activator, чтобы вы могли копировать и вставлять:

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

Примечание по безопасности:

если вам нужен защищенный метод завершения работы, вам также может понадобиться

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

настроить детали:

Кажется, что во всех ответах отсутствует тот факт, что вам может потребоваться завершить некоторую часть работы согласованным образом во время постепенного завершения работы (например, в корпоративном приложении).

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

Вы можете сделать приложение Springboot для записи PID в файл, и вы можете использовать файл pid, чтобы остановить или перезапустить или получить статус, используя скрипт bash. Чтобы записать PID в файл, зарегистрируйте прослушиватель SpringApplication с помощью ApplicationPidFileWriter, как показано ниже:

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

Затем напишите bash-скрипт для запуска приложения весенней загрузки. Ссылка

Теперь вы можете использовать скрипт для запуска, остановки или перезапуска.

Используйте статический exit() в классе SpringApplication для изящного закрытия приложения весенней загрузки.

public class SomeClass {
    @Autowire
    private ApplicationContext context

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

Начиная с Spring Boot 1.5, отсутствует механизм изящного отключения из коробки. Некоторые пружинные стартеры предоставляют такую ​​функциональность:

  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 более или менее основаны на этом посте Энди Уилкинсона. Они работают в одном направлении - после срабатывания они в конечном итоге закрывают контекст.

Я не выставляю никакие конечные точки и запускаю (с nohup в фоновом режиме и без файлов out, созданных через nohup) и останавливаюсь с помощью сценария оболочки (с изящным KILL PID и принудительным уничтожением, если приложение все еще работает через 3 минуты). Я просто создаю исполняемый jar и использую PID-файл для записи PID-файлов и сохраняю Jar и Pid в папке с тем же именем, что и у имени приложения, а сценарии оболочки также имеют одинаковые имена с 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

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

SpringApplication неявно регистрирует ловушку отключения в JVM, чтобы гарантировать, что ApplicationContext будет корректно закрыт при выходе. Это также вызовет все методы bean, помеченные как @PreDestroy, Это означает, что нам не нужно явно использовать 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");
        }
    }
}

Я могу сделать это в Spring Boot Version> = 2.5.3, выполнив следующие действия.

1. Добавьте следующую зависимость привода

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

2. Добавьте эти свойства в application.properties, чтобы выполнить корректное завершение работы.

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

3. Когда вы запустите приложение, вы должны увидеть это в консоли (в зависимости от количества открытых вами конечных точек).

      Exposing 1 endpoint(s) beneath base path '/actuator'

4. Чтобы закрыть приложение, выполните:

      POST: http://localhost:8080/<context-path-if-any>/actuator/shutdown

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

У них есть много способов закрыть приложение весны. Одним из них является вызов close() на ApplicationContext:

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

Ваш вопрос предполагает, что вы хотите закрыть приложение, выполнив Ctrl+C, который часто используется для завершения команды. В этом случае...

использование endpoints.shutdown.enabled=true это не лучший рецепт. Это означает, что вы предоставляете конечную точку для завершения вашего приложения. Таким образом, в зависимости от вашего варианта использования и вашей среды, вам придется обеспечить его...

Ctrl+C должно работать очень хорошо в вашем случае. Я предполагаю, что ваша проблема вызвана амперсандом (&). Более подробное объяснение:

В контексте приложения Spring может быть зарегистрирован хук отключения во время выполнения JVM. Смотрите документацию Application Context.

Я не знаю, настроил ли Spring Boot этот хук автоматически, как вы сказали. Я предполагаю, что это так.

На Ctrl+CВаша оболочка отправляет INT сигнал на переднем плане приложения. Это означает "пожалуйста, прервите казнь". Приложение может перехватить этот сигнал и выполнить очистку до его завершения (хук, зарегистрированный Spring), или просто проигнорировать его (плохо).

nohup это команда, которая выполняет следующую программу с ловушкой, чтобы игнорировать сигнал HUP. HUP используется для завершения программы, когда вы кладете трубку (например, закрывайте ssh-соединение). Более того, он перенаправляет выходные данные, чтобы ваша программа не блокировала пропавший TTY. nohupНЕ игнорирует сигнал INT. Так что это не мешает Ctrl+C работать.

Я предполагаю, что ваша проблема вызвана амперсандом (&), а не nohup. Ctrl+C посылает сигнал на передний план процессов. Амперсанд заставляет ваше приложение работать в фоновом режиме. Одно из решений: сделать

kill -INT pid

использование kill -9 или же kill -KILL плохо, потому что приложение (здесь JVM) не может перехватить его для корректного завершения.

Другое решение - вернуть ваше приложение на передний план. затем Ctrl+C буду работать. Взгляните на контроль Bash Job, точнее на fg,

Многие ответы на приводы в основном правильные. К сожалению, информация о конфигурации и конечной точке изменилась, поэтому они не на 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

попробуйте использовать следующую команду на сервере с запущенным терминалом cmd или bash.

      kill $(jobs -p)

Рекомендацию получить отMicroservices With Spring Boot And Spring Cloud - Build Resilient And Scalable Microservicesкнига.

Если вы используете весеннюю загрузку версии 2.3 и выше, существует встроенный способ корректно завершить работу приложения. Добавьте ниже в application.properties

server.shutdown = изящный spring.lifecycle.timeout-per-shutdown-phase=20 с

Если вы используете более низкую версию весенней загрузки, вы можете написать настраиваемую ловушку отключения и обрабатывать разные 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);
        }

    }


}

Если вы используете maven, вы можете использовать плагин для Maven App.

Демон mojo (который встраивает JSW) выведет сценарий оболочки с аргументом start/stop. stop завершит / убьет изящно ваше приложение Spring.

Тот же сценарий можно использовать для использования приложения maven в качестве службы linux.

Допустим, вы используете приложение Springboot в качестве веб-приложения и хотите обрабатывать существующие запросы, не прерывая их внезапно. Spring boot предоставляет готовое решение.

Отрывок из Spring doc

Плавное завершение работы поддерживается всеми четырьмя встроенными веб-серверами (Jetty, Reactor Netty, Tomcat и Undertow), а также реактивными веб-приложениями и веб-приложениями на основе сервлетов. Это происходит как часть закрытия контекста приложения и выполняется на самом раннем этапе остановки bean-компонентов SmartLifecycle. Эта остановка обработки использует тайм-аут, который обеспечивает период отсрочки, в течение которого существующие запросы будут разрешены для выполнения, но новые запросы не будут разрешены. Точный способ запрета новых запросов зависит от используемого веб-сервера. Jetty, Reactor Netty и Tomcat перестанут принимать запросы на сетевом уровне. Undertow будет принимать запросы, но немедленно отвечать сообщением о недоступности услуги (503).

ПРИМЕЧАНИЕ. Для корректного завершения работы с Tomcat требуется Tomcat 9.0.33 or later

.

Фрагмент кода

Чтобы включить плавное завершение работы

      server.shutdown=graceful

Чтобы настроить период ожидания (здесь я установил значения времени как 1 минута)

      spring.lifecycle.timeout-per-shutdown-phase=1m

Вывод в консоль:

      [INFO] [SpringContextShutdownHook] [org.springframework.boot.web.embedded.tomcat.GracefulShutdown:53] Comme
ncing graceful shutdown. Waiting for active requests to complete

...logs of existing running API endpoints

[tomcat-shutdown]   [org.springframework.boot.web.embedded.tomcat.GracefulShutdown:78] Graceful shut
down complete
[SpringContextShutdownHook] [org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor:218] S
hutting down ExecutorService 'applicationTaskExecutor'

Из приведенных выше журналов вы могли видеть, что весенняя загрузка ожидает обработки всех запущенных запросов и корректно закрывает сервер.

Попробуйте это: нажмите ctrl+C

       - [INFO]
   ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO]
   ------------------------------------------------------------------------ [INFO] Total time:  04:48 min [INFO] Finished at:
   2022-09-07T18:17:35+05:30 [INFO]
   ------------------------------------------------------------------------

Terminate batch job (Y/N)? 

Type Y to terminate

Если вы находитесь в среде Linux, все, что вам нужно сделать, это создать символическую ссылку на ваш файл.jar из /etc/init.d/

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

Таким образом, приложение не будет завершено при выходе из терминала. И приложение будет корректно завершено с командой остановки.

Запустите / запустите развертывание с помощью Spring Boot Maven Plugin

mvn spring-boot:run

и остановка / выключение с

Ctrl+C
Другие вопросы по тегам