Prototype-бины в singleton
- Область видимости бинов Spring: singleton и prototype
- Injection prototype в singleton в Spring
- Пример аннотации @Lookup в Spring
- ObjectFactory и Provider в Spring
- Как получить новый экземпляр бина prototype в Spring
- ApplicationContext Spring getBean для области видимости prototype
- Spring Scoped Proxy
Области видимости бинов Spring
Прежде всего – давайте вспомним ключевые типы областей видимости для бинов Spring: singleton, prototype, request, session.
Singleton-бины (область видимости по умолчанию) имеют только один общий экземпляр бина на каждый IoC-контейнер Spring.
Prototype-бины создаются каждый раз, когда их запрашивают из контейнера.
Проблема внедрения
Предположим, у нас есть этот prototype-бин:
@Service
@Scope(scopeName = "prototype")
public class MyPrototypeBean {
private final UUID uuid = UUID.randomUUID();
public String getId() {
return uuid.toString();
}
}
И этот REST-контроллер, который инджектит и использует MyPrototypeBean:
@RestController
public class MySingletonBean {
@Autowired
private MyPrototypeBean prototypeBean;
@GetMapping("/")
public String id() {
return prototypeBean.getId();
}
}
Когда мы запустим этот код, мы увидим, что id всегда будет одинаковым. Почему?
Потому что зависимости singleton-бина подключаются один раз, даже если они prototype.
Первое решение: ручное использование ApplicationContext
Мы можем подключить ApplicationContext (или BeanFactory) и получить prototype-бин из контейнера:
@RestController
public class MySingletonBean {
@Autowired
private ApplicationContext appContext;
@GetMapping("/")
public String id() {
return appContext.getBean(MyPrototypeBean.class).getId();
}
}
Второе решение: @Lookup
По сути, это решение является улучшением первого.
Аннотация @Lookup в Spring указывает фреймворку динамически заменять вызов метода логикой, которая возвращает новый экземпляр бина из контекста Spring – даже если бин имеет область видимости prototype, и мы вызываем его из класса с областью видимости singleton.
Если мы определим абстрактный метод с использованием @Lookup, Spring сгенерирует реализацию во время выполнения:
@Lookup
abstract MyPrototypeBean getMyPrototypeBean();
Его нужно использовать следующим образом:
MyPrototypeBean myPrototypeBean = getMyPrototypeBean();
String id = myPrototypeBean.getId();
Обратите внимание, что getMyPrototypeBean() является abstract и не может быть private. Также имейте в виду, что класс сервиса также должен быть abstract.
Третье решение: ObjectFactory<T>
Когда мы можем захотеть использовать ObjectFactory? Что ж, это особенно удобно для тестов: ObjectFactory имеет один метод getObject(), который легко можно замокать.
Но почему бы тогда не использовать setter-injection? Ответ в том, что если мы используем setter-injection – тогда характеристики prototype у MyPrototypeBean исчезают.
@RestController
public class MySingletonBean {
@Autowired
private ObjectFactory<MyPrototypeBean> objectFactory;
@GetMapping("/")
public String id() {
return objectFactory.getObject().getId();
}
}
Четвертый вариант: Provider<T>
Причины использования Provider<T> те же, что и для ObjectFactory<T>.
Это требует зависимости jakarta.inject:
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
И выглядит это так:
@RestController
public class MySingletonBean {
@Autowired
private Provider<MyPrototypeBean> provider;
@GetMapping("/")
public String id() {
return provider.get().getId();
}
}
Пятый вариант: Scoped Proxy
Когда мы устанавливаем proxyMode в аннотации @Scope для бина, Spring заменяет фактический бин на прокси в IoC-контейнере.
Затем, каждый вызов метода делегируется реальному экземпляру, разрешенному из области видимости (например, область видимости request).
Следовательно, Scoped Proxy корректно работают в средах, где контекст области видимости остается активным на протяжении нескольких вызовов методов, таких как:
- HTTP-запросы (с
@RequestScope) - HTTP-сессии (с
@SessionScope)
Теоретически, его все еще возможно использовать с prototype.
Однако, следует помнить, что если у нас есть область видимости prototype + scoped proxy, то новый экземпляр создается каждый раз, когда мы делаем вызов метода.
@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyPrototypeBean {
private final String id = UUID.randomUUID().toString();
public String getId() {
return id;
}
}
Осталось подключить MyPrototypeBean в MySingletonBean как обычно:
@RestController
public class MySingletonBean {
@Autowired
private MyPrototypeBean myPrototypeBean;
@GetMapping("/")
public String id() {
return myPrototypeBean.get().getId();
}
}
Новый бин будет создаваться для каждого вызова метода myPrototypeBean().