Привет, Хабр! На связи снова Сергей Соловых, Java-разработчик в команде МТС Digital.
Продолжаю рассказывать о Spring Boot Starter. В прошлой части мы создали принципиальное решение, которое позволит запустить стартер как подключаемую к другому Spring-Boot-приложению библиотеку.
В этой части мы разберемся с зависимостями, стандартными и кастомными аннотациями.

Зависимости
Созданный нами каркас Spring-Boot-стартера был неплох для понимания работы общих механизмов и знакомства с принципиальным устройством. Но развитие компонента требует пересмотреть его архитектуру. Мы реорганизуем фабрики: теперь они будут разделяться не по типам объектов (животные и места содержания), а по профилю каждого обитателя. Это позволит более тонко определить различные условия создания объектов. Давайте по порядку.
Каждую фабрику назовем по имени обитателя и перенесем туда создание питомца и вольеры для него:
@AutoConfiguration("napoleonFactory") public class NapoleonFactory { @Bean public Napoleon napoleon() { return new Napoleon(); } @Bean public Lawn lawn(Napoleon napoleon) { return new Lawn(napoleon); } }
Оказалось, есть условие: Наполеон постоянно что-то жует, и чтобы он мог нормально существовать в космозоопарке, на его территории должен быть огород с разными овощами. Создадим класс Garden:
@RequiredArgsConstructor public class Lawn { private final Napoleon napoleon; public static class Garden { //code } }
И добавим его в фабрику:
@AutoConfiguration("napoleonFactory") public class NapoleonFactory { @Bean public Napoleon napoleon() { System.out.println("Napoleon created"); return new Napoleon(); } @Bean public Lawn.Garden garden() { System.out.println("Garden created"); return new Lawn.Garden(); } @Bean public Lawn lawn(Napoleon napoleon) { System.out.println("Lawn created"); return new Lawn(napoleon); } }
Напишем и запустим тест для проверки:
@SpringBootTest(classes = NapoleonFactory.class) class NapoleonFactoryTest { @Autowired private ApplicationContext context; @ParameterizedTest @MethodSource("beanNames") void applicationContextContainsBean(String beanName) { Assertions.assertTrue(context.containsBean(beanName)); } private static Stream<String> beanNames() { return Stream.of( "napoleonFactory", "napoleon", "garden", "lawn" ); } }
Все бины созданы, но есть проблема — вывод в консоль показывает порядок их создания:
Napoleon created Garden created Lawn created
Как перед созданием Наполеона нам быть уверенными, что для него уже есть огород? Тут может помочь аннотация @DependsOn. Как гласит javadoc аннотации @DependsOn, она принимает параметром массив имен компонентов, от работы которых зависит наш bean. А еще гарантирует, что:
-
требуемые компоненты будут созданы до инициализации bean’а
-
при завершении работы приложения сначала будет завершена работа bean’а, а потом его зависимостей
Давайте укажем, что объект «napoleon» зависит от объекта «garden», а значит, должен быть создан после него:
@Bean @DependsOn("garden") public Napoleon napoleon() { System.out.println("Napoleon created"); return new Napoleon(); }
Запустим тест и посмотрим на вывод в консоль:
Garden created Napoleon created Lawn created
Вот теперь все правильно. Условие зависимости гарантирует, что требуемые бины создадутся. Иначе, если в контексте не будет компонента «garden«, приложение упадет с ошибкой NoSuchBeanDefinitionException.
Условия
Стандартные аннотации
Основное отличие @ConditionOn от @DependsOn в том, что невыполнение условий не приведет к ошибке старта приложения. Просто не будет создан бин определенного класса. Также условия более гибкие в настройке и работают с разными типами параметров. Давайте создадим несложный пример.
@ConditionalOnProperty
Одним из условий создания объектов могут быть заранее предопределенные настройки. С их помощью можно изменять поведение приложения, включая или исключая компоненты в зависимости от значений свойств в файле конфигурации. Это полезно для создания объекта-заглушки на случай, если в инфраструктуре отсутствует какой-либо компонент или недоступен внешний сервис.
Давайте посмотрим, как это работает. Вернемся к нашему примеру. Выяснилось, что во время детских экскурсий в зоопарк некоторые дети пугаются тигрокрыса — уж больно свирепым он выглядит. Так что было решено не показывать его в дни экскурсий малышей. Давайте добавим настройку в параметры нашего зоопарка, которая позволит включать и выключать создание этого животного. В файл application.properties внесем параметр:
app.tigrokris.create=false
Создадим фабрику и применим там данную аннотацию:
@Bean @ConditionalOnProperty( value = "app.tigrokris.create", havingValue = "true", matchIfMissing = false ) public Tigrokris tigrokris() { return new Tigrokris(); }
Параметр value = «app.tigrokris.create» сообщает название требуемого параметра. havingValue = «true» — это ожидаемое значение для выполнения условия. matchIfMissing = false — это определение поведения в случае отсутствия этого параметра.
@ConditionalOnBean и @ConditionalOnMissingBean
Аннотация @ConditionalOnBean диктует условия, что бин будет создан, только если в контексте приложения есть компонент определенного типа или названия. Применяя @ConditionalOnMissingBean, мы получаем обратное условие: бин будет создан в случае отсутствия указанного объекта. Эти аннотации полезны при создании конфигураций для различных сред выполнения, а еще для дефолтной реализации интерфейсов, которые могут быть переопределены пользователями.
В предыдущем примере была описана ситуация, что в определенных условиях бин класса Tigrokris не будет создан. Но теперь приложение может упасть с ошибкой — этот объект необходим для создания вольера. Можно, конечно, и тут добавить @ConditionalOnProperty с тем же условием, но более правильным решением будет ориентироваться на наличие нужного бина в контексте. То есть в зависимости от наличия бина будет или не будет генериться клетка для тигрокрыса. Вот так выглядит вся фабрика целиком:
@AutoConfiguration("tigrokrisFactory") public class TigrokrisFactory { @Bean @ConditionalOnProperty( value = "app.tigrokris.create", havingValue = "true", matchIfMissing = false ) public Tigrokris tigrokris() { return new Tigrokris(); } @Bean @ConditionalOnBean(name = "tigrokris") public ClosedEnclosure closedEnclosure(Tigrokris tigrokris) { return new ClosedEnclosure(tigrokris); } }
Давайте напишем и запустим тест:
@SpringBootTest(classes = TigrokrisFactory.class) class TigrokrisFactoryTest { @Autowired private ApplicationContext context; @ParameterizedTest @MethodSource("beanNames") void applicationContextContainsBean(String beanName, boolean expected) { Assertions.assertEquals(expected, context.containsBean(beanName)); } private static Stream<Arguments> beanNames() { return Stream.of( Arguments.of("tigrokrisFactory", true), Arguments.of("tigrokris", false), Arguments.of("closedEnclosure", false) ); } }
Давайте сразу распространим подобное условие на все вольеры.
@ConditionalOnResource
Проверяет наличие указанного ресурса. Этой аннотацией можно пользоваться для определения логгера, который будет использоваться в приложении — в зависимости от того, какой файл настроек размещен в classpath (например, logback.xml).
Применим это условие к Шуше. Шуша — личность творческая. Лунными ночами он любит писать стихи. И рядом всегда должен быть блокнот, куда он может записать то, что подсказала ему муза. Давайте создадим файл notebook.txt и разместим его в папке ресурсов. Фабрика по созданию объекта типа Shusha теперь выглядит так:
@AutoConfiguration("shushaFactory") public class ShushaFactory { @Bean @ConditionalOnResource(resources = "classpath:notebook.txt") public Shusha shusha() { return new Shusha(); } @Bean @ConditionalOnBean(name = "shusha") public Park park(Shusha shusha) { return new Park(shusha); } }
Потом добавим тест:
@SpringBootTest(classes = ShushaFactory.class) class ShushaFactoryTest { @Autowired private ApplicationContext context; @ParameterizedTest @MethodSource("beanNames") void applicationContextContainsBean(String beanName) { Assertions.assertTrue(context.containsBean(beanName)); } private static Stream<String> beanNames() { return Stream.of( "shushaFactory", "shusha", "park" ); } }
@ConditionalOnExpression
Эта аннотация позволяет указать условие как результат вычисления SpEL-выражения. Например, можно составить комбинацию нескольких параметров конфигурации, создавая объект-заглушку, который заместит необходимый, но недоступный в этом окружении веб-сервис с нужными нам данными: «${app.web.stub.enabled} and ${app.web.mock.data}».
Синий, как и все рептилии, очень любит греться на песочке в ясную теплую погоду. Давайте добавим два параметра в настройки приложения:
app.sun.is-shining=true app.weather=clear
Обратите внимание, что у одного параметра значение булево, а у второго — строковое. Теперь добавим обращения к этим параметрам с помощью Spring Expression Language:
@AutoConfiguration("siniiFactory") public class SiniiFactory { @Bean @ConditionalOnExpression("${app.sun.is-shining} and '${app.weather}'.equals('clear')") public Sinii sinii() { return new Sinii(); } @Bean @ConditionalOnBean(name = "sinii") public Swamp swamp(Sinii sinii) { return new Swamp(sinii); } }
Мы создали все условия, и Синий должен быть доволен. Дальше проверим создание этого объекта:
@SpringBootTest(classes = SiniiFactory.class) class SiniiFactoryTest { @Autowired private ApplicationContext context; @ParameterizedTest @MethodSource("beanNames") void applicationContextContainsBean(String beanName) { Assertions.assertTrue(context.containsBean(beanName)); } private static Stream<String> beanNames() { return Stream.of( "siniiFactory", "sinii", "swamp" ); } }
@ConditionalOnJava
Любопытная аннотация, позволяет регулировать создаваемую реализацию согласно версии Java. Это условие полезно для соблюдения обратной совместимости.
Давайте укажем специальный класс, который будет создаваться при использовании java 1.7 и более ранних версий. Для этого используем аннотацию @ConditionalOnJava, указав в параметрах версию java и правило сравнения:
@ConditionalOnJava(value = JavaVersion.EIGHT, range = OLDER_THAN) public class CosmoZooLegacy { @PostConstruct private void greeting() { System.out.println("Старая версия CosmoZoo"); } }
Не забудьте добавить его в spring.factories. А в основном классе CosmoZoo нам пригодится условие @ConditionalOnMissingBean:
@ConditionalOnMissingBean(CosmoZooLegacy.class) public class CosmoZoo { //code }
Внесем правку в тест:
@SpringBootTest class CosmoZooTest { @Autowired private ApplicationContext context; @ParameterizedTest @MethodSource("beanNames") void applicationContextContainsBean(String beanName, boolean expected) { Assertions.assertEquals(expected, context.containsBean(beanName)); } private static Stream<Arguments> beanNames() { return Stream.of( Arguments.of("science.zoology.CosmoZoo", true), Arguments.of("science.zoology.CosmoZooLegacy", false) ); } @SpringBootApplication public static class TestApplication { //no-op } }
Кроме аннотаций, которые мы рассмотрели, можно упомянуть еще несколько готовых к применению «из коробки». Давайте кратко по ним пробежимся:
-
@ConditionalOnClass и @ConditionalOnMissingClass — условия, проверяющие наличие указанного класса в classpath
-
@ConditionalOnSingleCandidate — это правило создания компонента требует, чтобы в контексте приложения был доступен только один бин определенного типа
-
@ConditionalOnWebApplication и @ConditionalOnNotWebApplication — помогают установить правила создания объекта в зависимости от того, веб-приложение — программа или нет
Существуют еще несколько аннотаций типа @Conditional, предложенных создателями Spring’а и готовых к использованию. Их можно найти в документации.
Собственные условия
Кроме готовых условий, можно создать свои. Настроим с их помощью условия для роботов, ухаживающих за питомцами и зоопарком. Например, робот-уборщик должен в течение дня поддерживать чистоту. Опишем это условие — для этого создадим класс, реализующий интерфейс Condition:
public class TimeCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String start = context.getEnvironment().getProperty("app.cleaning.start"); String end = context.getEnvironment().getProperty("app.cleaning.end"); int currentHour = LocalDateTime.now().getHour(); return Integer.parseInt(start) <= currentHour && currentHour <= Integer.parseInt(end); } }
Добавим в application.properties:
app.cleaning.start=10 app.cleaning.end=18
И конечно же, создадим номинальный класс самого робота-уборщика:
public class Cleaner { public void doWork() { //code } }
Добавим RobotFactory:
@AutoConfiguration("robotFactory") public class RobotFactory { @Bean @Conditional(TimeCondition.class) public CleaningRobot cleaningRobot() { return new CleaningRobot(); } }
Реализация условий через аннотацию
Реализовать условие можно более компактно, написав собственную аннотацию. Давайте добавим робота-фокусника, который по выходным развлекает посетителей:
public class Magician { public void doWork() { //code } }
Теперь добавим новый класс, реализующий интерфейс Condition:
public class DayOfWeekCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { int dayOfWeekNumber = LocalDateTime.now().get(DAY_OF_WEEK); return dayOfWeekNumber > 5; } }
Перенесем это условие в аннотацию:
@Retention(RUNTIME) @Conditional(DateCondition.class) public @interface ConditionalOnDayOfWeek { }
Добавим к созданию бина:
@Bean @ConditionalOnDayOfWeek public Magician magician() { return new Magician(); }
Соединение нескольких условий
AND
Если нужно соблюсти два и более условий, достаточно указать их над нужным классом:
@Bean @Conditional(TimeCondition.class) @ConditionalOnDayOfWeek public MyBean myBean() { return new MyBean(); }
В этом случае объект будет создан, когда выполняются оба условия: это выходные дни и время в указанном диапазоне. Но если условий много, удобнее собрать их в одно. Для этого нужно создать новый класс, наследовать его от AllNestedConditions и указать в нем все необходимые условия:
public class TimeAndDayOfWeekConditions extends AllNestedConditions { public TimeAndDayOfWeekConditions() { super(ConfigurationPhase.REGISTER_BEAN); } @Conditional(TimeCondition.class) static class OnTimeCondition { } @ConditionalOnDayOfWeek static class OnDayOfWeekCondition { } }
Потом создать новую аннотацию:
@Retention(RetentionPolicy.RUNTIME) @Conditional(TimeAndDayOfWeekConditions.class) public @interface ConditionalOnTimeAndDayOfWeek { }
И применить ее в нашей фабрике — пусть по выходным в дневное время работает автоматическая лавка сладостей:
@Bean @ConditionalOnTimeAndDayOfWeek public CandyShop candyShop() { return new CandyShop(); }
OR
Аналогично можно описать несколько условий, соединенных логическим «или». Для этого создаем класс, расширяющий абстрактный класс AnyNestedCondition:
public class TimeOrDayOfWeekConditions extends AnyNestedCondition { public TimeOrDayOfWeekConditions() { super(ConfigurationPhase.REGISTER_BEAN); } @Conditional(TimeCondition.class) static class OnTimeCondition { } @ConditionalOnDayOfWeek static class OnDayOfWeekCondition { } }
Дальнейшие шаги вам уже знакомы: нужно создать аннотацию, а потом вы сможете применить ее внутри фабрики к любому бину.
Очевидно, что при необходимости условия можно усложнять — например, включая несколько условий «AND» в одно «OR». AllNestedConditions и AnyNestedCondition могут принять любые условия — как стандартные, так и разработанные самостоятельно. Варианты добавления условий на результат работы влияния не оказывают — доступно применение как кастомной аннотации, так и @Conditional с параметром — классом, реализующим интерфейс Condition.
ConfigurationPhase
Оба класса, объединяющие условия вложенных классов, принимают в своем конструкторе параметр ConfigurationPhase, enum. Он позволяет выбрать одно из двух значений:
-
PARSE_CONFIGURATION — используется, если условие применяется над классами, обозначенными аннотацией @Configuration
-
REGISTER_BEAN — параметр, применяющийся при описании обычного (не @Configuration) bean-компонента
Вот мы и освоили навыки точного конфигурирования контекста через различные условия создания бинов. Использование условных аннотаций позволяет создавать более гибкие и настраиваемые компоненты, которые могут адаптироваться к различным условиям выполнения. К тому же это позволяет избежать создания лишних бинов, которые не будут использоваться в конкретной конфигурации приложения. Если есть вопросы или хотите поделиться своим опытом, добро пожаловать в комментарии. Все почитаю и обязательно вернусь с обратной связью!
ссылка на оригинал статьи https://habr.com/ru/articles/825014/
Добавить комментарий