Spring Boot Starter: практически, принципиально и подробно. Part 2

от автора

Привет, Хабр! На связи снова Сергей Соловых, 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/