Как реализовать в Spring создание нового Prototype бина при каждом вызове внутри Singleton бина
Что проверяют
Понимает ли кандидат, что инъекция в синглтон происходит один раз, и знает ли штатные способы обойти это. Ответ «поставить @Scope("prototype")» без продолжения считается неверным.
Как отвечать
Суть проблемы: контейнер внедряет зависимости в синглтон в момент его создания. Поле получает один экземпляр прототипа и держит его вечно, независимо от объявленной области видимости. Нужен способ спрашивать новый экземпляр в момент вызова.
Четыре рабочих варианта, от предпочтительного к запасному.
1. ObjectProvider (или ObjectFactory). Основной способ: типобезопасно, не тащит в класс весь контекст.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeBean { }
@Component
public class SingletonBean {
private final ObjectProvider<PrototypeBean> provider;
public SingletonBean(ObjectProvider<PrototypeBean> provider) {
this.provider = provider;
}
public PrototypeBean next() {
return provider.getObject(); // новый экземпляр на каждый вызов
}
}2. @Lookup. Spring подменяет метод CGLIB-наследником, возвращающим свежий бин. Класс и метод не должны быть final, метод может быть абстрактным.
@Lookup
protected abstract PrototypeBean createPrototype();3. Scoped proxy: @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) на прототипе, дальше обычный @Autowired. Важная тонкость: прокси добывает новый целевой объект на каждый вызов метода, а не на каждое обращение к полю, поэтому состояние между вызовами не переживает. Это удивляет, если ожидать привычной семантики.
4. Инъекция ApplicationContext и getBean() либо реализация ApplicationContextAware. Работает, но это service locator: класс знает про Spring, хуже тестируется и зависимость перестаёт быть видна в сигнатуре. Приводить как крайний вариант, а не как основной.
Отдельно стоит сказать про жизненный цикл: прототип контейнер до конца не ведёт. @PreDestroy и DisposableBean у него не вызываются, Spring отдаёт объект и забывает о нём. Если бин держит ресурс, закрывать обязан вызывающий код, иначе получите утечку.
Проверяется всё простым тестом: assertNotSame(bean.next(), bean.next()).
Где спотыкаются
Ставят @Scope("prototype") и удивляются, что поле в синглтоне не меняется, потому что путают область видимости бина с моментом его получения. Второе: используют прототип для хранения состояния и не закрывают его ресурсы, полагаясь на несуществующий вызов @PreDestroy.