Привет, Хабр! На связи снова Сергей Соловых, 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-компонента 
Вот мы и освоили навыки точного конфигурирования контекста через различные условия создания бинов. Использование условных аннотаций позволяет создавать более гибкие и настраиваемые компоненты, которые могут адаптироваться к различным условиям выполнения. К тому же это позволяет избежать создания лишних бинов, которые не будут использоваться в конкретной конфигурации приложения. Если есть вопросы или хотите поделиться своим опытом, добро пожаловать в комментарии. Все почитаю и обязательно вернусь с обратной связью!
 
          