В чем разница между init() в бинах Singleton и Prototype?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между init() в Singleton и Prototype бинах
Это важный вопрос, касающийся жизненного цикла Spring бинов и того, как @PostConstruct / init() методы ведут себя по-разному в зависимости от scope бина. Разница существенна и может привести к утечкам ресурсов или неожиданному поведению.
Основные отличия
Singleton scope (default)
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
// По умолчанию scope = Singleton
@Component
public class SingletonService {
@PostConstruct
public void init() {
System.out.println("SingletonService initialized");
// Выполнится ОДИН РАЗ при создании бина
// (обычно при запуске приложения)
}
@PreDestroy
public void destroy() {
System.out.println("SingletonService destroyed");
// Выполнится ОДИН РАЗ при завершении приложения
}
public void doSomething() {
System.out.println("Doing something");
}
}
// Использование
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
// Получение бина первый раз
SingletonService service1 = context.getBean(SingletonService.class);
// Вывод: "SingletonService initialized"
service1.doSomething();
// Получение бина второй раз - это ТОТ ЖЕ объект
SingletonService service2 = context.getBean(SingletonService.class);
System.out.println(service1 == service2); // true
// Контекст закрывается
context.close();
// Вывод: "SingletonService destroyed"
}
}
Характеристики Singleton:
- init() вызывается один раз при создании бина
- Обычно при старте приложения (eager initialization)
- destroy() вызывается один раз при закрытии контекста
- Один экземпляр на весь Spring контекст
- Часть стандартного lifecycle контекста
Prototype scope
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeService {
@PostConstruct
public void init() {
System.out.println("PrototypeService initialized");
// Выполнится КАЖДЫЙ РАЗ при создании нового экземпляра
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeService destroyed");
// МОЖЕТ НЕ БЫТЬ ВЫЗВАН! (зависит от контейнера)
}
public void doSomething() {
System.out.println("Doing something");
}
}
// Использование
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
// Получение бина первый раз
PrototypeService service1 = context.getBean(PrototypeService.class);
// Вывод: "PrototypeService initialized"
service1.doSomething();
// Получение бина второй раз - это НОВЫЙ объект!
PrototypeService service2 = context.getBean(PrototypeService.class);
// Вывод: "PrototypeService initialized" (еще раз)
System.out.println(service1 == service2); // false - разные объекты!
// Контекст закрывается
context.close();
// НЕ выводит "PrototypeService destroyed"
// Spring не управляет lifecycle Prototype бинов!
}
}
Характеристики Prototype:
- init() вызывается для каждого нового экземпляра
- Может быть создан много раз во время работы приложения
- destroy() НЕ вызывается Spring контейнером
- Новый экземпляр создается при каждом getBean() вызове
- Управление жизненным циклом — ответственность клиента
Сравнительная таблица
| Аспект | Singleton | Prototype |
|---|---|---|
| Количество инстанций | 1 на контекст | Много (каждый запрос) |
| init() вызовов | 1 раз при создании | Каждый раз при getBean() |
| destroy() вызовов | 1 раз при shutdown | 0 раз (не контролируется Spring) |
| Управление жизненным циклом | Spring управляет | Клиент управляет |
| Производительность | Оптимально (переиспользование) | Дороже (создание объектов) |
| Потокобезопасность | Потокобезопасные операции нужны | Нет проблем (отдельный экземпляр) |
| Когда использовать | Stateless сервисы | Stateful объекты |
Практические примеры
Пример 1: Singleton с ресурсами
@Component
public class DatabaseConnection {
private Connection connection;
@PostConstruct
public void init() {
try {
this.connection = DriverManager.getConnection("jdbc:mysql://localhost/db");
System.out.println("Database connected ONCE");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@PreDestroy
public void destroy() {
try {
if (connection != null) {
connection.close();
System.out.println("Database disconnected");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public void executeQuery(String sql) {
try {
connection.createStatement().execute(sql);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// Использование
@Service
public class UserService {
@Autowired
private DatabaseConnection db;
public void getAllUsers() {
// Использует ту же базовую连接 (singleton)
db.executeQuery("SELECT * FROM users");
}
}
Пример 2: Prototype для request-scoped объектов
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RequestContext {
private String requestId;
private LocalDateTime createdAt;
private Map<String, Object> attributes = new HashMap<>();
@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.createdAt = LocalDateTime.now();
System.out.println("RequestContext created: " + requestId);
}
public void setAttribute(String key, Object value) {
attributes.put(key, value);
}
public Object getAttribute(String key) {
return attributes.get(key);
}
public String getRequestId() {
return requestId;
}
}
// Использование в Controller
@RestController
public class UserController {
@Autowired
private RequestContext requestContext; // Каждый запрос = новый объект
@GetMapping("/users")
public List<User> getUsers() {
System.out.println("Request ID: " + requestContext.getRequestId());
// Каждый HTTP запрос получает новый RequestContext
return userService.findAll();
}
}
Важная проблема: destroy() в Prototype
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeWithResources {
private FileInputStream fileStream;
@PostConstruct
public void init() throws FileNotFoundException {
this.fileStream = new FileInputStream("myfile.txt");
System.out.println("FileInputStream opened");
}
@PreDestroy
public void destroy() {
try {
if (fileStream != null) {
fileStream.close();
System.out.println("FileInputStream closed");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// ПРОБЛЕМА!
@Service
public class MyService {
@Autowired
private ApplicationContext context;
public void processFiles() {
for (int i = 0; i < 1000; i++) {
PrototypeWithResources proto = context.getBean(PrototypeWithResources.class);
// init() вызывается -> FileInputStream открывается
proto.doSomething();
// destroy() НЕ вызывается -> утечка файловых дескрипторов!
}
}
}
Решение: явное управление жизненным циклом
@Service
public class MyService {
@Autowired
private ApplicationContext context;
public void processFiles() {
for (int i = 0; i < 1000; i++) {
PrototypeWithResources proto = context.getBean(PrototypeWithResources.class);
try {
proto.doSomething();
} finally {
// Явное управление
if (proto instanceof DisposableBean) {
((DisposableBean) proto).destroy();
}
}
}
}
}
// Или используй try-with-resources
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeWithResources implements AutoCloseable {
private FileInputStream fileStream;
@PostConstruct
public void init() throws FileNotFoundException {
this.fileStream = new FileInputStream("myfile.txt");
}
@Override
public void close() throws Exception {
if (fileStream != null) {
fileStream.close();
}
}
}
// Использование
public void processFiles() {
for (int i = 0; i < 1000; i++) {
try (PrototypeWithResources proto = context.getBean(PrototypeWithResources.class)) {
proto.doSomething();
} // close() вызывается автоматически
}
}
Request и Session scopes в Web
// REQUEST scope - создается для каждого HTTP запроса
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedService {
@PostConstruct
public void init() {
System.out.println("Request scope bean initialized");
}
@PreDestroy
public void destroy() {
System.out.println("Request scope bean destroyed (при завершении запроса)");
}
}
// SESSION scope - создается для каждой HTTP сессии
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedService {
@PostConstruct
public void init() {
System.out.println("Session scope bean initialized");
}
@PreDestroy
public void destroy() {
System.out.println("Session scope bean destroyed (при завершении сессии)");
}
}
Жизненный цикл Request/Session:
- init() вызывается один раз при создании бина
- destroy() вызывается один раз при завершении request/session
- Spring УПРАВЛЯЕТ жизненным циклом (в отличие от Prototype)
Best Practices
-
Singleton для stateless сервисов:
@Component public class UserService { // Singleton по default public User findById(Long id) { ... } } -
Prototype для stateful объектов:
@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class UserRequest { private String userId; private List<String> roles = new ArrayList<>(); } -
Request/Session scope в Web приложениях:
@Component @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) public class CurrentUser { private Long userId; } -
Явное управление ресурсами в Prototype:
// Используй try-with-resources или явное close() -
Помни о потокобезопасности Singleton:
// Singleton используется из разных потоков // Убедись, что операции потокобезопасные!
Заключение
Главное отличие: Singleton имеет одну init() при создании и одну destroy() при shutdown, а Prototype имеет init() для каждого экземпляра и НЕ имеет гарантированного destroy(). Выбор scope зависит от природы объекта (stateless vs stateful) и требований приложения. Неправильный выбор может привести к утечкам ресурсов или неожиданным race conditions.