[Из песочницы] Использование Conditional в Spring
В этом материале я хочу описать очень полезную, и часто используемую аннотацию Conditional и интерфейс Condition.
Контекст Spring — это огромный контейнер различных бинов, как самого спринга, так и пользовательских. Всегда хочется иметь гибкие инструменты управления этим зоопарком бинов. Аннотация @Conditional как раз и создана для этого.
Самым распространенным способом управления контекстом спринга являются профили. Они позволяют быстро и просто регулировать создание бинов. Но иногда может потребоваться более тонкая настройка.
Например, во время тестирования, возникает проблема: юнит тест на машине разработчика требует для своей работы бин типа X, при прогоне этого же теста на сервере сборки необходим бин Y, а на продакшене требуется бин Z. @Conditional предлагает в этом случае простое и легкое решение. Так же, как часто бывает при работе в несинхронизированных командах, кто-то не успевает к сроку выполнить свою доработку, а твой функционал уже готов. Нужно подстраиваться под данные условия и изменять поведение. То есть, добавить возможность изменять контекст приложения без перекомпиляции, например, изменяя только один параметр в конфигурации.
Рассмотрим эту аннотацию подробнее. Над каждым бином в исходном коде мы можем добавить @Conditional и спринг при его создании автоматически проверит условия, указанные в данной аннотации.
В официальной документации она объявлена так:
@Target(value={TYPE,METHOD})
@Retention(value=RUNTIME)
@Documented
public @interface Conditional
При этом, передавать в нее нужно набор условий:
Class extends Condition>[]
Где Conditional — функциональный интерфейс, который содержит метод
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
Проверим как это работает на практике, на живом примере. Наше приложение имеет интерфейсы как в виде soap/rest — services, так и в виде JMS. Но администраторы не успели вовремя подготовить соответствующую инфраструктуру — использовать JMS мы не можем.
В нашем проекте присутствует какая-то java-конфигурация для JMS:
@Configuration
public class JmsConfig {
...
}
Spring находит эту конфигурацию, и начинает ее инициализацию. Следом подтягиваются все остальные зависимые бины, например читающие из очереди. Чтобы отключить создание данной конфигурации воспользуемся аннотацией производной от Conditional — ConditioanalOnProperty
@ConditionalOnProperty(
value="project.mq.enabled",
matchIfMissing = false)
@Configuration
public class JmsConfig {
...
}
Здесь мы сообщаем контекст билдеру, что данную конфигурацию мы создаем, только в случае наличия положительного значения константы project.mq.enabled в файле настроек.
Теперь перейдем к зависимым бинам и пометим их аннотацией ConditioanalOnBean, которая не даст спрингу создать бины зависимые от нашей конфигурации.
@ConditionalOnBean(JmsConfig.class)
@Component
public class JmsConsumer {
...
}
Таким образом, с помощью одного параметра мы можем отключить не нужные нам компоненты приложения, а потом с помощью изменения конфигурации добавить их в контекст.
Вместе с фреймворком идет большое количество готовых аннотаций, покрывающих 99% потребностей разработчика (которые будут описаны ниже в статье). Но что, если нужно обработать какую-то специфичную ситуацию. Для этого в спринг можно добавить свою, кастомную логику.
Предположим, что у нас есть некоторый бин — SuperDBLogger, который мы хотим создавать, только в случае, если над каким-либо нашим бинам есть аннотация @Loggable. Как это будет выглядеть в коде:
@Component
@ConditionalOnLoggableAnnotation
public class SuperDBLogger …
Рассмотрим как устроена аннотация @ConditionalOnLoggableAnnotation:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnLoggableAnnotation.class)
public @interface ConditionalOnLoggableAnnotation {
}
Никакие более параметры нам не нужны, теперь перейдем к самой логике — содержимому класса OnLoggableAnnotation. В нем мы переопределяем метод matches, в котором реализуем поиск помеченных бинов в нашем пакете.
public class OnLoggableAnnotation implements Condition {
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ClassPathScanner scanner = new ClassPathScanner();
scanner.addIncludeFilter(new AnnotationTypeFilter(Loggable.class));
Set bd = scanner.findInPackage("ru.habr.mybeans");
if (!bd.isEmpty())
return true;
return false;
}
}
Таким образом, мы создали правило, по которому теперь Spring создает SuperDBLogger. Для приверженцев SpringBoot создатели фреймворка создали SpringBootCondition, который является наследником Condition. Он отличается сигнатурой переопределяемого метода:
public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);
То есть помимо ответа нужен нам бин или нет, можно добавить сообщение, которое потом можно будет увидеть в логах спринга, поняв по какой причине бин был создан или нет.
Для создания более сложных условий можно комбинировать различные условия, эти механизмы предоставляют классы AnyNestedCondition, AllNestedConditions и NoneNestedConditions. Допустим, мы хотим создать два условия, чтобы при выполнение одного из них наш бин создавался. Для этого создадим свой класс и наследуем его от AnyNestedCondition.
public class AnnotationAndPropertyCondition extends AnyNestedCondition {
public AnnotationAndPropertyCondition() {
super(REGISTER_BEAN);
}
@ConditionalOnProperty(value = "db.superLogger")
static class Condition1 {}
@ConditionalOnLoggableAnnotation
static class Condition2 {}
}
Класс не нужно дополнительно помечать какими нибудь аннотациями, спринг сам найдет его и правильно обработает. От пользователя требуется только указать на каком этапе конфигурации выполняются условия: ConfigurationPhase.REGISTER_BEAN — при создании обычных бинов, ConfigurationPhase.PARSE_CONFIGURATION — при работе с конфигурациями (то есть для бинов отмеченных аннотацией @Configuration).
Аналогично и для классов AllNestedConditions и NoneNestedConditions — первый следит за выполнением всех условий, второй — за тем, что ни одно условие не выполнено.
Так же, для того, чтобы проверить несколько условий, можно передать в @Conditional несколько классов с условиями. Например, @Conditional ({OnLoggableAnnotation.class, AnnotationAndPropertyCondition.class}). Оба должны вернуть true, чтобы условие выполнялось и бин создался.
Как я упоминал выше, со спрингом идет уже множество готовых решений, которые представлены в таблице ниже.
Все их можно применять совместно над одним определением бина.
Таким образом, @Conditional представляет из себя довольно мощный инструмент по конфигурированию контекста, позволяя делать приложения еще более гибкими. Но стоит учитывать тот факт, что использовать данную аннотацию нужно аккуратно, так как поведение контекста становится не таким очевидным как при использовании профилей — при большом количестве сконфигурированных бинов можно быстро запутаться. Желательно тщательно документировать и логировать ее применение в своем проекте, иначе поддержка кода вызовет трудности.