Обработка отсутствующих в контексте spring бинов

Статья содержит код, который можно найти на GitHub project.

В данном примере представлена ситуация и ее решение в случае если необходимый spring бин отсутствет в контексте. Например, бин включается по условию.

Представим, что есть такой компонент:

@Component
@ConditionalOnProperty(value = "bean", havingValue = "true")
public class UpperCaseProcessor implements Processor {

    @Override
    public String process(String str) {
        return str.toUpperCase();
    }
}

Данный компонент будет создан только если будет задано соответствующее свойство в файле конфигурации.

bean : true

Иначе — бина не будет. И если есть код, который использует методы данного бина, мы получим исключение. Для того, чтобы избежать такой ситуации, один из вариантов — использовать для инжекта бина поле типа Optional как представлено в данном фрагменте:

@SpringBootApplication
public class EmptyBeanApplication implements ApplicationRunner {

private final Optional<Processor> processor;

В таком случае, можно обработать ситуацию, когда бин отсутствует, следующим образом:

@Override
public void run(ApplicationArguments args) {

value = processor.map(p -> p.process(value)).orElse(value);
}

Применение бинов в неуправляемых экземплярах

Статья содержит код, который можно найти на GitHub project.

Иногда сталкиваешься с такой ситуацией, когда нужно использовать какой-нибудь сервис, представляющий собой spring bean, в экземпляре, который был создан в коде не spring контейнером, и соответственно, заинжектить обычным способом данный сервис не получится. В таком случае, можно получить нужный бин через контекст.

Например, сервис, который хотим использовать:

@Service
public class StringProcessorService {

public String toUpperCase(String str) {
return str.toUpperCase();
}
}

И класс, который использует данный сервис через контекст:

public class SimpleService {

private static StringProcessorService stringProcessorService = SpringUtility.getBean(StringProcessorService.class);

public String toUpperString(String str) {
return stringProcessorService.toUpperCase(str);
}
}

Получение бина из контекста для удобства реализовано через следующий утильный класс:

@Component
public class SpringUtility implements ApplicationContextAware {


private static ApplicationContext applicationContext;

public void setApplicationContext(ApplicationContext applicationContext) {
SpringUtility.applicationContext = applicationContext;
}

/**
* @param clazz bean class
* @param <T> bean type
* @return a class bean from the application context
*/
public static <T> T getBean(final Class<T> clazz) {
return SpringUtility.applicationContext.getBean(clazz);
}
}

Убедимся, что метод экземпляра работает корректно, запустив следующий тест:

@Test
void shouldCheckBeanCall() {

    assertThat(new SimpleService().toUpperString("test")).isEqualTo("TEST");
}

Область видимости бинов Prototype

Периодически приходится отвечать на вопросы про область видимости  bean в Spring Framework. В данном примере разбирается область видимости(scope) prototype, который не часто встречается в коде. Статья содержит код, который можно найти на GitHub project.

Создадим два бина с разной областью видимости.

@Component
public class SingletonCounter {

private int count = 0;

public void increment() {
count++;
}

public int getCount() {
return count;
}
}

Данный бин имеет область видимости singleton, так как по умолчанию, все бины создаются с данной областью видимости. Данный тип видимости можно задать явно, используя аннотацию ConfigurableBeanFactory.SCOPE_SINGLETON.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeCounter {

private final StringProcessor stringProcessor;
private int count = 0;

public PrototypeCounter(StringProcessor stringProcessor) {
this.stringProcessor = stringProcessor;
}

public void increment() {
count++;
}

public int getCount() {
return count;
}

public String process(String str) {
return stringProcessor.process(str);
}
}

Данный бин имеет область видимости prototype, которая задается соответствующей аннотацией ConfigurableBeanFactory.SCOPE_PROTOTYPE.

Теперь используем данные бины в классах сервисах, через внедрение с помощью конструктора:

@Service
public class PrototypeIncrementService {

private final PrototypeCounter prototypeCounter;
private final SingletonCounter singletonCounter;

public PrototypeIncrementService(PrototypeCounter prototypeCounter, SingletonCounter singletonCounter) {
this.prototypeCounter = prototypeCounter;
this.singletonCounter = singletonCounter;
}

public PrototypeCounter getPrototypeCounter() {
return prototypeCounter;
}

public SingletonCounter getSingletonCounter() {
return singletonCounter;
}
}
@Component
public class SingletonIncrementService {

private final PrototypeCounter prototypeCounter;
private final SingletonCounter singletonCounter;

public SingletonIncrementService(PrototypeCounter prototypeCounter, SingletonCounter singletonCounter) {
this.prototypeCounter = prototypeCounter;
this.singletonCounter = singletonCounter;
}

public PrototypeCounter getPrototypeCounter() {
return prototypeCounter;
}

public SingletonCounter getSingletonCounter() {
return singletonCounter;
}
}

Используем классы сервисы в основном приложении:

@SpringBootApplication
public class ScopeApplication implements ApplicationRunner {

private final PrototypeIncrementService prototypeIncrementService;
private final SingletonIncrementService singletonIncrementService;

public ScopeApplication(PrototypeIncrementService prototypeIncrementService, SingletonIncrementService singletonIncrementService) {
this.prototypeIncrementService = prototypeIncrementService;
this.singletonIncrementService = singletonIncrementService;
}

public static void main(String[] args) {
SpringApplication.run(ScopeApplication.class, args);
}


@Override
public void run(ApplicationArguments args) {

prototypeIncrementService.getPrototypeCounter().increment();
prototypeIncrementService.getSingletonCounter().increment();
singletonIncrementService.getPrototypeCounter().increment();
singletonIncrementService.getSingletonCounter().increment();
}

public PrototypeIncrementService getPrototypeIncrementService() {
return prototypeIncrementService;
}

public SingletonIncrementService getSingletonIncrementService() {
return singletonIncrementService;
}
}

В данным классе мы при запуске приложения инкрементируем наши счетчики.

Соответствующий класс с тестами:

@SpringBootTest
class ScopeApplicationTests {

@Autowired
private ScopeApplication scopeApplication;

@Test
void shouldCheckPrototypeAndSingletonScopeValues() {

assertThat(scopeApplication.getPrototypeIncrementService().getPrototypeCounter().getCount()).isEqualTo(1);
assertThat(scopeApplication.getSingletonIncrementService().getPrototypeCounter().getCount()).isEqualTo(1);
assertThat(scopeApplication.getPrototypeIncrementService().getSingletonCounter().getCount()).isEqualTo(2);
assertThat(scopeApplication.getSingletonIncrementService().getSingletonCounter().getCount()).isEqualTo(2);
assertThat(scopeApplication.getPrototypeIncrementService().getPrototypeCounter().process("String1")).isEqualTo("STRING1");
assertThat(scopeApplication.getSingletonIncrementService().getPrototypeCounter().process("String2")).isEqualTo("STRING2");
}
}

Из теста видно, что бины с областью видимости singleton создаются в единственном экземпляре, и если у них есть состояние, в данном случае поле private int count, при вызове метода для инкремента, будет инкрементироваться именно оно, где бы мы не использовали сервис с внедренным счетчиком.

Однако, если мы рассмотрим бины с областью видимости prototype, можно увидеть, что каждый раз, при внедрении данных бинов, создается новый экземпляр со своим состоянием. Похожего поведения, можно добиться если написать в программе явный вызов создания объекта через оператор new. Однако, в таком случае, мы потеряем возможность внедрять зависимости и друге бины в наши экземпляры, поскольку контейнер не управляет ими.

Выводы:

Singleton beanPrototype bean
Создается только один экземпляр. Он используется везде.Каждый раз при обращению к бину, создается новый экземпляр.
Уничтожается spring при закрытии контекста. Вызывается метод destroyУничтожается сборщиком мусора.
Spring не управляет удалением экземпляра и обработкой закрытия его ресурсов.
Используется, когда не нужно хранить состояние.Используется, когда нужно хранить состояние.

Добавление бинов в коллекцию типа List или Map

Реализацию, описанную в статье можно найти на GitHub project.

Зададим общий интерфейс.

public interface Processor {

String process(String str);
}
И пару spring beans, которые имплементируют данный интерфейс.
@Component
public class RemoveSpaceProcessor implements Processor {

@Override
public String process(String str) {
return str.replace(" ", "");
}
}
@Component
public class UpperCaseProcessor implements Processor {

@Override
public String process(String str) {
return str.toUpperCase();
}
}
В таком случае при инжектировании в spring bean коллекции, типизированной созданным интерфейсом, все бины, имплементирующие данный интерфейс, будут добавлены в эту коллекцию автоматически.
@SpringBootApplication
public class ListBeanApplication implements ApplicationRunner {

private final List<Processor> processorList;

@Value("${stringValue}")
private String value;

public ListBeanApplication(List<Processor> processorList) {
this.processorList = processorList;
}

Добавление бинов также можно организовать в ассоциативный массив. Для этого определяем следующий типизированный интерфейс

Map<String, Processor> processorMap;

По умолчанию, если не задано название бина, будет использоваться название класса. Таким образом все бины, имплементирующие интерфейс Processor будут добавлены в эту коллекцию.

Tест, проверяющий данный кейс

 @Test
    void checkMap() {

        assertThat(listBeanApplication.getProcessorMap()).hasSize(2);
        assertThat(listBeanApplication.getProcessorMap()
                           .get("removeSpaceProcessor")
                           .process("Test String"))
                           .isEqualTo("TestString");
    }