Это конспект доклада Евгения Борисова и Кирилла Толканова про нюансы работы СпрингБута Доклад записывался в 2017году, на Спринг Буте версии 2.3 или чтото такое. Стех пор кое-что поменялось, но в основном осталось то же самое
По мере освоения материала буду коммитить себе в гитхаб ("IronBank"), но когдамы будем писать своц стартер, придется завести еще один репозиторий ("IronBank-starter"), дело в том, что нашсобственный стартерэто модуль такого же уровня абстракции, что и основное приложение. А по замыслу, moneyraven это одно из многочисленных приложений банка, но корень гита я сделал именно в нем... Можно конечно сделать парент-пом, лучше не буду на это отвлекаться, а то вместо освоения материала буду 2дня заниматься настройкой проекта. . .
Для целей доклада нам нужен демо проект, это будет простая модель банка, который выдает кредиты по запросу /get?name=user&amount=999 в рест-контроллере.
/Для целей доклада пишем прототип "железного банка" из "игры престолов" с помощью спринг-бута. Пишем очень быстро,поэтому ничего лишнего: для демонстрации нам достаточно таблицы bank (id, total_amount), это сводная таблица с отделениями и остатками на счетах. При поднятии приложения создается отделение insert into bank(..) values (0, 100500) и это отделение может выдавать кредиты (TransferMoneyService), логика одобрения кредита основана на предсказаниях (PredskazService)./
Переводом средств занимается сервис TransferMoney с методом transfer(имя адресата, сумма).
Для успешного перевода кредит должент быть одобрен. В TransferMoney это реализовано с помощью сервиса PredskazService с методом boolean willSurvive(имя адресата), по имени он вычисляет надежность заемщика. В нашей реализации надежность определяется с помощью рандома, кроме случаев когда имя находится в черном списке (у нас он состоит из одного имени "Stark")
Возвращаемся в TransferMoney, если предаказание благосклонно, то метод transfer() возвращает остаток на корр.счете банка, в случае отказа возвращается -1.
Единственная наша модель это само отделение банка с остатком на счету. Класс Банка помечен @Data и @Entity и имеет метод credit(amount) {totalAmount-=amount;}
Интерфейс MoneyDao extends JpaRepository<Bank, String>(^почему Стринг,кстати?) параметризован этим Банком. Никакой БД мы не заводили, она проинжектится вконфигах и т.д. (у нас используется H2)
Пройдем по цепочке от Контроллера ему нужен resultDeposit, который возвр TransferMoneyService.transfer()
идем в сервис переводов в transfer() там если хватает остатка на счете банка и сервис предсказаний благосклонен,то вызывается bank.credit(amount) и moneyDao.save(bank) и возвращается остаток на счете банка. Иначе возвр -1
нехватает БД, у автора сделано чрез flywaydb как-то автоматически,
todo^ надо разобраться
у него в папке resources/db/mirgation файл v1_0_Init.SQL:
CREATE TABLE bank (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
total_amount BIGINT not null
);
insert into bank (id, total_amount) values (0, 100500);
А, ну еще добавить application.properties
все равно почему-то не созлается таблица.
----было вот чё----
I actually had (which didn't work):
src/main/resources/db.migration/
instead of the correct (which worked):
src/main/resources/db/migration/
The db.migration version obviously does not work, but it is hard to spot on the IDE.
----а также-----
I had a different problem, my migration file name was V1_Base_version.sql instead of V1__Base_version.sql. Flyway requires double underscore __ in name prefix.
поправил flyway, но таблица все равно не создавалась сравнил build.gradle автора, у меня оказалась не прописан Актуатор (? что это)
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
Программисты не любят думатьо зависимостях, откроем пом у нашего пома есть родитель-спринг-бут у котокорого есть родитель Спринг-бут-депенденсис у которгого огромный блок dependencyManagement при помощи этогоблока указываются версии иесли мы укажем зависимость без версии,то мавен смотрит в перенте этот блок, где прописаны версии в этом блоке проприсано около 500 зависимостей,которые согласованы друг с другом
проблема в том,что в нашей компании есть свой parent.pom, как нам быть если мы хотим использовать его.еслимы хотим исподьщлвать Спринг? множественногонаследованиея ведь нет.
Для этоговнашем блоке dependencyManagement можно прописать импорт на так наз "bom" подробнее см доклад "Maven vs Gradle"
<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>
это сейчас модно,писать свои Бомы, особенно в крупных компаниях
//в Градле это делается так:
dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:scpring-cloud-dependencies:Daltson.RELEASE'
}
}
Теперь непосредственно о зависимостях конкретнонашего приложения: допустим, нам надо чтобы оно отвечало по http, имело пожддержку БД и JPA Раньше это было так: "мне надоработать с БД" - и начинается: transactionalManager, SpringTX, Hibernate-entityManager -core ит.д.. Потом, "нам нужен Спринг" - значит нужет СпрингORM...
Для одной простой strob надо былодумать о 3-5 зависимостях. Теперь за нас об этом думает Стартер.
Идея стартера такая (кроме всего прочего), что он агрегирует те зависимости, которые нужны длятого "мира",откуда он пришел. Если это Стартер-security, то мы не думаем о том, какие нужны зависимости для security, астартер уже сагрегировал всеэти зависимости. Тоже для data-jpa и всегопрочего. Таким образом,у нас Пом выглядит в 3-5 зависимости - точто нужно, плюс например база данных.
Итак, у нас нет конфликта зависимостей и версий. Следующая боль - настроить контекст.
Раншье мы прописывали в xml или java-конфигах много инфраструктурных бинов Например для Hibernate нужен EntityManagertFactoryBean, TransactionManager, DataSource... короче кучач разных инфраструктурных бинов...и что мы раньше делали? создавали в Мейне контекст, если из XML то он был пустой. Еслимы строили из AnnotationConfigApplicationContext, то нам попадались некоторые бины,которые могли настраивать конекст согласно аннотациям....
А в мейне, который у нас сейчас? SpringApplication.run() а где же здесь контекст? что,спринг отказался от Контекста?
первое - а зачем он нам нужен? второе - он там есть (метод run() возвражает контекст). Но вообще-то он нам особо и не нужен.
Раньшебыло как? для десктоп-приложений мы писали, например, "new ClassPathXmlApplicationCotext..." если с Томкатом и webXml, то у нас был ДиспетчерСервлет, который по каким-то конвенциям искал какой-то xml по умолчанию,Юи из него строил Контекст так и иначе.
А теперь у нас только SpringApplication.run() Он может принимать на вход разные аргументы, например:
- сапмкласс,вкотором он написан
- String.class
- "context.xml"
- new ClassPathResource("context.xml")
- Package.getPackage("com.example.boot.ripper")
ответ - можно заставить работать все варианты (и даже String.class, поплясав) вдокументации написано, что передаем Имя класса, имя Пакета, расположение xml в виде массива Обжектов.
Когда мы сами создавали контекст в Мейне, у нас было много разных классов типа "#$%&ApplicationContext".
А наш спринг-Бутовый не заморачиваетсфя, и делвает только 2 вида контекстов: Web Context и Generic Context, и решает он очень просто: если в classpath есть javax.servlet.Servlet && ConfigurableWebApplicationContext, то делает Веб (AnnotationConfigEmbeddedWebApplicationContext). Иначе - делает AnnotationConfigAnnotationContext.
Передать "старые" типы контекстов мы все-таки можем, но все равно из них построится один из этих двух вариантов. //таймкод 16:35
И что там в этом контексте-то будет? Мы не создали ни одного бина, унас только application.run(), на вход он получает тот же класс,помеченный @SpringBootApplication - что же там будет,если в этот контекст заглянуть? (^как заглянуть?) ответ, в нашем случае около 436 бинов. И это мы подключили всего пару стартеров. Микросервисная архитектура,блин! микросервис 180 мБ!
*Как заглянуть в контекст?
на презентации открыто окно Evaluate expression: run.getBeanDefinitionNames()
хм, ясчитал что в нынешней версии Idea эта возможность вырезана
но в run-debugging нашел пункт Evaluate, он неактивен. Делаю Debug-Pause, стал активным, пишу в поле Code frafment:
SpringApplication.run(MoneyRavenApplication.class, null).getBeanDefinitionNames();
Получаю "Cannot evaluate methods after Pause action", а также "Cannot evaluate, current stack frame doesn't support evaluation"...
Остается только вызвать getBeanDefinbitionNames() в main()...
Получилось 362 бина. Кто это конкретно?
Откуда взялись все эти бины? Магия этих стартеров втом, что... вот мы подключили их 4 шт, и получили поти 400. Подключили бы 10 - получилои бы больше 1000. Потому что каждый стартер, кроме зависимостей, уже приносит какие-то конфигурации,вкоторых прописаны какие-то бины, во например: Хотим например стартер для Веба. Погнали: DispatcherServlet, InternalResourceIOReolver... Хотим starter-jpa - в нем EntityManagerFactoryBean и т.д. Это всепрописано в их конфигурациях,и они приходят сами, без нашего участия. Мы сегодлня сделаем точно также, то есть мы напишем стартер, который точно также будет приносить какие-то бины во все приложения, которые этим стартером будут пользоваться.
у Железного банка много разных приложений в разных филиалах, но они хотят, чтобы каждый раз, когда поднимается приложение, посылался ворон с информацией о том, что приложение поднялось.
То есть, нам не надо писать код в приложение конкретного банка, а мы будем писать стартер, чтобы все приложения Iron банка, которые используют этот стартер, посылали ворона при их поднятии.
Делаем новый Модуль iron-starter? структура каталогов src/java/main Напишем новй класс,который будетпосылать ворона. Пусть это будет Listener, который слушает контекст,икогдаон рефрешнудся,значит надо посылать. implements ApplicationListener
Листенер это круто, но его неплохо все же прописать в какой-то конфигурации. Можно конечно @ComponentScan поставить, но для нашей задачи не подходит,т к это чужасконфигурация, конфигурация стартера. Сегоднядекларирует Листенер,завтра заказчик попросит что-нибудь другое, мы тожепропишем их в конфигурации этогшостартера.
Делаем класс IronConfiguration под аннотацией @Configuration в нем @Bean RavenListener(){return new RavenListener();}
Вопрос, а как сделать, чтобы эта конфигурация автоматически подтянулась во все приложения? В СпрингБуте мы видели штуки типа @EnableSomeStarter на все случаи жизни. Допустим, мы зависим от 20стартеров,и у нас стоят @EnableFirstStarter, @EnableSecondStarter и т.д.?vs что тдолжны ими обвешаться, как ёлка! давайте еще сделаем @import(SomeStarterConfig.class).. Нет! мы хотим сделать некую инверсию контроля, мы хотим подключая стартер иничего не знать о том, как называются его внутренности, но чтобы все работало.
Поэтому мыбудем использовать spring.factories. Что это такое? В документации написано, что есть такой волшебный файл META-INF/spring.factories, в которм указано cответствие интерфейсов, и того, что надо по ним подгрузить (наши конфигурации). И после чего они волшебным образом появятся в нашем контексте.
Таким образом мы получаем инверсию контроля. То есть тот, кто подключил стартер, вместо того, чтобы обращаться к "кишкам" и выбирать, какую взять конфигурацию, все будет наоборот. У стартера будет файл,и в нем будет прописано,какая конфигурация дложна быть активизирована у всех тех, кто его подгрузил.
создаем resources/META-INF/spring.factories в нем пишем
org.springfrmework.boot.autoconfigure.EnableAutoConfiguration=IronConfiguration
В какой-то момент СпрингБут начинает сканировать все jar'ы, и начинает искать вот этот файл spring.factories, мы это потом разберем подробнее, но пока подключим так.
теперь надо подключить, в Градле это выглядит как compile project(':iron-starter'), в Мавене надо поставить в депенденси.
Запускаем...
Тут говорят,что начиная со СпрингБут 3.0 spring.factories больше не поддерживается
***
I spent literally days on a similar issue and this is what worked for me. If you're using Spring boot 3+ they changed all this! You can find the change documented in the 2.7 release notes here: 2.7 release notes#auto-configuration. In release 2.7 they supported both the old and new ways for backwards compatibility and marked spring.factories as deprecated (deprecation notice). They removed support completely in 3.0! You can find the removal information in the 2.7 -> 3.0 migration guide here:
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#auto-configuration-files.
In essence, you're now required to annotate your top level auto-configuration class with @AutoConfiguration and add the fully qualified name to a file in this location:
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
yes this is the name of the file. The different auto-configuration classes will be separated by newlines instead of a comma-separated string like in spring.factories.
***
Короче, сделал как в руководстве https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#auto-configuration-files, и ворон полетел.
Причем мы для этого ничего не делали в нашем приложении. С точки зрения пользователя мы просто подключили зависимость, и у нас появилась конфигурация и появился новый функцилонал.
(в другом видео про СпринргБут вроде бы говорилось,что исплользовать Листенер не очень правильно, todo разорбраться)
//В связи с устареванием spring.factories этот раздел может быть не актуален
Это работает благодаря аннотации @SpringBootApplication? это аннротация мощная, которая за собой несет кучу всего ив первую очередьона несет @EnableAutoConfigurtion ...который несет всебе тот самый импорт
вспомним.какмыстроили контекст руками через new AnnotationConfig //// () и передавали на вход конфигурацию,которая была java-классом. Сейчас мы тоже пишем application.run(), в который передаем какой-то класс,который явл конфигурацией,тольк он не помечен аанотвйцией @Configuration, а он помечен @SpringBootApplication. Но ели разобраться, что такое SpringBootApplication, то востоит внутри @configuration то есть наш главный класс это еще и конфигурация,там можно прописывать бины
Во-вторых там стоит еще и @ComponentScan, который сканирует все пакеты и подпакеты. Соответственно если мы пишем сервисы или контроллеры в наше пакете,то они автоматически просканируются.
И кроме этого там есть этот сммыйц @EnableAutoConfiguration (вообще SpringBootAppllication не делает ничегонового, он просто делает все то,что раньшеделали хорошие приложения,ниписанные на спринге, благодаря тому что это теперькомпоиция аннотаций,в т.ч @ComponentScan)
Вернемся в EnableAutoConfoiguration. Именно этот класс мы прописывали в spring.factories А его главная задача это сделать вот такой @Import({EnableAutoConfogurationImportSelector}) Именно от него мы хотели избавиться в нашем приложении, чтобы получить инверсию контроля, чтобы в приложении не писать название класса, который хотим подключить (чтобы не читать документацию :)
Этот класс заканчивается на "..ImportSelector". Это не обычная конфигурация, которую мы обычно импортируем через аннотыцию @Import Вообще с помощью этой аннотации можно импортировать 3 типа - Конфигурацию, ИмпортСелектор и ...еще что-то не сильно важное
ююИэтот ImportSelector протаскивает все наши стартеры (а в итоге весь наш контекст)
Для этого он использует SpringFactoriesLoader, который ищет всякие штуки по всем jar'ам
Каждый стартер несет всякие штуки,и у каждогостартера есть свой spring.factories при помощи которогокоторогоони рассказывают,чтоу нихесть.А у Спринг бута есть механизм, который из всех стартеров приосит то,что они рассказывают в этих spring.factories
Но есть нюанс: у самого Спринг-бута есть его личный jar с его личным spring.factories, в котором есть точно такая же строчка с EnableAutoConfiguration=, и в ней уже прописаны очень много разных конфигураций. Итого это около 80 автоконфигураций, не связанных друг с другом. И это независимо от того,подключали мы чтото сами или нет.
Более того,каждая эта автоконфигурация может содердать в себе множество другихконфигурация,как например кэш автоконфигураций spring-boot-autoconfigure.jar
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration{
...
for (int i=0; i<types.length; i++){
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
более того, далее он берет это класс, статически вынимает изнего какуюто мапу, и в этой мапе захардкожены еще другие конфигурации, которых нет в spring.factories Найти их не так просто, ипри этом они все будут пытаться загрузиться, причем там есть какие-то reddis,hazzlecast и прочие устаревшие не испольхующиеся штуки...
Бороться с этим нам поможет фильтр - аннотация @Conditional
..Итак, повторим с начала,подведем итоги концепцуии Часть конфигураций - те,которые хорошие и соблюдают O/C принцип - они несут свои spring.factories, и мысвой стартер пишем также, да и не можем по другому.
кроме этого часть конфинураций прописанывсамом сринг-бвуте (около 80-90шт) Кроме этого штук 30захардкожены в коде спрингбута
...и вот всеэто поднимается, и потом уже начинают фильтроваться
См лекцию Борисова "что нового в Спринге 4" за 2013год. Ранее в Спринге 3 была аннотация @Profile c помощью которой мыне хотим чтобы некоторые бины всегда создавались. А @Conditional это еще более мощный механихм, который оченьактивно используется в СпрингБуте, который дает возможность писать свои аннотации - @Conditions которые ссылаются на какие-то классы, которые возщвращают true/false, и в зависимости от этого бин либо создается, либо нет (см ниже)
А поскольку java-конфигурация в Спринге тоже является бином,то там тоде можноставить всыкие @Conditionalы, и если они векрнуди false, то эти конфигурации считались, но после этого отбросились. Так и работает этот фильтр.
Но есть нюансы, например что бин может быть или не быть, в зависимости от настроек окружения. Разберем как роз такой пример: Заказчик просит, чтобы Ворона нужно было запускать только на продакшене. То есть,Листенер недолджен создаваться всегда, а только если у нас продакшен.
Идем в нашу конфигурацию, пишем свою аннотацию @ConditionOnProduction Создаем новую, задаем ретеншн рантайм, затем над @интерфейсом пишем @Conditional(OnProductionCondition.class), создлаем этот класс в подпакете annotation
этот класс implements Condition {
оверрайдим matches(принимает Контекст и AnnotatedTypeMetadata){
return JOptionPane.showConfirmDialog(null, "это продакшен?") == 0;
}
}
Кстати, вот так делается "стары-добрый" Попап. Правда сначала выскакивал java.awt.HeadlessException, немного погуглив нашел решение
You can also just pass the a JVM parameter when running your application, no code change required:
-Djava.awt.headless=false
Все работает, запускаем - всплывает диалог, если нажали "yes", то ворон полетел, если "No" то нет.
Вот допустим,теперь у на есть 10 бинов, и на всех них мы поставим @ConditionOnProduction Вопрос: сколько раз отработает логика, которая опредяляет, продакшен у нас или нет? Допустим, она дорогая или долгая...
Представи,что унас есть 2 бина
@Configuration
@ConditionalOnSevereWinter
public class UndeadArmyConfiguration{
...
}
и второй
@Configuration
public class DragonIslandConfiguration{
@Bean
@ConditionalOnSevereWinter
public DragonGlassFactory dragonGlassFactory(){
return new DragonGlassFactory();
}
...
}
вопрос:сколько обращений за прогнозом погоды будет, если
Варианты:
-
если результат обращения кешируется, то один раз
-
кешироваться не должен,потому что вдруг за время,пока создается второй бин, погодные условия уже изменились, тогда кондишен будет разный.. ответ - 2раза
-
3или 4 раза
-
6-10 раз
-
больше 10раз итак,правильный ответ 3 или 4 раза.
-
Если @conditional стоит над классом (первый бин из примера) то отрабатывает 3раза о_0 Но если эта конфигурация прописана в стартере,то 2 раза
-
А если этот бин прописан внутри конфигурации (бин №2 в примере) - то всегда 1 раз.
Поэтому в нашем примере надо знать,прописана ли аннотация в стартере, если да, то 2+1, если нет то 3+1
Почему так получается? Мы не будет здесь касаться этого вопроса, это слишком сложно.
ВЫВОД: Когда мы пишем свою кондишен-анноитацию,нверно надо самим делать в ней кеширование,чтоьбы логика не вызывалась много раз. . .
Вообще,стартеры приносят конфигурацию, в которой есть куча бинов, и возникает вопрос - а как они настроены? Вот например data source - какой там логин и пароль к БД? Для этого унихесть дефолты на всеслучаи жизни: не сказали какой логин, ну значит root... Но у нас есть возможность это все переопределять, например в том же applicqation.properties или application.yml (которая еще и автокомплитится), вот все это можно рассказать стартеру, и наш стартер не исключение, мы будет точно такжепробовать его настроить.
Конкретно мы будем настраивать три вещи: 1)чтобы где-то был прописан список получателей (applicqation.properties или .iml) 2)чтобы ворон не создавался, если список получателей пуст (дополнительные кондишены на соз лание листенера, посылающего ворона. Т к в стартереможет быть еще многодругих полезных вещей, кроме ворона) 3) хочется, чтобы у того, кто наш стартер подтянул, был автокомплит на все проперти, которые считывает наш стартер.
Кроме того что мы можемписать свои реализации @Conditional существует множество уже написанных кондишенолов на разные случаи жизни. Посмотрим на них повнимательнее,может кто-то нам пригодится без переделок?
@ConditionalOnBean @ConditionalOnClass @ConditionalOnCloudPlatform @ConditionalOnExpression @ConditionalOnJava @ConditionalOnJndi @ConditionalOnMissingBean @ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnProperty @ConditionalOnResource @ConditionalOnSingleCandidate @ConditionalOnWebApplication ... и другие.
задачу со списком получателей мы сможем решить, поместив его в проперти-файл или yml. задачу создания ворона только при наличии списка - @ConditionalOnProperty (проперти из п.1) задачу автокомплита поможет @ConfigurationProperties, а вот это что такое? С него и начнём.
Откуда береься автокомплит? Есть JSON файл, в котором описаны все проперти,которые Идея должна уметь автокомплитить. Если мы хотим автокомплитить свои проперти, есть 2 варианта: добавить их вручную в этот JSON, и второй вариант - использовать специальный СпрингБутовый аннотейшнПроцессор, который умеет генерить этот JSON на этапе компиляции. А узнать о том, что конкретно надо добавлять, ему поможет аннотация СпрингБута @ConfigurationProperty, которой мы можем маркировать классы-прперти-холдеры, и на этапе компиляции процессор этой аннотации СпрингБута найдет все классы,помеченные этой аннотацией, считает все эти проперти и сгененит это JSON, затем все кто зависит от нашего стартера,получат это JSON, после чегоИдея сагрегирует все эти JSON'ы, и заработает автокомплит.
пишем этот класс-проперти-холдер RavenProperties помеченный аннотацией @ConfigurationhProperty("ворон")
public class RavenProperties{
List куда;
}
для работы этой аннотации надо добавить зависимость org.springframework.boot spring-boot-configuration-processor 3.1.2 true
т к у нас нет парента, то без версии работать не будет
Кроме подключения зависимости, надо еще сделать так, чтобы этот класс появился внутри нашегоконтекста. Для этого поставим аннотацию @EnableConfigurationProperties(RavenProperties.class) над конфигурацией
Еще, по непонятной причине была циклическая зависимость в pom, устранил. Все заработало, автокомплит есть. Причем как в yml, и application.properties
Ворон должен созаваться только при уловии, что где-то кто-то рассказал, куда ему лететь. Добавим @ConditionalOnProperty над объявлением бина RavenListener в Конфиге правда, здесь он почему-то у меня не комплитится, а комплитится только в проперти-файле
Ну допишем логику ворона, чтобы он выводил, куда летит ворон. Для этого заинжектим RavenProperties.
Рекомендуемый путь это конструктор инжекшн (поставим ломбоковкую @RequiredArgsConstructor). Начиная с 4 спринга единственный конструктор инжектится автоматически., даже если не стоит @Autowired. Так как у нас создался конструктор,который принимает проперти, придется изменить конфиг бина, чтобы он передавал проперти...
Запускаем,все работает. А если нет списка, то ворон не летит. И даже ... (?)
второй путь, это сделать филд инжекшен, поставив аннотацию @Autowired. Тогда не понадобится "лишний" аргумент в конструктор. Ктому же сам Йорген Холер (автор Спринга) рекомендует всегда ставить @Autowired, даже в первом случае. Т.к. это может быть неочевидно для новых людей на проекте. Запускаем, все работает (кстати, через запятую перечислим несколько адресов)
Кстати, автор запускает прилоддерние с параметром --debug, который показываетв том числе и какие кондишены отработал, и соответственно, какие бины отфильтровались.
Стартер принес кучу бинов, это хорошо. Но нам не нравится,как они настроены. Лезем в пропертиз, чтото меняем. Но бывают такие ситуации,что у нас такие сложные настройки, что нам проще самим прописать этот датасорс, чем пытаться в пропертиз. Например, есть логига - если А то датасорс на таком порте, если Б то на другом. В пропертях это неполучится прописать, и тогда этот конкретно дотасорс мы хотим приписать сами. А стартер принес свой - что теперь будет? какой из них сработает? или может их будет сразу два?
Мы продемонстрируем еще один кондишен, который говорит,чтобы стартер приносил какой-то бин только в случае, если такого бина нету у пользователя этого стартера. И это совсем не так тривиально, как могло бы показаться.
Вернемся к нашему примеру. Допустим, мы написали нового ворона, который быстро летает и пускает дым. И теперь нам надо, чтобы стандартный ворон не создавался. Сделаем это при помощи кондишена @ConditionalOnMissingBean.
(кстати, если мы вскроем большинство стартеров,то обнаружим там,что каждый бин и каждая конфигурация обвешана пачкой аннотаций-кондишенов на все случаи жизни)
Напишем свой листенер в основном проекте (правда тут получается зависимость на стартер..?) Вернемся в конфиг и скажем, что наш ворон должен создаваться только если такого бина до нас никто не создавал - поставив @ConditionalOnMissingBean
Запускаем и видим, что создался наш новый ворон
Но тут есть 2 момента: во-первых, мы экстендились от нашего существующего листенера, а не написали implements ApplicationListener
И второе - это что мы использовали @Component. Если же мы пропишем наш класс как бин в какой-н java-конфигурации, то не заработает.
Попробуем, проипишем в нашем главном классе приложенгия @Bean public MyRavenListener myRavenListener(){ return new MyRavenListener(); }
Но есть нюанс - это может заработать при условии, что мы попадем в название. Потому что если айдишник у этого бина будет такой же, то тогда....вернемся к этому вопросу позже.
Остается первая проблема: если в нашем MyRavenListener мы вместо extends имплементируем то же что первый листенер, то наш ConditionObBean работать не будет. Но. Мы можем при попытке его создать написать не myRavenListener, а просто ravenListener - то есть точно также, как в нашей конфигурации. А мы помним из доклада "Спринг-потрошитель", что в случае java-конфигурации имя бина будет браться из названия метода. И вэтом случае у нас создается бин с айдишником ravenListener.. То какие бы кондишены там не стояли,хоть @Bean String...
На сам деле зачем вообще все это нам надо знать? потому что вот вначале все здорово работает, никаких конфигураций, никаких зависимостей, тяп-ляп и в продакшкен. Но когда проект продвигается, у вас один стартер, 2й стартер, 3й стартер, какие-то вещи вы все-таки начинаете писать свои, потому что даже самый лучше стартер не даст то, что именно вам нужно. И у вас начинаются всякие вот эти вот конфликты бинов. И поэтому хорошо чтобы у вас имелось хотя бы общее представление о том, как можно сделать так, чтобы один бин не создавался, и как вы должны прописать бин для того чтобы стартер вам не принес что-то. Или у меня есть 2 стартера, которые приносят один и тот же бин, и они конфликтуют между собой, и чтобы решить их конфликт я пишу свой бин, который сделает так что ни тот ни тот не создастся...вот это все об этом.
Более того, конфликт бинов это самая хорошая ситуация. Вы увидели конфликт, он есть. В случае если мы вот здесь указали одинаковые имена бинов, у нас конфликта не будет - один бин просто перизадавит другой, и вы будете долго разбираться - где же там вот то что у меня было. То есть, если мы сделаем какой-нибудь datasource-бин то он перезатрёт существующей datasource-бин, и мы долго будем соображать.
Это на самом деле лучший способ: если вы понимаете, что этот стартер несет то что мне не надо, просто сделайте бин с таким же айдишником и всё. Но потом этот стартер в какой-то версии просто изменит название метода - то у вас их станет два. И тогда праймари да? а у них там тоже праймари, и у нас два праймари, и там праймари на праймери...
результаты на 3 спрингбуте: MyRaven делаю implements
- прописываю его как @Component - создаются оба
- прописываю его как @Bean 2.1 c таким жеименем ravenListener - конфликт, неудвется запустить приложение 2.2 с другим именем - тоже создаются оба. Спринг не воспринимает implements того же парента как экземпляр того же класса
и вот возникает вопрос: у нас есть еще @ConditionalOnClass, @ConditionalOnBean, там можно писать классы.
@Configuration
public class КонфигурацияКазни {
@Bean
@ConditionalOnClass(Мыло.class, Веревка.class)
@ConditionalOnMissingBean(ФабрикаКазни.class)
public ФабрикаКазни виселицы(){return new ФабрикаВиселиц("...");}
@Bean
@ConditionalOnClass(Стул.class, Ток.class)
@ConditionalOnMissingBean(ФабрикаКазни.class)
public ФабрикаКазни стулья(){return new ФабрикаЭлектрическихСтульев("...");}
}
То есть если у меня есть мыло и веревка, я могу создать виселицу, когда у меня такое условие конфигурация казни: есть мыло есть веревка - вешаем на виселице. есть стул и ток - ну логично сажать человека на электрический стул, есть гильотина и хорошее настроение значит нужно рубить голову гильотиной. И вот у нас есть такая конфигурация. Вопрос, как бы как же мы будем казнить?
это на самом деле тоже очень интересный вопрос - а как вообще собственно все это может работать, вот например в Спрингбуте точно такое же решение создавать контекст который будет Веб или не Веб?, в зависимости того есть ли класс Сервлета в classpath или нету. соответственно у них есть еще вот такие аннотации @ConditionalOnMissingClass, и вот реально стало интересно понять - а как это собственно может работать? то есть вот у меня есть метод который будет создавать мне виселицу, но бин из виселицы должен создасться только если есть мыло и веревка. А например, мыла нету. Как мне понять, что нет именно мыло или нет именно веревки? потому что как вы думаете что произойдет если я попытаюсь считать аннотации с метода, если эти аннотации ссылаются на классы, которых нету? Вообще, можно ли считать такие аннотации?
1й вариант: Если чтото и возникнет,то будет ClassDefNotFound в рантайме, и все что передано в аннотацию мыбудем получать в виде массива, когда классы считываются рефлекшеном, 2йвариант - так не скомпилируется 3й - что будет работать.
ответ - оно будет работать. Но не отлично, потому что рефлекшеном это будет нельзя считать, потому что будет эксепшен, и будет непонятно, чего конкретно не хватает,и что вообщеслучилось. Поэтому все будетработать с помощью ASM. То есть Спринг парсит байткод "вручную", то естсь считывает файл, чтобы не сделать преждевременную загрузку этого файла,и понимает,что там есть Кондишенал с мылом-веревкой,и можетпрочекать наличие этих классов отдельно. Но это оочень медленно, но хотя бы дает принципиальную возможность считать класс, не загрузив его, и понять эту метаинформацию.
Вообще рефлекшн работает очень грустно: если спросить аннотации метоа, и если хотя бы одна аннотация ссылается на класс,которогонет- значит всё, никаких аннотаций получить не может, ClassDefNotFound. Рефлекшеном никак. Поэтому Спринг,если вилит что рефлекшеном нельзя сдёрнуть аннотацию, лезет через ASM(todo ?) что медленно. Поэтому Й.Холлер рекомендует в кондишенах не завязываться на экземмпляры классов, которых нет. И несмотря на то что кондишен онМиссингКласс так называется, он может параметром принимать название класса, то есть Стринг, и лучше делать именно так, тогда не понадобится АСМ и все работает быстрее (но почему-то в 2017 такникто не делал - судя по исходникам, даже сами авторы Спринга...:)
Теперь нам нужна возможность включить-откл отдельно только ворона, можно конечно не подключать стартер целиком, но скоро внем появится еще и разный другой функционал, хотелось бы отключать ворона без всяких там ОнПродокшенов,т к он дорогой. Убирать дестинейшн - тожекакой-то костыть...
Заведем новую пропертю, типа @ConditionalOnProperty(voron.isEnabled). И тут Идея ругается, что эта аннотация - не Repeatable, так делать нельзя. Тоесть,мы не можем сделать ее несколько раздля разных пропертей пао разному.
Тем не менее, есть способ это сделать, это @AllNestedCondition и AnyNestedConditions. Сделать новый кондишен, который будет учитывать и "voron.destination", и "voron.isEnabled"
В конфигурации над бином Ворона напишем @CondidionalOnRaven, и сделаем новый класс анотации в пакете com.ironbank.starter.annotation. Над ним пишем @Conditonal({OnRavenConditional.class}) - такая конвенция именования. Создаем этот класс, в нем мы должны бы имплементить Condition, но мы можем так не делать, а сделать здесь композитный кондишен,который либо "All" либо "Any", и в нем содержатся другие классы,содержащие обычные аннотации с кондишенами. Для этого вместо implements Condition мы указываем extends AllNestedConditions, и уже внутри написать вложенные классы, помеченные интересующими нас аннотациями, такими как
public class OnRavenConditional extends AllNestedConditions{
@ConditionalOnProperty(name="ворон.куда", havingValue = "false")
public static class OnRavenProperty{ }
@ConditionalOnProperty(name="ворон.вкл", havingValue = "true", matchIfMissing = true)
public static class OnRavenEnabled{ }
...
}
// todo есть вопросики, что значит havingValue и почему оно на "куда" false, а на вкл true
Идея просит сделать тут конструктор, автор убирает аргументы, а в теле вызывает super(ConfigurationPhase.REGISTER_BEAN)
внутри пишем статические классы R и С проаннотированные @ConditionalOnProperty см выше, и почему-то havingValue должен быть именно true,
//todo возможно надо покопаться в аннотации @Conditional (таймкод 1:03:30), там у нас массив стрингов, этот самый String havingValue и boolean matchIfMissing, //todo или посмотреть другую версию доклада того же автора // Кстати, это все написано в джавадоках
...Итак, у нас стоит @ConditionalOnRaven, и нет пропертей вкл-выкл, поэтому ворон не должен полететь, запускаем...
//у меня заработал код нового кастомного ворона из Мейна, а автор его убрал текущей темой
Теперь сделаем автоколмплит, добавив в RavenProperties поле boolean isEnabled; И теперь, с комфортом, прописываем в проперти-файл. Проверяем...
CamelCase в пропертях неработает, в JSON создался автокомплит is-enabled, а во всех остальных местах остался isEnabled. Поменяем на просто "enabled"
//у меня не сработал, т к я использовал код со слайда, там было // @ConditionalOnProperty(name="voron.destination", havingValue = "false") //а должно быть // // 1) @ConditionalOnProperty("voron.destination") // 2) @ConditionalOnProperty(name="voron.enabled", havingValue = "true") // поменял, все заработало как надо
TODO: что же значит havingValue?
... ...Таким образом мы можем делать композитные аннотации,и впихивать их сколько угодно существующих,даже если они не-repeatable. //Лекция была на 4 спринге, и насколько знаю, в 6 Спринге поддержка @Repeatable до сих пор еще не реализована, все пишут AllNestedConditions...
В мире "Игры престолов" настаёт Зима, и все меняется. Раньше наш банк выдавал кредиты с вероятностью 50%, кроме "Старков". Теперь в тех филиалах, где она уже пришла кредиты выдаются только тем, кто возвращает долги. Заведём проперти ironbank.those-who-repay-debts = Ланистеры
Этой проперти соответствует класс PredskazProperties в пакете model @Data @ConfigurationProperties("ironbank") public class PredskazProperties{ List thoseWhoRepayDebts; }
//Кстати, здесь КэмеэКейс не мешает, значит и в прошлый раз дело было не в нём... //TODO: вернуться в часть 12, переписать на CamelCase
Заведем новый сервис предсказаний WhiteListPredskazService implements PredskazService, который будет работать только зимой. Инжектим в него нашу PredskazProperty, не забываем про конструктор инжекшен и аннотацию @Service. Ну и собственно возвращаем только еслиимя в списке: predskazProperties.getWhoseWhoRepayDebts().contains(name);
Также мы сделали профили, в зависимости от которых будет включаться та или иная реализация сервиса предсказаний "Зима близко"
То есть над старым сервисом стоит аннотация @Profile(ProfileConstants.NO_WINTER) а над новым - @Profile(ProfileConstants.WINTER_IS_HERE)
Запускаем.... И мы не указали никакой профИль, будет эксепшен. Можно предположить, что спринг не знает, какой из двух равнозначных бинов инжектить? Нет, проблема в том, что мы запустили без профилей, и просто нет ни одного бина. Дажепросто нра этеапе бинДефинишенов они не считались. Причем эксепшен совершенно непонятен, а уж тем более для работников филиала, которые забыли его включить, и вообще про профили им никто не объяснил. Попробуем это исправить.
Нужен нормальный эксепшен "Скажи какой профиль", сделаем его в нашем стартере, теперькроме ворона онбудет не давать прилоениеюзапуститься,если не проставлен профиль. Это должно происходить на довольно раннем этапе, БПП нам не подойдет. Будем писать AplicationContextInitializer. Онорабатываетна этапе,когда контекст ужесоздаН,но в нем еще пока нет ни бинов,ни биинДефинишенов. Единственное,что в нем пока есть, это Environment, он создается ещедосозлания Контекта, и когда он передвется вконтекст,тоотрабатывают вот ээти ContextInitializer'ы.
в пакете Стартера создаем RejectProfileAppI{nitializer implements ApplicationContextInitializer,единственный метод initialize? у негоесть доступ к Контексту, в которм на даннном моменте нет нияегокроме Environment , но из негоможновытщить активные профили, и если мы видим,что ихнету, бросаем эксепшен
if(applicationContext.getEnvironment().getActiveProfiles().length == 0){
throw new RuntimeException("please run with --spring.profiles.active=winterIsHere ")
}
Ок,теперь надо его задекларировать. Если мы пропишем как бин, это не поможет, т к все бины создаются намного позже. Поэтому надо прописать в СпрингФакториз (до СпрингБут 3):
...ApplicationContextInitializer=...RejectProfileAppInitializer
/* как мы помним, в Спрингбуте 3 поддержка spring.factories для автокорнфигураций прекращена. Но для контекст Инишалайзеров сполне себе можно использовать spring.factiories! Как оказалось. Вот что еще предлагает СтэкФверфлоу для тогочтобы прописать Initializer (https://stackoverflow.com/questions/35217354/how-to-add-custom-applicationcontextinitializer-to-a-spring-boot-application) Перед запуском приложения в мейне application.addInitializers(YourInitializer.class); application.run(args);
Или там же спомощью Билдера new SpringApplicationBuilder(YourApp.class).initializers(YourInitializer.class).run(args);
Еще интересный способ - это добавить проперти
context.initializer.classes=com.example.YourInitializer
в проперти/ямл файл
*/
Запускаем, и видим, что контьекст упадет сразу - не надо ждать. Аглавное, что упадет с простым и понятным эксепшеном. Запустим с профилем, все работает. //7:20
Рассмотрим подробнее объект Environment. В него складываются разные проперти и метаинформация, доступная в самом начале: systemProperties, проперти из команднойстроки, рандомы, инфориация про активные профили. Его создает тот самый SpringApplication (см. начало лекции), а значит мложно попробовать определить профиль автоматически, в зависимости от того, какая сейчас погода.
То есть,логика такая: если активного профИля нет, значит смотрим погоду сами, и решаем, настала зима или нет, чтобы выдавать кредиты по соответствующим правилам. Соответственно, мы должны вклиниться ещераньше, на этапе когдастроится Environment, и проверить наличие профИля, при необхоимости добавив свой.
Для этогонам понадобится EnvoronmentPostProcessor. Он работает, когда еще нет никакого контекста - практически самая первая стадия. Это штука довольно странная, и для того чтобы рассмотреть подробнее, напишемсвой EPP. В пакете старера создаем класс ProfilesEPP implements EPP. В EPP есть метод PPE, которому доступны собственно Environment, с которым мы будем радотать, а также SpringApplication, который его создал и передал ссылку на себя,хотя для наших целей он не нужен.
Пишем внутри метода логику if(resolveTemperature()<-272){ environment.addActiveProfile("winterIsHere"); }else{ environment.addActiveProfile("winterIsHere"); } можно еще добавить проверку на наличие активного профиля, но у нас она была ранее (? прим - почему? ведь ContextInitializer работает после EPP? )
Кроме addActiveProfile() есть еще и setActiveProfile(), и в отличие от add он перезаписывает текущее значение. То есть после нас можно еще добавить дополнительные профили...(не понятно)
Поскольку это очередная точка расширения, которая должна работать до всяких бинов (и даже самого контекста), то как и все предыдущие, ее надо задекларировать в spring.factories ...EnvironmentPostProcessor=ProfileEPP
...убираем из командной строки профиль, запускаем - все работает.
C ним не все так просто, потому что он раюотает очень рано. Вспомним, что на прошлом этапе Applicationlistener должен былзавалить контекст как можно раньше, пока еще ничего не создалось. А EPP отработал еще раньше, сперва разрезолвив профиль. Кто же его запускает? Его запускает странная штука ConfigFileApplicationListener, который сам является олдновременно EPP и ApplicationListener'ом. Будучи листенером, он слушает определенный эвент,который кидает SpringApplication (на самом деле еще одно звено, которое откровенно лишнее). А такжеон делает некоторые другие вещи,втом числе и запускает другие EPP (SRP и не пахнет).
Итак, SpringAppliocation cnhjbbn Environment, после этого он передает себя и энвайронимент в ConfigFileAppicationListener, и дальше он листенер слышит эти эвенты, а как EPP загружает все проперти и все остальное в энвайронмент, ипосле того он дает поработать другим EPP. Он тоже "ездит" по всем jar'ам с помощью SpringFactoriesLoader'а, и этот лоадерпритаскиваетт все EPP которые он нашел в системе. После этого он их сортирует по их ордерингу, и втом же самом листе с другими EPP передает сам себя в onApplicationEnvironmentPreparedEvent()
Оченьстранный дизайн-паттерн, но это оттого,что Спринг сам внутри себя не может использовать Спринг. Темюболее на текущем этапе, когда еще нет никакого контейнера.
Итак, наш ConfigFileApplicationListener слушает AppliocationPreparedEvent и ApplicationEnvironmentPreparedEvent И он жезагружает aplication.yml, application.properties, env vars, cmd args. Так сделано.потому что он на раннем этапе считывает всевот эти файлыит.д., но онииеще не смержены, и добавив свой EPP мы можем все сломать, потому что очень силтьно играет ролдь,вкаком порядке они исполняются, в т.ч.и наш. Особенно это актуально,если мц используем SPringCloud (Если это все жепроизошло, то аннотация @Order нам в помощь)
В обычном спринге мы слышали про какие-то эвенты,типа ContextStartedEvent, ContextStoppedEvent, ContextClosedEvent, ContextRefreshedEvent. А вот в СпрингБуте их гораздо больше, простотпотому что жизненный цикл егоприлодения гораздобольше,чем нга обыфчном Спринге:
ApplicationStartingEvent, --
ApplicationEnvironmentPreparedEvent, ---Этап конфигурации
ApplicationPreparedEvent, --/
ContextRefrehedEvent,
EmbeddedAServletContainerInitializedEvent,
ApplicationReadyEvent,
ApplicationFailedEvent,
ContextClosedEvent
Далеко Не всеэти эвенты нужны обычному программисту. ... ...Совсем необязательно прописывать ContextApplicationListener чтобы слушать эвенты. ... с помощью модной аннотации @EvemtListener слушать всеэти эвенты нельзхя, можно только обчные спринговые...
Кстати, где они в нашем списке? Вот эти, ContextStarted и ContextStopped. Забыли? Нет, просто они не работают :) Мы млжем кинуть их самостоятельно,иихбудет слушать листенер,но сами по себеони не работают. Такжеи обратное, разих никтоне вызыавпет,то ихникто и не слушает. Можно сколдько угождно вызывать context.stop(), и ничегоне произойдёт. Единственное это contet.close(),после этого дальше никакие эвенты вызывать нельзя,будет эксепшен.
///20:10 диаграмма, см скриншот///
Итоговая диаграмма прилоддения на springBoot выглядит так: SpringApplication.run() строит environment
ApplicationStrtedEvent работают EPP ApplicationPreparedEvent работают ApplicaitionContextInitialiaer'ы - например наш,который бросал исключение при пустом энвайронменте ContextRefreshedEvent --- здесь начинает работать "обычный Spring" - см. материал лекции "Spring-the-Ripper" --- EmberddedServletContainerInitializedEvent ApplicationReadyEvent