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