← Назад к вопросам

Выполнится ли всегда метод с аннотацией 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. Гарантии:

  1. Выполнится если:

    • Вызвать context.close() явно
    • Использовать try-finally или try-with-resources
    • Зарегистрировать shutdown hook
    • Приложение завершится gracefully (SIGTERM)
  2. НЕ выполнится если:

    • System.exit() без hook
    • Зависание в cleanup методе
    • SIGKILL (kill -9)
    • OutOfMemoryError
  3. Best Practice: Всегда оборачивай ApplicationContext в try-finally или try-with-resources для гарантированного cleanup.

Выполнится ли всегда метод с аннотацией PreDestroy у Bean с Singleton Scope в Spring? | PrepBro