Как правильно завершить работу приложения Spring Boot?


121

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

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

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

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


В зависимости от вашей конфигурации в файл записывается PID приложения. Вы можете отправить сигнал уничтожения на этот PID. См. Также комментарии в этом выпуске .
M. Deinum

Какой сигнал мне использовать, я не думаю, что kill -9 - хорошая идея, верно?
Крис

5
Вот почему я указал вам на эту ветку ... Но что-то вроде kill -SIGTERM <PID>должно помочь.
M. Deinum

kill with pid, no -9
Dapeng

1
kill $ (lsof -ti tcp: <port>) - на случай, если вы не хотите использовать актуатор и вам нужно быстрое убийство
Alex Nolasco

Ответы:


63

Если вы используете модуль исполнительного механизма, вы можете закрыть приложение через JMXили, HTTPесли конечная точка включена.

добавить в application.properties:

endpoints.shutdown.enabled = true

Будет доступен следующий URL:

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

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

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

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


1
Я не хочу включать модуль исполнительного механизма. Но я обнаружил, что в моем журнале консоли Spring Boot печатает PID в первой строке. Есть ли способ разрешить Spring Boot печатать PID в другом файле без добавления модуля привода (ApplicationPidListener)?
Крис

2
Обратите внимание, что это должен быть http-пост для этой конечной точки. Вы можете включить его с помощью endpoints.shutdown.enabled = true
sparkyspider

54

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

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; ./start_silent.sh'

Это должен быть ответ. Я только что подтвердил, что сигнал выключения 15 сообщает пружине, что она изящно выключается.
Чад

1
Почему бы вам не вызвать выполнение java-jar с помощью nohup внутри start.sh, а не вызывать выполнение java-jar внутри start.sh, которое вызывается с помощью nohup внутри другого сценария внешней оболочки ??
Ананд Варки Филипс

2
@AnandVarkeyPhilips Единственная причина в том, что я иногда вызываю start.sh из командной строки для целей тестирования, но если вам всегда нужно, nohupвы можете просто объединить команды
Дженс

@Jens, спасибо за информацию. Можете ли вы сказать, что вы делаете это: foo.out 2> foo.err </ dev / null &
Ананд Варки Филипс

1
@jens, спасибо, я сделал это успешно, и я разместил свой сценарий запуска и остановки здесь ниже. ( stackoverflow.com/questions/26547532/… )
Ананд Варки Филипс

52

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

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

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>

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


После второго шага при попытке развертывания возникло следующее сообщение об ошибке: Описание: Параметр 0 метода setAuthenticationConfiguration в org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter требует bean-компонента типа org.springframework.security.config .annotation.authentication.configuration.AuthenticationConfiguration ', который не может быть найден. Действие: Рассмотрите возможность определения bean-компонента типа org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration в вашей конфигурации.
Роберто

2
Обратите внимание: если бы я включил что-то вроде server.contextPath=/appNameв свой application.properties Итак, теперь команда для выключения будет выглядеть так: curl -X POST localhost:8080/appName/shutdown Надеюсь, это может кому-то помочь. Из-за этой ошибки мне пришлось много бороться.
Навин Кумар

В Spring Boot 1.5.8 рекомендуется, если без защиты, иметь в application.properties endpoints.shutdown.enabled=true management.security.enabled=false.
Стефано Скарпанти

Если вы не хотите открывать конечную точку и использовать файл PID для остановки и запуска через сценарий оболочки, попробуйте следующее: stackoverflow.com/questions/26547532/…
Ананд Варки Филипс

27

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

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

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

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


Общий и совместимый с Jenkins полный сценарий запуска и остановки оболочки можно найти здесь ( stackoverflow.com/questions/26547532/… )
Ананд Варки Филипс


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
Клаудио Москосо

8

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

Лучший ответ, который я нашел! Спасибо!
Вагиф

[@ user3137438], чем он отличается от входа в аннотацию pre destroy?
Ананд Варки Филипс

6

Начиная с Spring Boot 2.3 и новее, есть встроенный механизм плавного завершения работы .

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


5

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

В качестве альтернативы , @PostConstructи @PreDestroyя использовал initMethodи destroyMethodатрибуты в @Beanаннотации. Так что для этого примера: @Bean(initMethod="init", destroyMethod="destroy").
acker9

Одна загвоздка, о которой @PreDestroyмогут не знать некоторые разработчики, заключается в том, что такие методы вызываются только для bean-компонентов с областью действия Singleton. Разработчики должны управлять частью очистки жизненного цикла bean-компонента для других областей видимости
asgs

2

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

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

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

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

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

Контекст приложения Spring может зарегистрировать перехватчик выключения во время выполнения JVM. См. Документацию ApplicationContext .

Я не знаю, настраивает ли 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 control, точнее на fg.


2

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

public class SomeClass {
    @Autowire
    private ApplicationContext context

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

У меня это работает. Огромное спасибо.
Хачорнчит Сонгсаен

1

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


0

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

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

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


0

Если вы находитесь в среде 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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.