Как правильно закрыть приложение 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
(или просто отключен, если веб-безопасность не включена).
Вот еще один вариант, который не требует от вас изменения кода или предоставления конечной точки выключения. Создайте следующие сценарии и используйте их для запуска и остановки вашего приложения.
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, отсутствует механизм изящного отключения из коробки. Некоторые пружинные стартеры предоставляют такую функциональность:
- https://github.com/jihor/hiatus-spring-boot
- https://github.com/gesellix/graceful-shutdown-spring-boot
- 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
У них есть много способов закрыть приложение весны. Одним из них является вызов 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