Можно ли внедрять prototype бины в другие бины в Spring?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Можно ли внедрять prototype бины в другие бины в Spring?
Можно технически, но это приводит к проблемам. Prototype beans требуют специального обращения при внедрении в singleton beans, иначе получишь неправильное поведение.
Основная проблема
Singleton bean живёт всю жизнь приложения:
Spring container создаёт его один раз
↓
Все классы получают ссылку на один и тот же объект
↓
Объект никогда не пересоздаётся
Prototype bean должен пересоздаваться при каждом использовании:
Когда нужна новая ссылка
↓
Spring создаёт НОВЫЙ объект
↓
Клиент получает НОВЫЙ экземпляр
Проблема: Prototype в Singleton
@Component
@Scope("prototype")
public class ProtoBean {
private UUID id = UUID.randomUUID();
public UUID getId() {
return id;
}
}
@Component // Default: singleton
public class SingletonBean {
@Autowired
private ProtoBean protoBean; // ПРОБЛЕМА!
public void printId() {
System.out.println("ID: " + protoBean.getId());
}
}
// Использование
public static void main(String[] args) {
ApplicationContext ctx = new SpringApplicationContext(Main.class);
SingletonBean bean1 = ctx.getBean(SingletonBean.class);
bean1.printId(); // ID: abc123
SingletonBean bean2 = ctx.getBean(SingletonBean.class);
bean2.printId(); // ID: abc123 <- ОДИНАКОВЫЙ ID!
// Ожидали разные ID, но получили одинаковый
// Потому что protoBean внедрена один раз в singleton
}
ProtoBean создаётся один раз при создании SingletonBean и переиспользуется.
Решение 1: ObjectFactory (рекомендуется)
@Component
public class SingletonBean {
@Autowired
private ObjectFactory<ProtoBean> protoBeanFactory; // Factory вместо bean
public void printId() {
ProtoBean protoBean = protoBeanFactory.getObject(); // Новый объект!
System.out.println("ID: " + protoBean.getId());
}
}
// Использование
SingletonBean bean1 = ctx.getBean(SingletonBean.class);
bean1.printId(); // ID: abc123
SingletonBean bean2 = ctx.getBean(SingletonBean.class);
bean2.printId(); // ID: def456 <- РАЗНЫЕ ID!
// Теперь работает правильно
Решение 2: Provider (альтернатива)
import javax.inject.Provider;
@Component
public class SingletonBean {
@Autowired
private Provider<ProtoBean> protoBeanProvider; // Provider вместо bean
public void printId() {
ProtoBean protoBean = protoBeanProvider.get(); // Новый объект!
System.out.println("ID: " + protoBean.getId());
}
}
// Результат: то же самое, что ObjectFactory
Решение 3: Lookup method injection (старый способ)
@Component
public class SingletonBean {
@Lookup // Spring переопределит этот метод
protected ProtoBean getProtoBean() {
return null; // Реализация будет сгенерирована
}
public void printId() {
ProtoBean protoBean = getProtoBean(); // Новый объект!
System.out.println("ID: " + protoBean.getId());
}
}
// Результат: прямой вызов метода вернёт новый prototype bean
Решение 4: Scoped Proxy (если нужна инъекция)
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ProtoBean {
private UUID id = UUID.randomUUID();
public UUID getId() {
return id;
}
}
@Component // singleton
public class SingletonBean {
@Autowired
private ProtoBean protoBean; // Теперь это proxy
public void printId() {
System.out.println("ID: " + protoBean.getId());
}
}
// Результат: proxy каждый раз перенаправляет на новый объект
Сравнение решений
1. ObjectFactory<ProtoBean>
Плюсы:
+ Spring-специфичный
+ Явный код (видно что берёшь новый объект)
+ Производительность выше
Минусы:
- Spring-специфичный (не portable)
- Нужно вызывать getObject()
2. Provider<ProtoBean>
Плюсы:
+ JSR-330 стандарт (более portable)
+ Работает в других DI фреймворках
+ Явный код
Минусы:
- Нужно добавлять javax.inject зависимость
- Нужно вызывать get()
3. @Lookup
Плюсы:
+ Очень чистый код
+ Выглядит как обычный метод
Минусы:
- Использует CGLIB proxy (может быть медленнее)
- Нужно что-то возвращать (null/dummy)
4. Scoped Proxy
Плюсы:
+ Можно инъектить как обычный bean
+ Прозрачно для клиента
Минусы:
- Всегда через proxy (небольшие overheads)
- Может быть запутанно что происходит
Практический пример: Request scope
Частый случай - внедрять request-scoped bean в singleton:
// Request scope - новый для каждого HTTP request
@Component
@Scope("request")
public class UserContext {
private String userId;
private String requestId;
public UserContext() {
this.requestId = UUID.randomUUID().toString();
}
// getters/setters
}
// Singleton service
@Service
public class UserService {
// НЕПРАВИЛЬНО - будет всегда один и тот же UserContext
// @Autowired
// private UserContext userContext;
// ПРАВИЛЬНО - ObjectFactory
@Autowired
private ObjectFactory<UserContext> userContextFactory;
public void processUser(String userId) {
UserContext context = userContextFactory.getObject(); // Новый для текущего request!
String requestId = context.getRequestId();
// ...
}
}
Когда prototype вообще использовать?
Prototype используется когда:
1. Нужен stateful объект
2. Каждый запрос требует своего экземпляра
3. Объект содержит данные, специфичные для использования
Примеры:
- Request-scoped user context
- Prototype для каждой задачи в потоке
- Объекты, которые изменяют состояние
Лучше использовать:
- Stateless singleton beans (по умолчанию)
- Кэширование для performance
- Prototype только если действительно нужно
Антипаттерны
Антипаттерн 1: Prototype в Singleton без Factory
// ПЛОХО
@Component
public class SingletonBean {
@Autowired
private ProtoBean protoBean; // Это будет один объект!
}
Антипаттерн 2: Избыточное использование Prototype
// Если объект stateless, зачем prototype?
@Component
@Scope("prototype")
public class UtilityService {
public String doSomething(String input) {
return input.toUpperCase();
}
}
// Singleton был бы правильнее
@Component
public class UtilityService {
public String doSomething(String input) {
return input.toUpperCase();
}
}
Правильный паттерн
// 1. Определи scope явно
@Component
@Scope("prototype")
public class MyProtoBean {
// ...
}
// 2. Внедрi через Factory
@Service
public class MyService {
@Autowired
private ObjectFactory<MyProtoBean> factory;
public void doWork() {
MyProtoBean bean = factory.getObject(); // Новый объект
// ...
}
}
// 3. Или используй @Lookup
@Service
public class MyService {
@Lookup
protected MyProtoBean getProtoBean() {
return null;
}
public void doWork() {
MyProtoBean bean = getProtoBean(); // Новый объект
// ...
}
}
Тестирование
Правильное тестирование prototype beans:
@SpringBootTest
public class PrototypeBeanTest {
@Autowired
private ObjectFactory<ProtoBean> factory;
@Test
public void testPrototypeCreatesNewInstance() {
ProtoBean bean1 = factory.getObject();
ProtoBean bean2 = factory.getObject();
assertNotSame(bean1, bean2); // Разные объекты
assertNotEquals(bean1.getId(), bean2.getId()); // Разные ID
}
}
Рекомендации
- По умолчанию используй Singleton — это более эффективно
- Используй Prototype только если действительно нужно — stateful объекты
- При внедрении Prototype в Singleton используй ObjectFactory или Provider — самый чистый способ
- Избегай Scoped Proxy если можно — излишняя сложность
- Документируй почему объект Prototype — не очевидно для других разработчиков
Вывод
Можно внедрять prototype beans, но с правильным подходом:
- Технически работает — Spring позволяет
- Но неправильно просто инъектить — singleton переиспользует prototype
- Используй ObjectFactory<T> или Provider<T> — лучший способ
- Или @Lookup — если нужен чистый код
- Или Scoped Proxy — если нужна инъекция
Главное — понимай разницу между scope'ами и выбирай правильный инструмент для внедрения.