Як Spring Boot обробляє механізми завершення роботи

pic

Джерело зображення

Spring Boot допомагає розробникам ефективно створювати Java-додатки, але розуміння того, як він керує завершенням роботи додатка, є важливою технічною темою. У цій статті пояснюється цей процес, зосереджуючи увагу на механізмах реєстрації hooks для завершення роботи, як знищуються біні (beans), та як очищаються ресурси.

Що таке Shutdown Hooks у Spring Boot?

Shutdown hooks у Spring Boot є критично важливою частиною того, як фреймворк керує життєвим циклом додатка під час завершення його роботи. Ці hooks з'єднують час виконання додатка і життєвий цикл базової Java Virtual Machine (JVM), дозволяючи контролювати послідовність завершення роботи. Інтеграція Spring Boot з процесом завершення роботи JVM забезпечує інфраструктуру для звільнення ресурсів та коректного завершення операцій.

JVM Shutdown Hooks

У основі обробки завершення роботи в Spring Boot лежить механізм shutdown hook JVM. JVM дозволяє реєструвати екземпляри Thread як shutdown hooks, які виконуються в певному порядку, коли JVM починає процес завершення роботи. Ці hooks активуються під час:

  • Нормального завершення (наприклад, отримання сигналу завершення, як SIGTERM, або виклику System.exit()).
  • Аномального завершення через критичні помилки, такі як неперехоплені винятки в головному потоці.

Spring Boot використовує цей механізм, автоматично реєструючи shutdown hook, коли ініціалізується контекст додатка. Це дозволяє фреймворку впроваджувати власну логіку в послідовність завершення роботи, синхронізуючи життєвий цикл додатка з життєвим циклом JVM.

Роль SpringApplication у інтеграції Shutdown Hook

Інтеграція shutdown hook ініціюється класом SpringApplication. Під час запуску додатка метод SpringApplication.run() ініціалізує контекст додатка. Коли контекст повністю підготовлений, реєструється shutdown hook за допомогою Runtime.getRuntime().addShutdownHook().

Shutdown hook викликає метод close() контексту додатка, коли JVM починає процес завершення роботи. Цей метод визначений в класі AbstractApplicationContext, який є основою для ієрархії контекстів Spring. Таким чином, shutdown hook виступає як точка входу для процесу завершення роботи.

ApplicationContext та логіка завершення роботи

ApplicationContext є центральним елементом для управління життєвим циклом додатка Spring, включаючи операції завершення роботи. Під час фази завершення роботи він координує низку дій:

  1. Сповіщення бінів (beans) з інтерфейсом Lifecycle
    Біни, що реалізують інтерфейс Lifecycle, отримують сповіщення про завершення роботи через їх метод stop(). Це дає можливість зупинити фонові завдання або процеси перед закриттям контексту.
  2. Поширення подій
    Контекст додатка випускає подію ContextClosedEvent, щоб сповістити всіх слухачів (listeners), що контекст закривається. Слухачі подій можуть використати цю подію для виконання специфічних дій, таких як логування або звільнення додаткових ресурсів.
  3. Управління ресурсами
    Ресурси, що керуються в контексті додатка, такі як пул потоків, підключення до баз даних та файлові дескриптори, фіналізуються або закриваються. Це відбувається через зворотні виклики знищення бінів, як описано в наступних розділах.
  4. Завершення роботи вбудованого сервера
    Для додатків, що використовують вбудовані сервери, shutdown hook гарантує, що сервер буде коректно зупинено.

Розгляд безпеки потоків

Послідовність завершення роботи виконується в окремому потоці, яким управляє JVM. Внаслідок цього всі операції, виконувані під час завершення роботи, повинні бути потокобезпечними.
Будь-які довготривалі або блокуючі операції в логіці завершення роботи можуть затримати процес завершення JVM, тому важливо уникати надмірних обчислень у shutdown hooks.

Обмеження JVM Shutdown Hooks

Хоча shutdown hooks JVM надають надійний механізм для завершення роботи додатка, вони не виконуються за певних умов, таких як:

  • Процес примусово завершується (наприклад, kill -9 в системах Unix-подібних).
  • JVM аварійно завершується через серйозні внутрішні помилки (наприклад, сегментаційні помилки).

Залежність Spring Boot від цих hooks означає, що розробники повинні доповнювати обробку завершення роботи додатка додатковими заходами безпеки, такими як зовнішні монітори процесів або механізми перевірки здоров'я, щоб справлятися з крайніми випадками, коли hooks JVM можуть не спрацювати.

Spring Boot інтегрується з shutdown hooks JVM і розширює їх функціональність, щоб досягти структурованого та передбачуваного процесу завершення роботи. Ця інтеграція закладає основу для обробки очищення ресурсів та знищення бінів на наступних етапах.

Як реєструються Shutdown Hooks

Процес реєстрації shutdown hook у Spring Boot тісно інтегрований з життєвим циклом ApplicationContext і залежить від рідної можливості JVM виконувати shutdown hooks під час завершення роботи додатка. Цей розділ розглядає механізм того, як ці hooks реєструються і яку роль вони відіграють в ініціації послідовності завершення роботи.

Реєстрація JVM Shutdown Hook через SpringApplication

Процес починається, коли метод SpringApplication.run() ініціалізує додаток Spring Boot. Під час цієї фази створюється та готується ApplicationContext. Коли контекст повністю ініціалізовано, Spring Boot реєструє shutdown hook у JVM для обробки завершення роботи додатка.

Ця реєстрація відбувається через метод Runtime.getRuntime().addShutdownHook(Thread hook). Функція addShutdownHook() приймає екземпляр Thread, що інкапсулює логіку для виконання під час завершення роботи JVM. У Spring Boot цей потік відповідає за виклик методу close() на контексті додатка.

Фактична реєстрація здійснюється всередині класу SpringApplication і включає наступну послідовність операцій:

  • Ініціалізація контексту
    Як частина методу run(), SpringApplication ініціалізує ConfigurableApplicationContext, викликаючи refresh(). Цей крок готує контекст додатка, завантажуючи всі конфігурації, біни (beans) та властивості.
  • Створення та реєстрація hook
    Після оновлення контексту, реалізація AbstractApplicationContext реєструє shutdown hook, створюючи спеціальний потік. Це відбувається всередині методу registerShutdownHook(), який визначений у класі AbstractApplicationContext:
public void registerShutdownHook() {  
 if (this.shutdownHook == null) {  
 this.shutdownHook = new Thread(() -> {  
 synchronized (this.startupShutdownMonitor) {  
 doClose();  
 }  
 });  
 Runtime.getRuntime().addShutdownHook(this.shutdownHook);  
 }  
}

Ось як працює цей метод:

  1. Поле shutdownHook утримує посилання на потік, який буде викликати метод doClose() на контексті додатка.
  2. Метод doClose() інкапсулює логіку завершення роботи контексту, яка включає сповіщення бінів (beans) життєвого циклу, публікацію подій завершення роботи та знищення бінів.

Hook реєструється лише один раз протягом життєвого циклу додатка, що запобігає повторній реєстрації.

  • Синхронізація потоку
    Потік shutdown hook виконується в синхронізованому блоці, що гарантує, що жоден інший потік не змінює контекст додатка під час завершення роботи.
    Ця синхронізація запобігає виникненню гонки (race condition), яка може статися, якщо кілька потоків намагаються одночасно взаємодіяти з контекстом під час завершення роботи.

  • Час на виконання hook
    Після того як JVM виявляє сигнал завершення (наприклад, SIGTERM або System.exit()), вона призупиняє нормальне виконання додатка і починає викликати зареєстровані shutdown hooks у порядку їх реєстрації. Кожен shutdown hook виконується в окремому потоці, що дозволяє виконувати операції з очищення ресурсів паралельно. Однак JVM надає обмежений час для виконання цих hooks, після чого вона примусово завершує роботу.

Налаштування Shutdown Hooks у Spring Boot

Spring Boot надає механізми для налаштування поведінки завершення роботи, виходячи за межі стандартного закриття контексту:

  • Додавання власних Shutdown Hooks
    Розробники можуть додавати свої власні hooks до JVM за допомогою того ж механізму Runtime.getRuntime().addShutdownHook(). Це дозволяє виконувати додаткову логіку поза контекстом Spring:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {  
 System.out.println("Виконано власну логіку завершення роботи.");  
}));
  • Конфігурація SpringApplication
    Клас SpringApplication надає hooks для програмного налаштування поведінки завершення роботи. Наприклад, розробники можуть використовувати метод setRegisterShutdownHook(), щоб вимкнути автоматичну реєстрацію shutdown hook в Spring Boot, якщо потрібна альтернативна обробка.

Цей підхід чудово підходить для сценаріїв, коли зовнішні оркестратори або контейнеризовані середовища обробляють завершення роботи додатка іншим чином.

Інтеграція вбудованого сервера з Shutdown Hooks

Для додатків, які використовують вбудовані сервери, такі як Tomcat або Jetty, Spring Boot інтегрує серверну логіку завершення роботи з її JVM shutdown hook. Під час реєстрації Spring Boot передає управління системі управління життєвим циклом вбудованого сервера, яка реєструє додаткові hooks для закриття серверних з'єднань і звільнення ресурсів, таких як порти та потоки.

Ця інтеграція здійснюється через абстракцію EmbeddedWebServer, яка взаємодіє з JVM shutdown hook для виклику серверних процедур завершення роботи. Ці процедури виконуються паралельно з закриттям контексту, що забезпечує обробку як ресурсів додатка, так і сервера під час фази завершення роботи.

Життєвий цикл бінів (beans) під час завершення роботи

Коли додаток Spring Boot починає процес завершення роботи, життєвий цикл бінів проходить серію ретельно організованих етапів, щоб забезпечити упорядковане завершення ресурсів і сервісів. Це включає виклик специфічних методів, звільнення залежностей і вирішення пріоритетів знищення бінів, що контролюється через ApplicationContext.

Порядок знищення бінів та вирішення залежностей

Spring Boot управляє знищенням бінів на основі їхніх відносин залежностей. Порядок знищення є зворотним порядку створення бінів, що гарантує, що залежні біни будуть знищені до бінів, від яких вони залежать. Це зворотне вирішення залежностей є критичним, щоб уникнути порушення відносин під час очищення.

  • Черга знищення бінів
    Внутрішньо DefaultListableBeanFactory підтримує реєстр бінів. Під час завершення роботи біни додаються в чергу знищення в зворотному порядку їх ініціалізації. Ця черга керує послідовним знищенням бінів.
  • Прохід по графу залежностей
    Spring Boot здійснює прохід по графу залежностей, ідентифікуючи біни, які мають залежності від інших бінів. Ці залежності позначаються, а їхнє знищення відкладається до того, як будуть оброблені всі залежні біни. Цей прохід по графу виконується рекурсивно, підтримуючи правильну послідовність знищення.
  • Знищення Singleton бінів
    Singleton біни, які ініціалізуються один раз і використовуються в усьому контексті додатка, знищуються в зворотному порядку їх створення.
    Spring Boot перебирає чергу знищення бінів, викликаючи колбеки знищення для кожного сінглтону.
  • Виключення бінів прототипу
    Біни з областю видимості прототипу не управляються контейнером Spring після ініціалізації. Тому вони не включені в процес знищення. Це відповідальність розробника очищати ресурси, пов'язані з бінами прототипу, якщо це необхідно.

Колбеки знищення для бінів

Біни в Spring Boot можуть реалізовувати специфічні інтерфейси або використовувати анотації для визначення власної логіки знищення. Ці колбеки викликаються під час процесу завершення роботи для обробки очищення ресурсів і завершення роботи сервісів.

  • Інтерфейс DisposableBean
    Біни, які реалізують інтерфейс DisposableBean, мають викликаний метод destroy() під час завершення роботи. Цей метод може містити логіку для закриття з'єднань, зупинки виконавців або звільнення ресурсів.
import org.springframework.beans.factory.DisposableBean;  
import org.springframework.stereotype.Component;  

@Component  
public class ExampleDisposableBean implements DisposableBean {  
 @Override  
 public void destroy() {  
 System.out.println("Звільнення ресурсів у ExampleDisposableBean...");  
 // Додайте тут логіку очищення  
 }  
}
  • Анотація @PreDestroy
    Біни також можуть використовувати анотацію @PreDestroy для визначення методу, який буде виконано безпосередньо перед знищенням біна. Цей підхід є більш декларативним і безперешкодно інтегрується з анотаціями для управління життєвим циклом.
import javax.annotation.PreDestroy;  
import org.springframework.stereotype.Component;  

@Component  
public class ExamplePreDestroyBean {  
 @PreDestroy  
 public void cleanup() {  
 System.out.println("Виконання очищення у ExamplePreDestroyBean...");  
 // Додайте логіку звільнення ресурсів тут  
 }  
}
  • Власні методи знищення
    Окрім наведених вище способів, Spring дозволяє вказати власні методи знищення в конфігурації біна. Ці методи явно визначаються та викликаються під час завершення роботи:
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  

@Configuration  
public class CustomDestroyMethodConfig {  
 @Bean(destroyMethod = "customDestroyMethod")  
 public ExampleService exampleService() {  
 return new ExampleService();  
 }  
}  

public class ExampleService {  
 public void customDestroyMethod() {  
 System.out.println("Виконання власного методу знищення...");  
 // Логіка очищення  
 }  
}

Пропагування події завершення роботи

Під час завершення роботи Spring пропагує подію ContextClosedEvent всім зареєстрованим прослуховувачам подій (Event Listeners). Це надає додатковий механізм для бінів для виконання логіки завершення роботи. Прослуховувачі подій (Event Listeners) можуть підписуватися на подію завершення роботи і виконувати завдання, такі як збереження стану, очищення журналів або оновлення зовнішніх систем.

import org.springframework.context.ApplicationListener;  
import org.springframework.context.event.ContextClosedEvent;  
import org.springframework.stereotype.Component;  

@Component  
public class ShutdownEventListener implements ApplicationListener {  
 @Override  
 public void onApplicationEvent(ContextClosedEvent event) {  
 System.out.println("Отримано ContextClosedEvent. Виконання додаткових завдань завершення роботи...");  
 // Додаткові завдання завершення роботи  
 }  
}

Очищення ресурсів та зовнішні залежності

Біни, які керують зовнішніми ресурсами — такими як з'єднання з базами даних, файлові дескриптори або пул потужностей — потребують явного очищення під час процесу завершення роботи. Spring Framework делегує цю відповідальність колбекам знищення відповідних бінів.
Наприклад:

  • Управління пулом потоків:
    Сервіси виконання, визначені як біни в контексті Spring, можуть вказати метод знищення для завершення всіх потоків.
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  

import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  

@Configuration  
public class ExecutorConfig {  
 @Bean(destroyMethod = "shutdown")  
 public ExecutorService executorService() {  
 return Executors.newFixedThreadPool(10);  
 }  
}

Метод shutdown() викликається під час фази знищення для звільнення ресурсів потоків.

  • Пули з'єднань з базами даних
    Пули з'єднань, такі як HikariCP, інтегровані з життєвим циклом Spring Boot. Під час завершення роботи викликається метод close() пулу, що звільняє всі з'єднання.
import com.zaxxer.hikari.HikariDataSource;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  

@Configuration  
public class DataSourceConfig {  
 @Bean(destroyMethod = "close")  
 public HikariDataSource dataSource() {  
 HikariDataSource dataSource = new HikariDataSource();  
 dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/example");  
 dataSource.setUsername("user");  
 dataSource.setPassword("password");  
 return dataSource;  
 }  
}

Координація вбудованого сервера та ApplicationContext

Для додатків з вбудованими серверами Spring Boot координує завершення роботи сервера з знищенням бінів. EmbeddedWebApplicationContext інтегрується з життєвим циклом сервера, викликаючи його механізм зупинки під час фази знищення контексту. Це гарантує, що ресурси сервера, такі як активні HTTP-з'єднання, будуть звільнені паралельно з очищенням ресурсів на рівні додатка.

Висновок

Обробка механізмів завершення роботи в Spring Boot поєднує можливості JVM з власною специфічною логікою фреймворку для створення структурованого процесу завершення роботи. Керуючи знищенням бінів, очищенням ресурсів та завершенням роботи вбудованого сервера точним чином, він надає надійну систему для завершення роботи додатка. Розробники можуть використовувати ці знання для узгодження своїх вимог до завершення роботи з механізмами Spring Boot, створюючи додатки, які ефективно та передбачувано обробляють завершення роботи.

  1. Документація Spring Boot
  2. Java Shutdown Hooks
  3. Інтерфейси життєвого циклу Spring Framework
  4. Анотація @PreDestroy

Дякую за прочитання! Якщо вам сподобалась ця стаття, будь ласка, розгляньте можливість підкреслити, поплескати, відповісти або підключитися до мене на Twitter/X це дуже цінується і допомагає підтримувати контент на такому рівні!

pic

Іконка Spring Boot від Icons8

Перекладено з: How Spring Boot Handles Shutdown Hooks

Leave a Reply

Your email address will not be published. Required fields are marked *