Хабрахабр

Boot yourself, Spring is coming (Часть 1)

Евгений EvgenyBorisov Борисов (NAYA Technologies) и Кирилл tolkkv Толкачев (ЦИАН, Твиттер) рассказывают о самых важных и интересных моментах Spring Boot на примере стартера для воображаемого Железного банка.

Под катом — видео и текстовая расшифровка доклада.
В основе статьи — доклад Евгения и Кирилла с нашей конференции Joker 2017.

Конференцию Joker спонсируют много банков, поэтому представим, что приложение, на котором мы будем изучать работу Spring boot и создаваемый нами стартер, связано именно с банком.

Обычный банк просто переводит деньги туда-сюда. Итак, предположим, поступил заказ на некое приложение от «Железного банка Браавоса». Например, так (для этого у нас предусмотрен API):

Может быть, он не переживет зиму и возвращать будет некому. http://localhost:8080/credit\?name\=Targarian\&amount\=100

А в «Железном банке» перед тем, как перевести деньги, необходимо, чтобы API банка просчитал, сможет ли человек их вернуть. Поэтому там предусмотрен сервис, который проверяет надежность.

Например, если мы попытаемся перевести деньги Таргариену, операция будет одобрена:

А вот если Старку, то нет:

А зачем переводить деньги, если человек не переживет зиму? Ничего удивительного: Старки слишком часто умирают.

Посмотрим, как это выглядит внутри.

@RestController
@RequiredArgsConstructor
public class IronBankController return format( "<i>Credit approved for %s</i> <br/>Current bank balance: <b>%s</b>", name, resultedDeposit ); } @GetMapping("/state") public long currentState() { return moneyDao.findAll().get(0).getTotalAmount(); }
}

Это обычный стринговый контроллер.

Простая строчка: если тебя зовут Старк, точно не выдаем. Кто отвечает за логику выбора, кому выдавать кредит, а кому — нет? Обычный банк. В остальных случаях — как повезет.

@Service
public class NameBasedProphetService implements ProphetService { @Override public boolean willSurvive(String name) { return !name.contains("Stark") && ThreadLocalRandom.current().nextBoolean(); }
}

Все остальное не так интересно. Это какие-то аннотации, которые делают за нас всю работу. Все очень быстро.

Контроллер — только один. Где же тут все основные конфигурации? В Dao — вообще пустой интерфейс.

public interface MoneyDao extends JpaRepository<Bank, String> {
}

В сервисах — только сервисы переводов и предсказаний, кому можно выдавать. Директории Conf нет. По сути у нас есть только application.yml (список тех, кто возвращает долги). И main — самый обычный:

@SpringBootApplication
@EnableConfigurationProperties(ProphetProperties.class)
public class MoneyRavenApplication { public static void main(String[] args) { SpringApplication.run(MoneyRavenApplication.class, args); }
}

Так где же спрятана вся магия?

Поэтому Spring Boot решает эти проблемы за нас. Дело в том, что разработчики не любят думать о зависимостях, настраивать конфигурации, особенно если это XML конфигурации, и думать о том, как запускается их приложение. Нам же остается только задача написать приложение.

Зависимости

Первая проблема, которая у нас всегда была — это конфликт версий. Каждый раз, когда мы подключаем разные библиотеки, которые ссылаются на другие библиотеки, появляются конфликты зависимостей. Каждый раз, когда я читал в интернете, что мне надо добавить какой-нибудь entity-manager, возникал вопрос, а какую версию мне надо добавить, чтобы она ничего не сломала?

Spring Boot решает проблему конфликтов версий.

Как мы обычно получаем проект Spring Boot (если мы не пришли в какое-то место, где он уже есть)?

  • либо заходим на start.spring.io, ставим чек-боксы, которые нас Josh Long учил ставить, нажимаем на Download Project и открываем проект, где уже все есть;
  • либо используем IntelliJ, где благодаря появившейся опции галочки в Spring Initializer можно прямо оттуда проставить.

Если мы работаем с Maven, то в проекте будет pom.xml, где есть родитель Spring Boot-а, который называется spring-boot-dependencies. Там и будет огромный блок dependency-менеджмента.

Буквально два слова. Я сейчас не буду вдаваться в подробности Maven-а.

Это блок, при помощи которого можно указать версии на случай, если эти зависимости будут нужны. Блок dependency-менеджмента не прописывает зависимости. Т.е. И когда вы указываете в блоке dependency-менеджмента какую-то зависимость, не указав версию, то Maven начинает искать, а нет ли в parent pom-е или где-то еще блока dependency-менеджмента, в котором эта версия прописана. А если она не указана в parent-е, то она точно не создаст ни с кем никакого конфликта. в своем проекте, добавляя новую зависимость, я уже не буду указывать версию в надежде, что она указана где-то в parent-е. У нас в dependency-менеджменте указаны добрые пять сотен зависимостей, и они все согласованы между собой.

Проблема в том, что в моей компании, например, есть свой parent pom. Но в чем проблема? Если я хочу использовать Spring, как мне быть с моим parent pom?

Мы хотим использовать свой pom, а блок dependency-менеджмента получить со стороны. Множественного наследования у нас нет.

Достаточно прописать блоке dependency-менеджмента импорт BOM’а. Это сделать можно.

<dependencyManagement> <dependencies> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Brussels-SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies>
</dependencyManagement>

Кто хочет узнать подробнее про bom — смотрите доклад "Maven против Gradle". Там все это подробно объяснялось.

И это называется bom. Сегодня среди больших компаний достаточно модно стало писать такие огромные блоки dependency-менеджмента, где они указывают все версии своих продуктов и все версии продуктов, которые используют их продукты, и которые не конфликтуют друг с другом. Эту штуку можно импортировать в ваш блок dependency-менеджмента без наследования.

А вот так это делается в Gradle (как обычно, то же самое, только проще):

dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE' }
}

Теперь давайте поговорим про сами зависимости.

Dependency-менеджмент — хорошо, но мы хотим, чтобы у приложения были определенные способности, например, чтобы оно отвечало по HTTP, чтобы была БД или поддержка JPA. Что мы пропишем в приложении? Я хочу работать с БД и начинается: transaction менеджер какой-то нужен, соответственно нужно модуль spring-tx. Поэтому все, что нам нужно сейчас — это получить три зависимости.
Раньше это выглядело так. Я все настраиваю через Spring, значит нужен spring core. Мне нужен какой-нибудь hibernate, поэтому требуется EntityManager, hibernate-core или еще что-то. То есть для одной простой вещи надо было думать о десятке зависимостях.

Идея стартера заключается в том, что мы ставим зависимость на него. Сегодня у нас есть стартеры. Например, если это стартер security, то вы не думаете о том, какие нужны зависимости, они сразу прилетают в виде транзитивных зависимостей к стартеру. Начнем с того, что он агрегирует те зависимости, которые нужны для того мира, из которого он пришел. Или если вы работаете со Spring Data Jpa, то ставите зависимость на стартер, и он принесет все модули, которые нужны, чтобы работать со Spring Data Jpa.

pom у нас выглядит следующим образом: содержит только те 3-5 зависимостей, которые нам нужны: Т.е.

'org.springframework.boot:spring-boot-starter-web' 'org.springframework.boot:spring-boot-starter-data-jpa' 'com.h2database:h2'

С зависимостями разобрались, все стало проще. Думать нам теперь нужно меньше. При этом нет конфликта, а количество зависимостей уменьшилось.

Настройка контекста

Поговорим про следующую боль, которая у нас была всегда, — настройка контекста. Каждый раз, когда мы начинаем с нуля писать приложение, на то, чтобы настроить всю инфраструктуру, уходит куча времени. Мы прописывали либо в xml, либо в java config-ах очень много так называемых инфраструктурных бинов. Если мы работали с hibernate, нам нужен был бин EntityManagerFactory. Много инфраструктурных бинов — и transaction manager, и data source, и т.п. — нужно было настраивать руками. Естественно, все они попадали в контекст.

Если же мы строили контекст через AnnotationConfigApplicationContext, туда попадали некоторые beanpostprocessor-ы, которые умели настраивать бины согласно аннотациям, но контекст тоже был практически пустой.
А вот сейчас в main-е есть SpringApplication.run и не видно никакого контекста: В ходе доклада "Spring-потрошитель" мы в main-е создавали контекст, и если это был xml-ный контекст, он изначально был пустой.

@SpringBootApplilcation
class App { public static void main(String[] args) { SpringApplication.run(App.class,args); }
}

Но на самом деле контекст у нас есть. SpringApplication.run возвращает нам какой-то контекст.

Раньше было два варианта:
Это совершенно нетипичный кейс.

  • если это desktop-приложение, прямо в main-е руками требовалось писать new, выбирать ClassPathXmlApplicationContext и т.д.
  • если мы работали с Tomcat, то там присутствовал диспетчер сервлета, который по неким конвенциям искал XML и по умолчанию строил из него контекст.

Иными словами, контекст так или иначе был. И мы все равно на вход передавали какие-то классы конфигурации. По большому счету мы выбирали тип контекста. Теперь же у нас только SpringApplication.run,  он принимает конфигурации в качестве аргументов и конструирует контекст

Загадка: что мы можем туда передать?

Дано:

RipperApplication.class

public… main(String[] args) { SpringApplication.run(?,args);
}

Вопрос: что еще можно туда передать?

Варианты:

  1. RipperApplication.class
  2. String.class
  3. "context.xml"
  4. new ClassPathResource("context.xml")
  5. Package.getPackage("conference.spring.boot.ripper")

Ответ

Как минимум, это скомпилируется и будет как-то работать.
Ответ:
Документация говорит, что передать туда можно все, что угодно.

на самом деле все ответы верны. Т.е. Но это отдельная история. Любой из них можно заставить работать, даже String.class, и в каких-то условиях даже ничего не придется делать, чтобы это заработало.

Но это уже из области тайного знания. Единственное, что в документации не сказано, это в каком виде нам туда передавать.

SpringApplication.run(Object[] sources, String[] args)
# APPLICATION SETTINGS (SpringApplication)
spring.main.sources= # class name, package name, xml location
spring.main.web-environment= # true/false
spring.main.banner-mode=console # log/off

По-настоящему важен здесь SpringApplication — далее по слайдам он у нас будет Карлсоном.

Напоминаю, передаем мы ему, например, такие пять замечательных вариантов, которые все можно заставить работать с помощью SpringApplication.run: Наш Карлсон создает какой-то контекст на основе входных данных, которые мы ему передаем.

  • RipperApplication.class
  • String.class
  • "context.xml"
  • new ClassPathResource("context.xml")
  • Package.getPackage("conference.spring.boot.ripper")

Что же делает для нас SpringApplication?

Когда мы в main-е сами через new создавали контекст, у нас было очень много разных классов, которые имплементируют интерфейс ApplicationContext:

А какие варианты есть, когда контекст строит Карлсон?

Он делает только два вида контекста: либо web-контекст (WebApplicationContext), либо дженерик-контекст (AnnotationConfigApplicationContext).

Выбор контекста основывается на наличии в classpath двух классов:

Чтобы построить контекст, мы можем указать все варианты конфигураций. То есть количество конфигураций не стало меньше. То есть у меня есть все возможности. Для построения контекста я могу передать groovy-скрипт или xml; могу указать, какие пакеты просканировать или передать класс, помеченный какими-то аннотациями.

Мы еще ни одного бина не создали, ни одного класса не написали, у нас есть только main, а в нем — наш Карлсон — SpringApplication.run. Однако это Spring Boot. На вход он получает класс, помеченный какой-то Spring Boot аннотацией.

Если в этот контекст заглянуть, что там будет?

В нашем приложении после подключения пары стартеров было 436 бинов.

Почти 500 бинов только для того, что начать писать.

Далее мы поймем, откуда взялись эти бины.

Но в первую очередь мы хотим сделать так же.

Подключили бы 10 стартеров, бинов было бы сильно больше 1000, потому что каждый стартер, кроме зависимостей, уже приносит конфигурации, в которых прописаны какие-то необходимые бины. Магия стартеров, кроме того, что они нам решили все проблемы с зависимостями, заключаются в том, что мы подключили всего 3-4 стартера, и у нас 436 бинов. вы сказали, что хотите стартер для веба, значит нужен диспатчер сервлет и InternalResourceViewResolver. Т.е. Все эти бины эти уже где-то в конфигурациях стартеров прописаны, и они магическим образом приходят в приложение без каких-то действий с нашей стороны. Подключили стартер jpa — нужен EntityManagerFactory бин.

Чтобы понять, как это работает, мы сегодня будем писать стартер, который тоже будет приносить во все приложения, которые этим стартером будут пользоваться, инфраструктурные бины.

Железный закон 1.1. Всегда посылай ворона

У Железного банка есть много разных приложений, запущенных в разных филиалах. Давайте посмотрим на требование от заказчика. Заказчики хотят, чтобы каждый раз, когда поднимается приложение, посылался ворон — информация о том, что приложение поднялось.

Будем писать стартер, чтобы все приложения Iron Bank, которые будут зависеть от этого стартера, смогли автоматически посылать ворона. Начнем писать код в приложении конкретного Железного банка (Iron bank). А главное, мы не пишем практически никакой конфигурации. Мы помним, что стартеры позволяют нам автоматически подтягивать зависимости.

Будем слушать ContextRefreshEvent. Мы делаем listener, который слушает, что контекст обновился (последний event), после чего отправляет ворона.

public class IronListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { System.out.println("отправляем ворона..."); }
}

Пока там будет только listener, но завтра заказчик попросит еще какие-то инфраструктурные штуки, и мы их тоже пропишем в этой конфигурации. Пропишем listener в конфигурации стартера.

@Configuration
public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); }
}

Возникает вопрос: как сделать так, чтобы конфигурация нашего стартера автоматически подтянулась во все приложения, которые этим стартером пользуются?

На все случаи жизни есть «enable что-то».

А если у стартера несколько конфигураций? Неужели, если я буду зависеть от 20 стартеров, мне придется ставить @EnableЧтоТоТам для каждого? Главный конфигурационный класс будет весь увешан @Enable*, как новогодняя елка?

Хочу подключать стартер (чтобы все заработало), и ничего не знать о том, как называются его внутренности. На самом деле я хочу получить некую инверсию контроля на уровне зависимостей. Поэтому мы будем использовать spring.factories.

Итак, что такое spring.factories

В документации написано, что есть такой spring.factories, в котором нужно указать соответствие интерфейсов и того, что нужно по ним подгрузить — наших конфигураций. И все это волшебным образом появится в контексте, при этом на них отработают различные условия.

Таким образом мы получаем инверсию контроля, что нам и нужно было.

Вместо того, чтобы обращаться к кишкам стартера, который я подключил (эту конфигурацию взять, и эту…), все будет ровно наоборот. Попробуем реализовать. В этом файле мы прописываем, какая конфигурация у этого стартера должна быть активизирована у всех, кто его подгрузил. У стартера будет файл, который называется spring.factories. Чуть позже я объясню, как именно это работает в Spring Boot — в какой-то момент он начинает сканировать все jar-ы и искать файл spring.factories.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ironbank.IronConfiguration

@Configuration
public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); }
}

Теперь все, что нам остается сделать, это подключить стартер в проекте.

compile project(‘:iron-starter’)

В maven аналогично — нужно прописать  dependency.

Ворон должен взлететь в тот момент, когда оно поднимется, хотя в самом приложении мы ничего не сделали. Запускаем наше приложение. Но с точки зрения разработчика мы просто подключили зависимость и появилась конфигурация — ворон полетел. С точки зрения инфраструктуры мы, конечно, написали и сконфигурировали стартер. Все, как мы и хотели.

Инверсия контроля не должна быть магией. Это не магия. Мы знаем, что это фреймворк в первую очередь для inversion of control. Также, как не должно быть магией использование Spring. Как есть инверсия контроля для вашего кода, так есть и инверсия контроля для модулей.

@SpringBootApplication всему голова

Вспомните момент, когда мы руками строили контекст в main. Мы писали new AnnotationConfigApplicationContext и передавали туда на вход какую-то конфигурацию, которая была классом java. Сейчас мы тоже пишем  SpringApplication.run и передаем туда класс, который является конфигурацией, только он помечен другой довольно мощной аннотацией @SpringBootApplication, которая несет за собой целый мир.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication { …
}

Во-первых, внутри там стоит @Configuration, то есть это конфигурация. Там можно написать @Bean и как обычно прописывать бины.

По умолчанию он сканирует абсолютно все пакеты и подпакеты. Во-вторых, над ним стоит @ComponentScan. Соответственно, если вы в том же пакете или в его подпакетах начинаете создавать сервисы — @Service, @RestController — они автоматически сканируются, поскольку процесс сканирования запускает ваша главная конфигурация.

Он просто собрал все best practice, которые были в приложениях на Spring, благодаря чему это теперь некоторая композиция аннотаций, в том числе и @ComponentScan. На самом деле @SpringBootApplication не делает ничего нового.

Именно этот класс я прописывал в spring.factories.
@EnableAutoConfiguration, если разобраться, несет с собой @Import: Кроме этого тут есть еще вещи, которых не было раньше — @EnableAutoConfiguration.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({EnableAutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {};
}

Главная задача @EnableAutoConfiguration — сделать импорт, от которого мы хотели избавиться в нашем приложении, потому что его реализация должна была заставлять нас писать название какого-то класса из стартера. А узнать мы его можем разве что из документации. Но все должно быть само.

Он заканчивается на ImportSelector. Нужно обратить внимание на этот класс. Это же ImportSelector, это не конфигурация. В обычном Spring мы пишем Import(Some Configuration.class) какой-то конфигурации и она загружается, как и все зависимые от нее.  Он обрабатывает аннотацию @EnableAutoConfiguration из spring.factories, которая выбирает, какие конфигурации загрузить, и добавляет в контекст те бины, которые мы прописали в IronConfiguration. ImportSelector протаскивает все наши стартеры в контекст.

Как он это делает?

У него есть два метода, но они не сильно отличаются. В первую очередь использует незамысловатый утилитный класс, SpringFactoriesLoader, который смотрит на spring.factories и грузит все из то, что попросят.

2, просто им никто не пользовался. Spring Factories Loader существовал еще в Spring 3. И вот он перерос в Spring Boot, где есть очень много механизмов пользующейся конвенцией spring.factories. Его, видимо, написали, как потенциальное развитие фреймворка. Мы покажем далее, что еще, кроме конфигурации, можно прописывать в spring.factories — listener-ы, необычные процессоры и т.п.

static <T> List<T> loadFactories(
Class<T> factoryClass, ClassLoader cl
)
static List<String> loadFactoryNames(
Class<?> factoryClass, ClassLoader cl
)

Так работает инверсия контроля. Мы как бы соблюдаем open closed principle, в соответствии с которым не надо каждый раз где-то что-то менять. Каждый стартер несет очень много полезных вещей в проект (пока мы говорим только про конфигурации, которые он несет). И у каждого стартера может быть свой собственный файл, который называется spring.factories. При помощи него он рассказывает, что именно несет. А в Spring Boot есть много разных механизмов, которые умеют из всех стартеров приносить то, что рассказывают spring.factories.

Если мы пойдем изучать, как это устроено в самом Spring, как пишут люди, которые придумали всю эту схему стартеров, то увидим, что у них есть одна зависимость org.springframework.boot:spring-boot-autoconfigure, в  META-INF/spring.factories присутствует строчка с EnableAutoConfiguration, и в ней много конфигураций (последний раз, когда я смотрел, не связанных друг с другом автоконфигураций там было порядка 80). Но есть нюанс во всей этой схеме.

spring-boot-autoconfigure.jar/spring.factories</b> org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration.\
...

То есть вне зависимости от того, подключил я стартер или не подключил, когда я работаю со Spring Boot, всегда будет один из jar-ов (jar самого Spring Boot), в котором есть его личный spring.factories, где прописано 90 конфигураций. Каждая из этих конфигураций может содержать в себе множество других конфигураций, например, CacheAutoConfiguration, содержащий вот такую вещь — то, от чего мы хотели уйти:

for (int i = 0; i < types.length; i++) { Imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;

Более того, потом там статически из класса вынимается какая-то мапа, и в этой мапе захардкожены загружаемые конфигурации (которых нет в этом spring.factories). Их уже будет не так просто найти.

private static final Map<CacheType, Class<?>> MAPPINGS;
static { Map<CacheType, Class<?>> mappings = new HashMap<CacheType, Class<?>>(); mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class); mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class); mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class); mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class); mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class); mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class); mappings.put(CacheType.REDIS, RedisCacheConfiguration.class); mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class); addGuavaMapping(mappings); mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class); mappings.put(CacheType.NONE, NoOpCacheConfiguration.class); MAPPINGS = Collections.unmodifiableMap(mappings);
}

Самое интересное, что на этапе загрузки они все действительно будут пытаться загрузиться.

Но: Они будут пытаться.

Часть конфигураций — хорошие, добрые, правильные стартеры, которые соблюдают инверсию контроля и open closed principle — несут свои spring.factories, в которых прописаны их кишки. Подведем промежуточные итоги. Мы будем делать именно так, мы в принципе по-другому не можем сделать.

Также есть еще штук 30 конфигураций, которые просто захардкожены в Spring Boot. Кроме этого есть еще часть конфигураций, прописанных в самом Spring Boot, которые грузятся всегда — их еще 90 штук.

В конце 2013 года был доклад о том, что нового в Spring 4, где рассказывалось, что появилась аннотация @Conditional, которая дает возможность писать в свои аннотации conditions, которые ссылаются на классы, возвращающие true или false. Все это дело поднимается, а потом конфигурации начинают фильтроваться. Поскольку java-конфигурация в Spring тоже является бином, там тоже можно ставить разные conditional. В зависимости от этого бины либо создаются, либо нет. Таким образом, конфигурации считаются, но если conditional вернет false, то они будут отброшены.

В первую очередь это приводит к ситуации, в которой бин может быть, а может не быть, в зависимости от каких-то настроек окружения. Но есть нюансы.

Рассмотрим это на примере.

Железный закон 1.2. Ворон только в продакшене

У заказчика появилось новое требование. Ворон — штука дорогая, их не очень много. Поэтому запускать их надо, только если мы знаем, что поднялся продакшн.

Попробуем это сделать. Соответственно listener, который запускает ворона, должен создаваться, только если это продакшн.

Идем в конфигурацию и пишем:

@Configuration
<b>@ConditionalOnProduction</b>
public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); }
}

Как мы решаем, продакшн это или не продакшн? У меня была одна странная компания, которая говорила: «Если Windows на машине, значит не продакшн, а если не Windows, значит продакшн». У всех свои conditional.

Такой condition не предусмотрен в Spring Boot. Конкретно Железный банк сказал, что они хотят управлять этим в ручном режиме: когда поднимается сервис, должен выскакивать попап: «продакшн или нет».

@Retention(RUNTIME)
@Conditional(OnProductionCondition.class)
public @interface ConditionalOnProduction {
}

Делаем старый-добрый попап:

public class OnProductionCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return JOptionPane.showConfirmDialog(parentComponent: null, "это продакшен?") == 0; }
}

Попробуем.

Поднимаем сервис, жмем в окне yes, и ворон летит (listener создается).
Запускаем еще раз, отвечаем No, ворон не летит.

Такие кондишены можно придумывать самостоятельно, что делает приложение очень динамичным, позволяет ему работать по-разному в разных условиях. Итак, аннотация @Conditional(OnProductionCondition.class) ссылается на только что написанный класс, где есть метод, который должен вернуть true или false.

Паззлер

Итак, @ConditionalOnProduction мы написали. Можем сделать несколько конфигураций, поставить на них кондишены. Допустим, у нас есть  свой кондишн и он популярный — типа @ConditionalOnProduction. И есть, например, 15 бинов, которые нужны только в продакшене. Я их этой аннотацией пометил.

Ну может эта логика дорогая, требует времени, а время — это деньги. Вопрос: логика, которая узнает, продакшн это или нет, сколько раз должна отработать?
Какая разница, сколько отработает?

В качестве иллюстрации мы придумали такой пример:

@Configuration
@ConditionalOnСуроваяЗима
public class UndeadArmyConfiguration { ...
}
@Configuration
public class DragonIslandConfiguration { @Bean @ConditionalOnСуроваяЗима public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } ...
}

Здесь у нас есть два бина: один обычный, один конфигурационный. Оба помечены кондишн-аннотацией — они нужны, только если пришла зима.

Каждое обращение к гидрометцентру в мире Игры престолов дорогое — надо каждый раз платить деньги, чтобы узнать, какая погода. Аннотация стоит два раза.

Консистентная работа выглядит логично. Если бы это работало с кешированием, логика вызывалась бы только один раз (то есть OnProductionCondition.class вызвался бы один, один раз показалось окошко с выбором — продакшн или нет). Вдруг зима наступит за эти 5 секунд? С другой стороны, конфигурация создается в один момент времени, а другой бин может создаться через несколько секунд, когда что-то измениться.

Тут на самом деле какая-то полная дичь. Правильный ответ не очень четкий — 300 или 400. Как оно происходит — это отдельный вопрос. Мы очень долго ковырялись, чтобы сначала понять, что происходит.

Если кондишн стоит над классом сверху (класс @Component, @Configuration или @Service и вместе с ним стоит кондишн), то он отрабатывает три раза на каждый такой бин. Ситуация такая. При этом если это конфигурация прописана в стартере, то два раза.

@Configuration
@ConditionalOnСуроваяЗима
public class UndeadArmyConfiguration { ...
}

Если же бин прописан внутри конфигурации, то всегда один раз.

@Configuration
public class DragonIslandConfiguration { @Bean @ConditionalOnСуроваяЗима public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } ...
}

Поэтому данная загадка не имеет точного ответа, поскольку нужно выяснить, где прописана конфигурация. Если она прописана в стартере, то ее кондишн почему-то отработает два раза, а кондишн на бин в любом случае отработает один раз, получаем 300. Но если конфигурация прописана не в стартере, только ее кондишн трижды запустится, плюс еще один раз на бин. Получаем 400.

И ответ у меня только такой: Возникает вопрос: а как это вообще работает и почему оно так?

Важно понимать следующее: когда вы пишите свою кондишн-аннотацию, стоит в ней самостоятельно делать кэширование, причем через статический филд, чтобы логика не вызывалась много раз. Не важно, как оно работает. Потому что даже если вы этой аннотацией воспользовались один раз, логика отработает больше, чем один раз.

Железный закон 1.3. Ворон по адресу

Продолжаем развивать наш стартер. Надо как-то конкретизировать полет ворона.

Стартеры приносят конфигурацию, в которой есть бины. В каком файле мы прописываем вещи для стартера? Откуда они берут data source, user и т.п. Как эти бины настроены? Есть два варианта: application.properties и application.yml. У них, естественно, есть дефолты на все случаи жизни, но как они позволяют это все переопределять? Туда можно вписать некую информацию, которая будет еще красиво автокомплититься в IDEA.

Тот, кто им пользуется, тоже должен иметь возможность сообщить, по каким адресам лететь ворону — нам нужно сделать список получателей. Чем наш стартер хуже? Это первое.

Нам нужен дополнительный кондишн на создание listener-а, который посылает ворона. Второе — мы хотим, чтобы листенер не создавался и ворон не отсылался, если человек не прописал у себя адресатов. сам стартер нужен, потому что в нем может быть много разных вещей, помимо ворона. Т.е. Но если не написано, куда ворон должен летать, тот просто не создается.

И третье — мы тоже хотим автокомплит, чтобы люди, которые к себе подтянули наш стартер, получили комплит на все свойства, которые считывает стартер.

Но в первую очередь нужно посмотреть на существующие аннотации. Для каждого их этих заданий у нас есть свой инструмент. Может быть нас что-то устроит?

@ConditionalOnBean
@ConditionalOnClass
@ConditionalOnCloudPlatform
@ConditionalOnExpression
@ConditionalOnJava
@ConditionalOnJndi
@ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnNotWebApplication
@ConditionalOnProperty
@ConditionalOnResource
@ConditionalOnSingleCandidate
@ConditionalOnWebApplication
...

И действительно, тут есть штуки, которые нам помогут. В первую очередь @ConditionalOnProperty. Это кондишн, который срабатывает, если есть определенное property или property с каким-то значением, указанным в application.yml. Аналогично у нас есть @ConfigurationalProperty, чтобы сделать автокомплит.

Автокомплит

Мы должны сделать так, чтобы все property начали автокомплититься. Хорошо бы, чтобы это автокомплитилось не только у людей, которые в своем application.yml будут их прописывать, но и в нашем стартере.

Он должен знать, куда лететь. Назовем наше property «ворон».

@ConfigurationProperties("ворон")
public class RavenProperties { List<String> куда;
}

IDEA нам сообщает, что здесь что-то не так:

Просто добавим ее в нужный проект. В документации написано, что у нас не добавлена зависимость (в Maven была бы не отсылка к документации, а кнопочка «добавить зависимость»).

subproject { dependencies { compileOnly 'org.springframework.boot:spring-boot-configuration-processor' compile 'org.springframework.boot: spring-boot-starter' }
}

Теперь по мнению IDEA, у нас все есть.

Все знают, что такое annotation processor. Объясню, что за зависимость мы добавили. Например, у Lombok есть свой annotation processor, который на этапе компиляции генерит кучу много полезного кода — сеттеры, геттеры. В упрощенном виде это такая штука, которая на этапе компиляции может что-то делать.

Есть JSON-файл, с которым IDEA умеет работать. Откуда берется автокомплит на property, которые находятся в application properties? Если вы хотите, чтобы property, которые вы придумали для стартера, IDEA тоже могла автокомплитить, у вас есть два пути: В этом файле описаны все property, которые IDEA должна уметь автокомплитить.

  • вы можете сами вручную залезть в этот JSON и там в определенном формате их добавить;
  • вы можете подтянуть annotation processor из Spring Boot, который умеет этот кусок JSON-а генерить сам на этапе компиляции. Какие именно properties надо туда добавить, определяет магическая аннотация Spring Boot, которой мы можем маркировать классы, являющиеся property holder. На этапе компиляции annotation processor Spring Boot находит все классы, помеченные @ConfigurationalProperties, считывает с них название property, и генерит JSON. В итоге все, кто будет зависеть от стартера, получит этот JSON в подарок.

Еще нужно не забыть @EnableConfigurationProperties, чтобы этот класс появился внутри вашего контекста как бин.

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction public RavenListener ravenListener() { return new RavenListener(); }
}

Выглядит все не очень, но так нужно делать, чтобы он появлялся чуть раньше, чем остальные бины (потому что остальные бины используют его property, чтобы настраивать себя).

В итоге надо было поставить две аннотации:

  • @EnableConfigurationProperties, указав, чьи свойства;
  • @ConfigurationalProperties, рассказав, какой префикс.

И еще надо не забыть геттеры и сеттеры. Они тоже важны, иначе ничего не работает — action не лезет.

Но вручную писать никто не любит. В результате мы имеем файл, который можно в принципе написать вручную.

{ "hints": [], "groups": [ { "sourceType": "com.ironbank.RavenProperties", "name": "ворон", "type": "com.ironbankRavenProperties" } ], "properties": [ { "sourceType": "com.ironbank.RavenProperties", "name": "ворон.куда", "type": "java.util.List<java.lang.String>" } ]
}

Адрес для ворона

Мы сделали первую часть задачи — у нас появились какие-то properties. Но к этим properties пока еще никто не относится. Теперь их надо поставить как condition для создания нашего listener.

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty("ворон.куда") public RavenListener ravenListener() { return new RavenListener(); }
}

Мы добавили еще один кондишн — ворон должен создаваться только при условии, что где-то кто-то рассказал, куда лететь.
Теперь пропишем, куда лететь, в application.yml.

spring: application.name: money-raven jpa.hibernate.ddl-auto: validate
ironbank: те-кто-возвращают-долги: - Ланистеры
ворон: куда-лететь: браавос, главный банк вкл: true

Осталось в логике прописать, чтобы он летит туда, куда ему сказали.

В новом Spring есть constructor injection — это рекомендуемый путь. Для этого мы можем сгенерировать конструктор. Я же люблю следовать тем конвенциям, которые предлагает Spring: Евгений любит делать @Autowired, чтобы все оказывалось в приложении через reflection.

@RequiredArgsConstructor
public class RavenListener implements ApplicationListener<ContextRefreshedEvent>{ private final RavenProperties ravenProperties; @Override public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.getКуда().forEach(s -> { System.out.println("отправляем ворона… в" + s); }); }
}

Но это не бесплатно. Вы с одной стороны получаете проверяемое поведение, с другой стороны получаете некоторый геморрой.

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty("ворон.куда") public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); }
}

Нигде не стоит никакой @Aurowired, со Spring 4.3 его можно не ставить. Если существует один единственный конструктор, он и есть @Aurowired. В данном случае используется аннотация @RequiredArgsConstructor, которая генерит единственный конструктор. Это эквивалентно такому поведению:

public class RavenListener implements ApplicationListener<ContextRefreshedEvent>{ private final RavenProperties ravenProperties; public RavenListener(RavenProperties ravenProperties) { this.ravenProperties = ravenProperties; } public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.getКуда().forEach(s -> { System.out.println("отправляем ворона… в" + s); }); }
}

Spring советует писать так или с помощью Lombok. Jurgen Holler, который с 2002 года пишет 80% кода Spring, советует ставить @Aurowired, чтобы было видно (иначе большинство людей смотрит и не видит никакого инжекшена).

public class RavenListener implements ApplicationListener<ContextRefreshedEvent>{ private final RavenProperties ravenProperties; @Aurowired public RavenListener(RavenProperties ravenProperties) { this.ravenProperties = ravenProperties; } public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.getКуда().forEach(s -> { System.out.println("отправляем ворона… в" + s); }); }
}

Чем мы заплатили за этот подход? Нам пришлось добавить RavenProperties в Java-конфигурацию. А поставил бы @Aurowired над филдом, ничего не надо было бы менять.

Мы выполнили задание, которое дало возможность пользователям нашего стартера иметь комплит в их конфигурациях, при этом у нас появился бин, который включается и отключается в зависимости от этих конфигураций. Итак, вороны отсылаются.

Железный закон 1.4. Кастомный ворон

Например, у нас есть свой черный ворон. Случается так, что нужно кастомизировать поведение стартера. А нужен белый, который курит, и мы хотим отправлять именно его, чтобы люди видели на горизонте дым.

Стартер принес мне кучу инфраструктурных бинов, и это прекрасно. Перейдем от аллегории к реальной жизни. Я полез в application properties, и чего-то там изменил, и теперь мне все нравится. Но мне не нравится, как они настроены. Т.е.мы хотим в бине, полученном от стартера, прописать data source самостоятельно. Но бывают ситуации, когда настройки настолько сложные, что проще самому прописать data source, чем пытаться разобраться в application properties. Что тогда будет?

У меня их теперь два? Я прописал что-то сам, а стартер мне принес свой data source. Или один другой задавит (и какой?)?

Как оказалось, это совсем нетривиально. Мы хотим продемонстрировать вам еще один кондишн, позволяющий стартеру приносить какой-то бин, только если у человека, использующего стартер, такого бина нет.

Есть огромное количество кондишенов, которые уже сделали до нас:

@ConditionalOnBean
@ConditionalOnClass
@ConditionalOnCloudPlatform
@ConditionalOnExpression
@ConditionalOnJava
@ConditionalOnJndi
@ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnNotWebApplication
@ConditionalOnProperty
@ConditionalOnResource
@ConditionalOnSingleCandidate
@ConditionalOnWebApplication
...

В принципе, @ConditionalOnMissingBean тоже есть, поэтому просто воспользуемся готовым. Перейдем в конфигурацию, где укажем, что она должна создаваться, только если такого бина до нас никто не создавал.

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty("ворон.куда") @ConditionalOnMissingBean</b> public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); }
}

Если вы откроете большинство стартеров, то вы увидите, что там каждый бин, каждая конфигурация обвешана такой пачкой аннотаций. Мы просто пытаемся сделать аналог.

При попытке запуска ворон не отправился, зато появился Event, который мы написали в нашем новом listener — MyRavenListener.

Здесь есть два важных момента.
Первый момент — мы заэкстендились от нашего существующего listener, а не написали какой-то там listener:

@Component
public class MyRavenListener implements ApplicationListener { public MyRavenListener(RavenProperties ravenProperties) { super(ravenProperties); } @Override public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.getКуда().forEach(s -> { System.out.println("event = " + event); }); }
}

Второе — мы сделали его именно с помощью компонента. Если бы мы сделали его в Java-конфигурации, т.е. прописали бы этот же класс, как бин конфигурации, у нас ничего бы не заработало.

Но т.к. Если я уберу extends и сделаю просто какой-то application listener, то @ConditionalOnMissingBean работать не будет. Выше мы акцентировали внимание на том, что имя бина в Java-конфигурации будет по названию метода. класс называется также, при попытке его создать мы можем написать ravenListener — также как у нас было в нашей конфигурации. И в этом случае у нас создается бин, который называется ravenListener.

Со Spring Boot все обычно супер, но только вначале. Зачем вам все это надо знать? Какие-то вещи вы начинаете писать руками, потому что даже самый лучший стартер не даст то, что вам нужно. Когда проект продвигается, появляется один стартер, второй, третий. Поэтому хорошо, если у вас будет хотя бы общее представление о том, как можно сделать так, чтобы один бин не создавался, и как прописать бин у себя, чтобы стартер не принес конфликта (или у вас есть два стартера, которые приносят один и тот же бин, чтобы они не конфликтовали между собой). И начинаются конфликты бинов. Чтобы решить конфликт, я пишу свой бин, который сделает так, что ни первый, ни второй не создастся.

Если же мы указали одинаковые имена бинов, у нас конфликта не будет. Более того, конфликт бинов — это хорошая ситуация, поскольку вы его видите. И вы будете долго разбираться, где же там то, что было. Один бин просто затрет другой. Правда потом если стартер в какой-то версии изменит название метода, то всё, у вас бина опять станет два. Например, если мы сделаем какой-нибудь dataSource @Bean, он перезатрет существущий dataSource @Bean.
Кстати, если стартер несет то, что вам не надо, просто сделайте бин с таким же ID и все.

ConditionalOnPuzzler

У нас есть @ConditionalOnClass, @ConditionalOnMissingBean, там можно писать классы. В качестве примера рассмотрим конфигурацию казни.

Есть стул и ток — логично сажать человека на стул. Есть мыло, есть веревка — вешаем на виселице. Есть гильотина и хорошее настроение — значит нужно рубить головы.

@Configuration
public class КонфигурацияКазни { @Bean @ConditionalOnClass({Мыло.class, Веревка.class}) @ConditionalOnMissingBean({ФабрикаКазни.class}) public ФабрикаКазни виселицы() { return new ФабрикаВиселиц("..."); } @Bean @ConditionalOnClass({Стул.class, Ток.class}) @ConditionalOnMissingBean({ФабрикаКазни.class}) public ФабрикаКазни cтулья() { return new ФабрикаЭлектрическихСтульев("вж вж"); } @Bean @ConditionalOnClass({Гильотина.class, ХорошееНастроение.class}) @ConditionalOnMissingBean({ФабрикаКазни.class}) public ФабрикаКазни гильотины() { return new ФабрикаГильотин("хрусть хрусть"); } }

Как же мы будем казнить?

Вопрос: Как вообще аннотации типа @ConditionalOnMissingClass могут работать?

Но бин из виселицы должен создаться, только если есть мыло и веревка. Предположим, у меня есть метод, который будет создавать виселицу. Как мне понять, что нет именно мыла или нет именно веревки. А мыла нет. Могу ли я считать такие аннотации? Что произойдет, если я попытаюсь считать аннотации с метода, а эти аннотации ссылаются на классы, которых нет?

Варианты ответа:

  • ClassDefNotFound? Компилируют это в тот момент, когда все классы есть. Поэтому если у вас что-то исчезнет, будет ClassDefNotFound в рантайме, когда эта конфигурация будет считываться и reflection-ом вы будете получать массив, переданный в conditional as long as;
  • Так вообще нельзя, не компилируется. Наш reflection разорвет от такого поведения. Мы просто не будем знать, что произошло.
  • Будет работать;
  • Будет отлично работать.

Ответ

Действительно с помощью reflection это считать нельзя. Ответ: работать будет, но не отлично. Как работает reflection? У вас будет exception, и вы не будете знать, нет ли у вас мыла или веревки — что там вообще случилось. Если вы попросите у метода все его аннотации, и хотя бы одна из аннотаций, которые над методом стоят, ссылается на отсутствующий класс, ничего вы не получите — у вас ClassDefNotFound.

Это будет работать с помощью ASM. Увидев, что через reflection — никак, Spring будет парсить байт-код, условно, вручную. Он считывает файл, чтобы не делать преждевременную загрузку этого файла, и понимает, что там есть @Conditional с мылом, веревкой. Он уже может проверить наличие этих классов в контексте отдельно. Но ASM — это, как говориться, не про скорость. Это возможность считать класс, не загрузив его, и понять методную информацию.

Если следовать этой рекомендации, все работает быстрее, и ASM не нужен. Но Juergen Hoeller рекомендует не завязываться на имена классов, прописывая кондишены, несмотря на то, что есть аннотация OnMissingClass, которая в качестве параметра может принимать название класса (String). Но, судя по исходникам, никто так не делает.

Железный закон 1.5. Включаем и отключаем ворона

Чтобы вообще никого не посылать гарантированно. Нам понадобилась еще одна property — возможность включить или отключить ворона вручную. Это последний кондишн, который мы вам покажем.

Поэтому вы можете спросить, зачем иметь возможность его включать / выключать, можно же его просто не брать? Наш стартер кроме ворона ничего не дает. А конкретно ворон может не понадобиться — он дорогой, его можно выключить. Но во второй части в этот стартер будут набиваться дополнительные полезные вещи. При этом убирать конечную точку, куда его отправить, как-то не очень — выглядит как костыль.

Поэтому мы все сделаем через @ConditionalOnProperty("ворон.куда").

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty("ворон.куда") @ConditionalOnProperty("ворон.вкл") public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); }
}

И он нам ругается, что так делать нельзя: Duplicate annotation. Проблема в том, что если у нас аннотация с какими-то параметрами, она не repeatable. Мы не можем сделать это по двум property.

У нас есть методы этой аннотации, там есть String, и это массив — там можно указать несколько property.

@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty { String[] value() default {}; String prefix() default ""; String[] name() default {}; String havingValue() default ""; boolean matchIfMissing() default false; boolean relaxedNames() default true;
}

И все хорошо, пока вы не пытаетесь кастомизировать значение для каждого элемента в этом массиве отдельно. У нас property вкл, которое должно быть false и есть какое-то другое property, которое должно быть string с определенным значением. Но вы можете на все property указать только одно значение.

То есть вот так делать нельзя:

@ConditionalOnProduction
@ConditionalOnProperty(name = "ворон.вкл", havingValue="true")
@ConditionalOnProperty(name = "зима.вкл", havingValue="true")
@ConditionalOnProperty(name = "старки.вкл",havingValue="false")
public IronBankApplicationListener applicationListener() { ... }

@ConditionalOnProduction
@ConditionalOnProperty( name = { "ворон.вкл", "зима.вкл", "старки.вкл" }, havingValue = "true"
)
public IronBankApplicationListener applicationListener() { ... }

Выделенное здесь — это не массив.

Есть извращение, которое позволяет работать с несколькими property, правда, только с одним значением для них: AllNestedConditions и AnyNestedCondition.

Но это работает. Выглядит это, откровенно говоря, странно. Давайте попробуем сделать конфигурацию — новый кондишн, который будет учитывать и ворон.куда и ворон.вкл.

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty("ворон.куда") @ConditionalOnRaven public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); }
}

Мы ставим нашу аннотацию @Conditional() и здесь мы должны прописать какой-то класс.

@Retention(RUNTIME)
@Conditional({OnRavenCondional.class})
public @interface CondionalOnRaven {
}

Создаем его.

public class OnRavenCondional implements Condition {
}

Дальше мы должны были имплементить какой-то кондишн, но мы можем так не делать, потому что у нас есть следующая аннотация:

public class CompositeCondition extends AllNestedConditions { @ConditionalOnProperty( name = "ворон.куда", havingValue = "false") public static class OnRavenProperty { } @ConditionalOnProperty( name = "ворон.enabled", havingValue = "true", matchIfMissing = true) public static class OnRavenEnabled { } ...
}

У нас есть композитный Conditional, который тоже будет наследоваться от другого класса — либо AllNestedConditions, либо AnyNestedCondition — и в нем будут содержаться другие классы, содержащие обычные аннотации с кондишенами. Т.е. вместо @Condition мы должны указать:

public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); }
}

При этом внутри нужно создать конструктор.

Мы делаем какой-то класс (назовем его R). Теперь мы должны создать здесь статические классы.

public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } public static class R {}
}

Делаем наше значение вкл (он должен быть именно true).

public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } @ConditionalOnProperty("ворон.куда") public static class R {} @ConditionalOnProperty(value= "ворон.вкл", havingValue = "true") public static class C {}
}

Чтобы это повторить, достаточно запомнить название класса. У Spring хорошие Java-доки. Можно не выходить из IDEA, читать Java-док и понимать, что нужно сделать.

В принципе, туда можно завернуть и @ConditionalOnProduction, и @ConditionalOnMissingBean, но сейчас мы этого делать не будем. Мы поставила наш @ConditionalOnRaven. Просто посмотрим, что у нас получилось.

@Configuration
@EnableConfigurationProperties(RavenProperties.class)
public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnRaven @ConditionalOnMissingBean public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); }
}

При отсутствии вкл ворон не должен полететь. Он и не полетел.
Я не хочу ставить вкл, потому что мы сначала должны сделать автокомплит — это одно из наших требований.

@Data
@ConfigurationalProperties("ворон")
public class RavenProperties { List<String> куда; boolean вкл;
}

Все. вкл по умолчанию false, ставим true в
application.yml:

jpa.hibernate.ddl-auto: validate ironbank: те-кто-возвращают-долги: - Ланистеры
ворон: куда: браавос, главное отделение вкл: true

Запускаем, и наш ворон летит.

Это работает на любой Java. Таким образом мы можем делать композитные аннотации и помещать в них сколько угодно уже существующих аннотаций, даже если они не repeatable. Это нас спасет.

Во второй части статьи, которая выйдет в ближайшие несколько дней, мы остановимся на профилях и тонкостях запуска приложения.

19-20 октября состоится конференция Joker 2018, на которой Евгений Борисов вместе с Барухом Садогурским выступят с докладом «Приключения Сеньора Холмса и Джуниора Ватсона в мире разработки ПО [Joker Edition]», а Кирилл Толкачев с Максимом Гореликовым представят доклад «Micronaut vs Spring Boot, или Кто тут самый маленький?». Минутка рекламы. Приобрести билеты можно на официальном сайте конференции. В целом, на Joker будет ещё множество интересных и заслуживающих пристального внимания докладов.

А ещё у нас для вас есть небольшой опрос!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть