Обработка отсутствующих в контексте 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);
}

Область видимости бинов 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");
    }