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

Как удаляются бины при завершении программы в Spring

2.2 Middle🔥 171 комментариев
#Spring Boot и Spring Data#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ: Как удаляются бины при завершении программы в Spring

Цикл жизни Bean'а в Spring включает инициализацию и уничтожение. При завершении приложения Spring вызывает методы разрушения в обратном порядке инициализации — это гарантирует чистое завершение и освобождение ресурсов.

Полный цикл жизни Bean'а

1. Создание экземпляра (new)
2. Установка свойств (dependency injection)
3. Вызов @PostConstruct (инициализация)
4. БИН РАБОТАЕТ (используется приложением)
5. Вызов @PreDestroy (очистка ресурсов)
6. Удаление из памяти (garbage collection)

Механизм 1: @PreDestroy аннотация (рекомендуется)

Это самый простой и понятный способ. При завершении контекста Spring вызывает методы с @PreDestroy.

@Component
public class DatabaseConnection {
    private Connection connection;
    
    @PostConstruct  // Вызывается при создании бина
    public void initialize() {
        connection = DriverManager.getConnection(
            "jdbc:mysql://localhost/mydb",
            "user",
            "password"
        );
        System.out.println("Database connection opened");
    }
    
    @PreDestroy  // Вызывается перед удалением бина
    public void cleanup() {
        if (connection != null) {
            try {
                connection.close();
                System.out.println("Database connection closed");
            } catch (SQLException e) {
                System.err.println("Error closing connection: " + e.getMessage());
            }
        }
    }
    
    public void executeQuery(String sql) throws SQLException {
        // Выполнить запрос
    }
}

// Использование
public class Main {
    public static void main(String[] args) {
        // Создание контекста
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        
        DatabaseConnection db = context.getBean(DatabaseConnection.class);
        // Вывод:
        // Database connection opened
        
        // Использование бина
        db.executeQuery("SELECT * FROM users");
        
        // Завершение контекста
        ((ConfigurableApplicationContext) context).close();
        // Вывод:
        // Database connection closed
    }
}

Механизм 2: InitializingBean и DisposableBean интерфейсы

Древний способ, но всё ещё используется. Bean реализует интерфейсы и переопределяет методы.

@Component
public class FileManager implements InitializingBean, DisposableBean {
    private FileWriter fileWriter;
    
    @Override
    public void afterPropertiesSet() throws Exception {  // Вместо @PostConstruct
        fileWriter = new FileWriter("application.log");
        System.out.println("File opened for writing");
    }
    
    @Override
    public void destroy() throws Exception {  // Вместо @PreDestroy
        if (fileWriter != null) {
            fileWriter.close();
            System.out.println("File closed");
        }
    }
    
    public void writeLog(String message) throws IOException {
        fileWriter.write(message + "\n");
    }
}

// Минусы:
// - Класс тесно связан со Spring
// - Менее читаемо
// - Сложнее тестировать

Механизм 3: Bean конфигурация с initMethod и destroyMethod

Используется когда контролируешь конфигурацию, но не исходный класс.

public class LegacyResource {
    public void startup() {  // Не аннотирован
        System.out.println("Resource starting...");
    }
    
    public void shutdown() {  // Не аннотирован
        System.out.println("Resource shutting down...");
    }
}

// Конфигурация
@Configuration
public class AppConfig {
    @Bean(initMethod = "startup", destroyMethod = "shutdown")
    public LegacyResource legacyResource() {
        return new LegacyResource();
    }
}

// Spring вызовет:
// 1. new LegacyResource() - создание
// 2. legacyResource.startup() - инициализация
// 3. (использование)
// 4. legacyResource.shutdown() - удаление

// Использование
public class Main {
    public static void main(String[] args) {
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        // Вывод: Resource starting...
        
        ((ConfigurableApplicationContext) context).close();
        // Вывод: Resource shutting down...
    }
}

Механизм 4: Shutdown hooks (когда контекст закрывается)

Специальные обработчики для корректного завершения.

@Component
public class GracefulShutdown {
    private ExecutorService executorService;
    
    @PostConstruct
    public void init() {
        executorService = Executors.newFixedThreadPool(10);
        
        // Регистрируем shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutdown hook called");
            shutdown();  // Явно вызываем очистку
        }));
    }
    
    @PreDestroy
    public void shutdown() {
        System.out.println("Shutting down executor service");
        executorService.shutdown();
        
        try {
            if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
                executorService.shutdownNow();  // Force shutdown
                System.out.println("Forced shutdown");
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }
    }
    
    public void submitTask(Runnable task) {
        executorService.submit(task);
    }
}

Порядок вызова @PreDestroy методов

Важно! Методы вызываются в обратном порядке инициализации (LIFO - Last In First Out).

@Configuration
public class AppConfig {
    @Bean
    public ServiceA serviceA() {
        return new ServiceA();  // Инициализируется первым
    }
    
    @Bean
    public ServiceB serviceB(ServiceA serviceA) {  // Зависит от A
        return new ServiceB(serviceA);  // Инициализируется вторым
    }
    
    @Bean
    public ServiceC serviceC(ServiceB serviceB) {  // Зависит от B
        return new ServiceC(serviceB);  // Инициализируется третьим
    }
}

class ServiceA {
    @PostConstruct
    public void init() { System.out.println("A init"); }  // 1
    
    @PreDestroy
    public void cleanup() { System.out.println("A cleanup"); }  // 3
}

class ServiceB {
    @PostConstruct
    public void init() { System.out.println("B init"); }  // 2
    
    @PreDestroy
    public void cleanup() { System.out.println("B cleanup"); }  // 2
}

class ServiceC {
    @PostConstruct
    public void init() { System.out.println("C init"); }  // 3
    
    @PreDestroy
    public void cleanup() { System.out.println("C cleanup"); }  // 1
}

// Вывод при запуске:
// A init
// B init
// C init
// (приложение работает)
// C cleanup  <- Удаляется в обратном порядке
// B cleanup
// A cleanup

Почему обратный порядок? Потому что C зависит от B, B зависит от A. Нужно сначала очистить C, потом B (освободит ресурсы для A), потом A.

Обработка ошибок при cleanup

@Component
public class ResourceManager {
    private Resource resource1;
    private Resource resource2;
    private Resource resource3;
    
    @PostConstruct
    public void initialize() {
        resource1 = new Resource("Resource 1");
        resource2 = new Resource("Resource 2");
        resource3 = new Resource("Resource 3");
    }
    
    @PreDestroy
    public void cleanup() {
        // Правильно: очищаем ВСЕ ресурсы даже если один упадёт
        List<String> errors = new ArrayList<>();
        
        try {
            resource1.close();
        } catch (Exception e) {
            errors.add("Resource 1 cleanup failed: " + e.getMessage());
        }
        
        try {
            resource2.close();
        } catch (Exception e) {
            errors.add("Resource 2 cleanup failed: " + e.getMessage());
        }
        
        try {
            resource3.close();
        } catch (Exception e) {
            errors.add("Resource 3 cleanup failed: " + e.getMessage());
        }
        
        if (!errors.isEmpty()) {
            System.err.println("Cleanup errors:");
            errors.forEach(System.err::println);
        }
    }
}

Специальные случаи: Bean Scope

Singleton (по умолчанию) — удаляется при close контекста:

@Component
@Scope("singleton")  // По умолчанию
public class SingletonBean {
    @PreDestroy
    public void cleanup() {  // ВЫЗЫВАЕТСЯ
        System.out.println("Singleton cleaned up");
    }
}

Prototype — НЕ удаляется Spring:

@Component
@Scope("prototype")
public class PrototypeBean {
    @PreDestroy
    public void cleanup() {  // НЕ ВЫЗЫВАЕТСЯ автоматически!
        System.out.println("Prototype cleaned up");
    }
}

// Нужно очищать вручную
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
PrototypeBean bean = context.getBean(PrototypeBean.class);
// ... использование
bean.cleanup();  // Явно вызываем cleanup

Spring Boot: Graceful Shutdown

В Spring Boot есть встроенная поддержка изящного завершения.

# application.yml
server:
  shutdown: graceful  # Ждёт завершения текущих запросов
  tomcat:
    max-connections: 100

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # Максимум 30 секунд на закрытие
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
        // При ctrl+c или SIGTERM сигнале:
        // 1. Spring ждёт завершения текущих HTTP запросов
        // 2. Закрывает контекст
        // 3. Вызывает все @PreDestroy методы
        // 4. Завершает приложение
    }
}

Практический пример: Полное приложение

@Component
public class DatabasePool {
    private DataSource dataSource;
    
    @PostConstruct
    public void init() {
        dataSource = new HikariDataSource();
        System.out.println("Database pool initialized");
    }
    
    @PreDestroy
    public void shutdown() {
        if (dataSource != null) {
            ((HikariDataSource) dataSource).close();
            System.out.println("Database pool closed");
        }
    }
}

@Component
public class CacheManager {
    private Map<String, Object> cache;
    
    @PostConstruct
    public void init() {
        cache = new ConcurrentHashMap<>();
        System.out.println("Cache initialized");
    }
    
    @PreDestroy
    public void shutdown() {
        cache.clear();
        System.out.println("Cache cleared");
    }
}

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
        // Вывод:
        // Database pool initialized
        // Cache initialized
        // (приложение работает)
        // Cache cleared
        // Database pool closed
    }
}

Итоговый вывод

Удаление бинов при завершении программы происходит через:

  1. @PreDestroy аннотация (рекомендуется) — самый простой способ
  2. DisposableBean интерфейс (legacy) — старинный способ
  3. destroyMethod в @Bean (конфигурация) — для внешних классов
  4. Shutdown hooks (специальные случаи) — для сложной логики

Важные моменты:

  • Методы вызываются в обратном порядке инициализации
  • Spring гарантирует вызов методов при нормальном завершении
  • Prototype beans НЕ очищаются автоматически
  • Обрабатывай ошибки при cleanup, чтобы очистить ВСЕ ресурсы
  • Spring Boot поддерживает graceful shutdown

Правильная очистка ресурсов критична для production систем — иначе утечки соединений, потеря данных, дедлоки.