← Назад к вопросам
Выполнится ли всегда метод с аннотацией PreDestroy у Bean с Singleton Scope в Spring?
2.0 Middle🔥 221 комментариев
#Spring Framework
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# PreDestroy у Bean с Singleton Scope в Spring
Краткий Ответ
НЕ всегда. @PreDestroy выполнится только если ApplicationContext будет корректно завершён через метод close() или shutdown(). Если приложение завершится аварийно или без явного вызова close(), @PreDestroy не выполнится.
Условия Выполнения @PreDestroy
1. Нормальное Завершение (Да, выполнится)
@Component
public class SingletonService {
@PostConstruct
public void init() {
System.out.println("[SingletonService] Initialized");
}
@PreDestroy
public void cleanup() {
System.out.println("[SingletonService] Cleaned up"); // ВЫПОЛНИТСЯ
}
}
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println("Application running...");
// Нормальное завершение
context.close(); // @PreDestroy выполнится
System.out.println("Application stopped");
}
}
// Output:
// [SingletonService] Initialized
// Application running...
// [SingletonService] Cleaned up
// Application stopped
2. Аварийное Завершение (Нет, не выполнится)
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println("Application running...");
// Аварийное завершение
System.exit(1); // @PreDestroy НЕ выполнится!
// context.close(); // Этот код не выполнится
}
}
// Output:
// [SingletonService] Initialized
// Application running...
// (завершение без Cleaned up)
3. Исключение в main() (Зависит от обработки)
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
try {
System.out.println("Application running...");
throw new RuntimeException("Critical error!");
} finally {
context.close(); // @PreDestroy выполнится благодаря finally
}
}
}
// Output:
// [SingletonService] Initialized
// Application running...
// [SingletonService] Cleaned up
// Exception in thread "main"
Реальные Сценарии, Когда @PreDestroy НЕ Выполнится
Сценарий 1: System.exit() — Критический!
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
// Какая-то работа
processRequests();
// Неправильное завершение
System.exit(0); // Shutdown hooks НЕ выполнятся!
// context.close(); // Не выполнится
}
private static void processRequests() {
// ...
}
}
Сценарий 2: Убийство процесса ОС
# Linux/Mac
kill -9 <pid> # SIGKILL — @PreDestroy НЕ выполнится
# Правильнее — graceful shutdown
kill <pid> # SIGTERM — обработчик может выполнить cleanup
Сценарий 3: Зависание (Deadlock)
public class DeadlockExample {
static class ServiceA {
@PreDestroy
public void cleanup() {
System.out.println("ServiceA cleanup");
}
}
static class ServiceB {
@PreDestroy
public void cleanup() {
// ЗАВИСАНИЕ — бесконечный цикл
while (true) {
// ...
}
// System.out.println("ServiceB cleanup"); // Никогда не выполнится
}
}
}
// context.close() зависнет на ServiceB и ServiceA cleanup НЕ выполнится
Сценарий 4: OutOfMemoryError
public class OutOfMemoryExample {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
// Исчерпание памяти
List<byte[]> memory = new ArrayList<>();
while (true) {
memory.add(new byte[1024 * 1024]); // 1MB
}
// OutOfMemoryError — @PreDestroy вероятно НЕ выполнится
}
}
Как Гарантировать Выполнение @PreDestroy?
1. Используй try-finally (Нужно в main)
public class Main {
public static void main(String[] args) {
ApplicationContext context = null;
try {
context = new AnnotationConfigApplicationContext(AppConfig.class);
// Логика приложения
runApplication(context);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (context != null) {
context.close(); // ГАРАНТИРОВАНО выполнится
}
}
}
private static void runApplication(ApplicationContext context) {
SingletonService service = context.getBean(SingletonService.class);
service.doWork();
}
}
2. Используй try-with-resources (Java 9+, ConfigurableApplicationContext)
public class Main {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Логика приложения
runApplication(context);
// context.close() выполнится АВТОМАТИЧЕСКИ
}
}
private static void runApplication(ApplicationContext context) {
SingletonService service = context.getBean(SingletonService.class);
service.doWork();
}
}
// Это работает, т.к. AnnotationConfigApplicationContext implements Closeable
3. Обработчик Shutdown Hook (Для System.exit)
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
// Зарегистрируем shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook triggered");
context.close(); // @PreDestroy выполнится
}));
// Теперь даже System.exit() приведёт к cleanup
System.exit(0);
}
}
// Output:
// [SingletonService] Initialized
// Shutdown hook triggered
// [SingletonService] Cleaned up
4. В Spring Boot (Обычно Автоматически)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// Spring Boot автоматически управляет lifecycle при graceful shutdown
}
}
@Component
public class SingletonService {
@PreDestroy
public void cleanup() {
System.out.println("Service cleaned up");
// При SIGTERM Spring Boot выполнит это
}
}
// Graceful shutdown в UNIX
// kill <pid> → SIGTERM → Spring Boot обрабатывает → cleanup выполнится
Таблица Сценариев
| Сценарий | @PreDestroy | Почему |
|---|---|---|
context.close() | ✅ Да | Явный вызов |
| try-finally + close | ✅ Да | Гарантированный вызов |
| try-with-resources | ✅ Да | Автоматический close |
System.exit() | ❌ Нет | Immediate termination |
| SIGKILL (kill -9) | ❌ Нет | Форсированное завершение |
| SIGTERM + hook | ✅ Да | Обработчик может закрыть |
| OutOfMemoryError | ❌ Обычно Нет | Недостаточно памяти |
| Deadlock в cleanup | ❌ Зависание | Другие бины не очистятся |
| Spring Boot graceful | ✅ Да | Встроенная обработка |
Проблема: Зависание в PreDestroy
@Component
public class BadService {
@PreDestroy
public void cleanup() {
// ПРОБЛЕМА: Зависание
waitForExternalService(); // Может никогда не вернуться
}
private void waitForExternalService() {
while (!externalServiceResponds()) {
Thread.sleep(100);
}
}
}
// РЕШЕНИЕ: Добавить timeout
@Component
public class GoodService {
@PreDestroy
public void cleanup() {
var executor = Executors.newSingleThreadExecutor();
try {
// Выполни cleanup с timeout
executor.submit(this::waitForExternalService)
.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("Cleanup timeout, force shutdown");
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdownNow();
}
}
private void waitForExternalService() {
// ...
}
}
Заключение
@PreDestroy НЕ ВСЕГДА выполнится, даже для Singleton beans. Гарантии:
-
✅ Выполнится если:
- Вызвать
context.close()явно - Использовать try-finally или try-with-resources
- Зарегистрировать shutdown hook
- Приложение завершится gracefully (SIGTERM)
- Вызвать
-
❌ НЕ выполнится если:
System.exit()без hook- Зависание в cleanup методе
- SIGKILL (kill -9)
- OutOfMemoryError
-
Best Practice: Всегда оборачивай ApplicationContext в try-finally или try-with-resources для гарантированного cleanup.