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().