Почему к Prototype нельзя применить @PreDestroy?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Почему к Prototype нельзя применить @PreDestroy?
Жизненный цикл Bean в Spring
В Spring контейнере существует два основных scope'а для bean'ов:
- Singleton (default) — один экземпляр на весь контекст
- 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:
- Spring создаёт новый экземпляр при каждом запросе
- Spring теряет трекинг когда bean больше не нужен
- Ответственность за очистку лежит на клиенте
- @PreDestroy не имеет смысла для Prototype scope
Правильные подходы:
- Использовать Singleton если bean stateless
- Использовать Request/Session scope если нужны callback'и
- Явно вызывать cleanup методы в try-finally
- Реализовать AutoCloseable для try-with-resources