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

Почему к Prototype нельзя применить @PreDestroy?

2.0 Middle🔥 101 комментариев
#Spring Framework

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

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

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

# Почему к Prototype нельзя применить @PreDestroy?

Жизненный цикл Bean в Spring

В Spring контейнере существует два основных scope'а для bean'ов:

  1. Singleton (default) — один экземпляр на весь контекст
  2. Prototype — новый экземпляр при каждом request'е

@PreDestroy относится к методам очистки ресурсов, вызываемым перед удалением bean'а. Но в Prototype scope этот механизм работает иначе, чем ожидают разработчики.

1. Жизненный цикл Singleton

Полный цикл

Spring Context Start
    ↓
@PostConstruct (инициализация)
    ↓
Business Logic (использование)
    ↓
Spring Context Shutdown
    ↓
@PreDestroy (очистка)
    ↓
Bean удаляется из памяти

Пример

@Component
@Scope("singleton") // default
public class DatabaseConnection {
    private Connection conn;
    
    @PostConstruct
    public void init() {
        System.out.println("Opening connection");
        // conn = DriverManager.getConnection(url);
    }
    
    public void query() {
        System.out.println("Executing query");
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("Closing connection");
        // conn.close();
    }
}

Вывод

Singleton:
Opening connection
Executing query
Closing connection ✓ (ВЫЗЫВАЕТСЯ)

2. Жизненный цикл Prototype

Проблема

Prototype bean'ы создаются заново каждый раз, когда их запрашивают. Spring контейнер:

  • Создаёт новый экземпляр
  • Вызывает @PostConstruct
  • Передаёт bean'а клиенту
  • ЗАБЫВАЕТ про bean'а

Spring НЕ отслеживает, когда bean перестанет использоваться, поэтому НЕ может вызвать @PreDestroy.

Prototype Bean Creation:
1. new DatabaseConnection()
2. @PostConstruct (инициализация) ✓
3. Возвращение клиенту
4. @PreDestroy (очистка) ✗ НИКОГДА НЕ ВЫЗЫВАЕТСЯ
5. Утечка ресурсов (соединение не закрыто)

Демонстрация

@Component
@Scope("prototype")
public class DatabaseConnection {
    private Connection conn;
    
    @PostConstruct
    public void init() {
        System.out.println("Opening connection");
        // Соединение открыто
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("Closing connection");
        // Это НИКОГДА не выполнится!
    }
}

@Component
public class Service {
    @Autowired
    private DatabaseConnection dbConn; // Новый экземпляр каждый раз
    
    public void doWork() {
        dbConn.query(); // Использование
        // После метода Spring не знает когда dbConn больше не нужен
    }
}

3. Почему это происходит?

Архитектурное решение

Prototype scope разработан для stateful bean'ов, которые клиент создаёт по demand:

@Component
public class RequestFactory {
    @Autowired
    private ApplicationContext context;
    
    public UserRequest createRequest(String userId) {
        // Каждый раз новый bean
        UserRequest req = context.getBean(UserRequest.class);
        req.setUserId(userId);
        return req; // Клиент получает и управляет жизненным циклом
    }
}

В этом сценарии:

  • Клиент ответственен за управление жизненным циклом
  • Spring не может знать, когда bean больше не нужен
  • @PreDestroy не имеет смысла

4. Утечка ресурсов

Проблема в коде

@Component
@Scope("prototype")
public class FileReader implements Closeable {
    private FileInputStream fileStream;
    
    @PostConstruct
    public void init() throws IOException {
        fileStream = new FileInputStream("data.txt");
        System.out.println("File opened");
    }
    
    @PreDestroy // ❌ НИКОГДА НЕ ВЫЗЫВАЕТСЯ
    @Override
    public void close() throws IOException {
        if (fileStream != null) {
            fileStream.close();
            System.out.println("File closed");
        }
    }
}

@Service
public class DataService {
    @Autowired
    private ApplicationContext context;
    
    public void process() {
        // Создание prototype bean'а
        FileReader reader = context.getBean(FileReader.class);
        // File opened
        // ... использование reader
        // ... метод завершится
        // File НЕ закрывается! ❌ Утечка ресурсов
    }
}

5. Решение 1: Использовать Singleton (если возможно)

// ✅ Если bean может быть stateless и потокобезопасным
@Component
@Scope("singleton") // Явно указываем
public class DatabaseConnection {
    private Connection conn;
    
    @PostConstruct
    public void init() {
        System.out.println("Opening connection once");
    }
    
    @PreDestroy // Гарантированно вызовется
    public void cleanup() {
        System.out.println("Closing connection");
    }
}

6. Решение 2: Request scope (для веб-приложений)

Request scope привязан к HTTP request'у, поэтому Spring знает когда удалять bean'ы:

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String userId;
    
    @PostConstruct
    public void init() {
        System.out.println("Request created");
    }
    
    @PreDestroy // ✓ Вызывается в конце request'а
    public void cleanup() {
        System.out.println("Request cleaned up");
    }
}

7. Решение 3: ObjectFactory для управления жизненным циклом

@Component
public class PrototypeService {
    @Autowired
    private ObjectFactory<DatabaseConnection> dbFactory;
    
    public void doWork() {
        // Явно создаём экземпляр
        DatabaseConnection conn = dbFactory.getObject();
        try {
            conn.query();
        } finally {
            // Явно очищаем ресурсы
            conn.cleanup();
        }
    }
}

8. Решение 4: Реализовать DisposableBean

Если использовать DisposableBean вместо @PreDestroy:

@Component
@Scope("prototype")
public class DatabaseConnection implements DisposableBean {
    private Connection conn;
    
    @PostConstruct
    public void init() {
        System.out.println("Opening connection");
    }
    
    @Override
    public void destroy() throws Exception {
        // Клиент может явно вызвать этот метод
        System.out.println("Closing connection");
    }
}

// Использование
DatabaseConnection conn = context.getBean(DatabaseConnection.class);
try {
    conn.query();
} finally {
    conn.destroy(); // Явный вызов
}

9. Решение 5: Try-with-resources (рекомендуется)

Если bean реализует Closeable/AutoCloseable:

@Component
@Scope("prototype")
public class DatabaseConnection implements AutoCloseable {
    private Connection conn;
    
    @PostConstruct
    public void init() {
        System.out.println("Opening connection");
    }
    
    @Override
    public void close() {
        System.out.println("Closing connection");
    }
}

// Использование
try (DatabaseConnection conn = context.getBean(DatabaseConnection.class)) {
    conn.query();
} // conn.close() вызовется автоматически ✓

10. Сравнение scope'ов

┌──────────┬──────────┬────────────────┬──────────────────┐
│  Scope   │ Новые?   │ @PostConstruct │ @PreDestroy      │
├──────────┼──────────┼────────────────┼──────────────────┤
│Singleton │ Нет      │ ✓ Да           │ ✓ Да             │
│Prototype │ Да       │ ✓ Да           │ ✗ Нет (проблема) │
│Request   │ Да/request│✓ Да           │ ✓ Да (end req)   │
│Session   │ Да/session│✓ Да           │ ✓ Да (end sess)  │
│Custom    │ Зависит  │ ✓ Да           │ ✓ Зависит        │
└──────────┴──────────┴────────────────┴──────────────────┘

11. Лучшая практика

// ❌ Избегать
@Component
@Scope("prototype")
public class BadExample {
    @PreDestroy
    public void cleanup() {} // Никогда не вызовется
}

// ✅ Правильно
@Component // Singleton по умолчанию
public class GoodExample {
    @PreDestroy
    public void cleanup() {} // Гарантированно вызовется
}

// ✅ Или для stateful bean'ов
@Component
@Scope("request")
public class RequestScopedExample {
    @PreDestroy
    public void cleanup() {} // Вызовется в конце request'а
}

Итого

Почему @PreDestroy не работает с Prototype:

  1. Spring создаёт новый экземпляр при каждом запросе
  2. Spring теряет трекинг когда bean больше не нужен
  3. Ответственность за очистку лежит на клиенте
  4. @PreDestroy не имеет смысла для Prototype scope

Правильные подходы:

  • Использовать Singleton если bean stateless
  • Использовать Request/Session scope если нужны callback'и
  • Явно вызывать cleanup методы в try-finally
  • Реализовать AutoCloseable для try-with-resources
Почему к Prototype нельзя применить @PreDestroy? | PrepBro