Как удаляются бины при завершении программы в Spring
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ: Как удаляются бины при завершении программы в 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
}
}
Итоговый вывод
Удаление бинов при завершении программы происходит через:
- @PreDestroy аннотация (рекомендуется) — самый простой способ
- DisposableBean интерфейс (legacy) — старинный способ
- destroyMethod в @Bean (конфигурация) — для внешних классов
- Shutdown hooks (специальные случаи) — для сложной логики
Важные моменты:
- Методы вызываются в обратном порядке инициализации
- Spring гарантирует вызов методов при нормальном завершении
- Prototype beans НЕ очищаются автоматически
- Обрабатывай ошибки при cleanup, чтобы очистить ВСЕ ресурсы
- Spring Boot поддерживает graceful shutdown
Правильная очистка ресурсов критична для production систем — иначе утечки соединений, потеря данных, дедлоки.