Евгений EvgenyBorisov Борисов (NAYA Technologies) и Кирилл tolkkv Толкачев (Циан.Финанс, Твиттер) рассказывают о самых важных и интересных моментах Spring Boot на примере стартера для воображаемого Железного банка.
![](https://habrastorage.org/r/w1560/webt/wj/p5/pf/wjp5pfvfgcnp_mda9gcpuiln3zs.png)
В основе статьи — доклад Евгения и Кирилла с нашей конференции Joker 2017. Под катом — видео и текстовая расшифровка доклада.
Конференцию Joker спонсируют много банков, поэтому представим, что приложение, на котором мы будем изучать работу Spring boot и создаваемый нами стартер, связано именно с банком.
![](https://lh3.googleusercontent.com/M_mq4I8ViT6xCMSA6qsX5axsAib8aznsbaY0pFTclTeawPMxuuzTAOT5lrbP6RBeQDGPNryri_4S9Q5DzWBiwR0E99rZX2Y-QB_eksEv1o0bw4H4C29PiESr_XKhF78-BH-PlTg0)
Итак, предположим, поступил заказ на некое приложение от «Железного банка Браавоса». Обычный банк просто переводит деньги туда-сюда. Например, так (для этого у нас предусмотрен API):
А в «Железном банке» перед тем, как перевести деньги, необходимо, чтобы API банка просчитал, сможет ли человек их вернуть. Может быть, он не переживет зиму и возвращать будет некому. Поэтому там предусмотрен сервис, который проверяет надежность.
Например, если мы попытаемся перевести деньги Таргариену, операция будет одобрена:
![](https://lh4.googleusercontent.com/lqDKuJR-E2skOO3ao-ykD2t2sJbQ6i8Wo73BR2eOOd7ytos3OR-vXMAtUkkUoO9Ndv8mwvXftAR6wt06VU1lnZawfN1PIMePq5VoOXHSVwP2H4CYXTsfzdfZJEE7I6kph2y_2Xtm)
А вот если Старку, то нет:
![](https://lh3.googleusercontent.com/7Fc2trnuG-pn5BsZPLqBIxPIJ5mp6az2v1QLmYU0ROiJHJk9SGDEYPOJ3k8bqKjaj2V5QpTSCE0iXIReMlWrFvq5TxEFll6r3SzNmG2shhrHEp4xoHlmWRsbHAHqFsuI0kZPZUIh)
Ничего удивительного: Старки слишком часто умирают. А зачем переводить деньги, если человек не переживет зиму?
Посмотрим, как это выглядит внутри.
Это обычный стринговый контроллер.
Кто отвечает за логику выбора, кому выдавать кредит, а кому — нет? Простая строчка: если тебя зовут Старк, точно не выдаем. В остальных случаях — как повезет. Обычный банк.
Все остальное не так интересно. Это какие-то аннотации, которые делают за нас всю работу. Все очень быстро.
Где же тут все основные конфигурации? Контроллер — только один. В Dao — вообще пустой интерфейс.
В сервисах — только сервисы переводов и предсказаний, кому можно выдавать. Директории Conf нет. По сути у нас есть только application.yml (список тех, кто возвращает долги). И main — самый обычный:
Так где же спрятана вся магия?
Дело в том, что разработчики не любят думать о зависимостях, настраивать конфигурации, особенно если это XML конфигурации, и думать о том, как запускается их приложение. Поэтому Spring Boot решает эти проблемы за нас. Нам же остается только задача написать приложение.
Первая проблема, которая у нас всегда была — это конфликт версий. Каждый раз, когда мы подключаем разные библиотеки, которые ссылаются на другие библиотеки, появляются конфликты зависимостей. Каждый раз, когда я читал в интернете, что мне надо добавить какой-нибудь entity-manager, возникал вопрос, а какую версию мне надо добавить, чтобы она ничего не сломала?
Spring Boot решает проблему конфликтов версий.
Как мы обычно получаем проект Spring Boot (если мы не пришли в какое-то место, где он уже есть)?
Если мы работаем с Maven, то в проекте будет pom.xml, где есть родитель Spring Boot-а, который называется
Я сейчас не буду вдаваться в подробности Maven-а. Буквально два слова.
Блок dependency-менеджмента не прописывает зависимости. Это блок, при помощи которого можно указать версии на случай, если эти зависимости будут нужны. И когда вы указываете в блоке dependency-менеджмента какую-то зависимость, не указав версию, то Maven начинает искать, а нет ли в parent pom-е или где-то еще блока dependency-менеджмента, в котором эта версия прописана. Т.е. в своем проекте, добавляя новую зависимость, я уже не буду указывать версию в надежде, что она указана где-то в parent-е. А если она не указана в parent-е, то она точно не создаст ни с кем никакого конфликта. У нас в dependency-менеджменте указаны добрые пять сотен зависимостей, и они все согласованы между собой.
Но в чем проблема? Проблема в том, что в моей компании, например, есть свой parent pom. Если я хочу использовать Spring, как мне быть с моим parent pom?
![](https://lh5.googleusercontent.com/PlZyF8f_voM5teZkYn4Vyar45YDFXlzzeWYddviXedAi9YG3elV34Iz5ooOp39CZOHI1dBMXNiezPg68zFd2TU1oKBWqLtAnD_WgILLSdaqck9vssLj9tvOvdmauPk7r7sI7AiF1)
Множественного наследования у нас нет. Мы хотим использовать свой pom, а блок dependency-менеджмента получить со стороны.
![](https://lh5.googleusercontent.com/0uAD0DG4sRoCpbWZjVxr1saUaDRi0B1dbx00hfXjo4m9_Kp4kqv8kFUDVzkto9tdB-25_UY9Bc48plVyAYoyQEZsvvJHCK5t9ioKbSBxMZ7uZi2CQAH8tbuT7iBr3rg1YbaA6ld_)
Это сделать можно. Достаточно прописать блоке dependency-менеджмента импорт BOM’а.
Кто хочет узнать подробнее про bom — смотрите доклад "Maven против Gradle". Там все это подробно объяснялось.
Сегодня среди больших компаний достаточно модно стало писать такие огромные блоки dependency-менеджмента, где они указывают все версии своих продуктов и все версии продуктов, которые используют их продукты, и которые не конфликтуют друг с другом. И это называется bom. Эту штуку можно импортировать в ваш блок dependency-менеджмента без наследования.
А вот так это делается в Gradle (как обычно, то же самое, только проще):
Теперь давайте поговорим про сами зависимости.
Что мы пропишем в приложении? Dependency-менеджмент — хорошо, но мы хотим, чтобы у приложения были определенные способности, например, чтобы оно отвечало по HTTP, чтобы была БД или поддержка JPA. Поэтому все, что нам нужно сейчас — это получить три зависимости.
Раньше это выглядело так. Я хочу работать с БД и начинается: transaction менеджер какой-то нужен, соответственно нужно модуль spring-tx. Мне нужен какой-нибудь hibernate, поэтому требуется EntityManager, hibernate-core или еще что-то. Я все настраиваю через Spring, значит нужен spring core. То есть для одной простой вещи надо было думать о десятке зависимостях.
Сегодня у нас есть стартеры. Идея стартера заключается в том, что мы ставим зависимость на него. Начнем с того, что он агрегирует те зависимости, которые нужны для того мира, из которого он пришел. Например, если это стартер security, то вы не думаете о том, какие нужны зависимости, они сразу прилетают в виде транзитивных зависимостей к стартеру. Или если вы работаете со Spring Data Jpa, то ставите зависимость на стартер, и он принесет все модули, которые нужны, чтобы работать со Spring Data Jpa.
Т.е. pom у нас выглядит следующим образом: содержит только те 3-5 зависимостей, которые нам нужны:
С зависимостями разобрались, все стало проще. Думать нам теперь нужно меньше. При этом нет конфликта, а количество зависимостей уменьшилось.
Поговорим про следующую боль, которая у нас была всегда, — настройка контекста. Каждый раз, когда мы начинаем с нуля писать приложение, на то, чтобы настроить всю инфраструктуру, уходит куча времени. Мы прописывали либо в xml, либо в java config-ах очень много так называемых инфраструктурных бинов. Если мы работали с hibernate, нам нужен был бин EntityManagerFactory. Много инфраструктурных бинов — и transaction manager, и data source, и т.п. — нужно было настраивать руками. Естественно, все они попадали в контекст.
В ходе доклада "Spring-потрошитель" мы в main-е создавали контекст, и если это был xml-ный контекст, он изначально был пустой. Если же мы строили контекст через
А вот сейчас в main-е есть
Но на самом деле контекст у нас есть.
![](https://lh3.googleusercontent.com/7t4F0sYYqrFkxjWcMOMU4sfZ63sJ1vNksOl3IdK6wvqCb7DTv7bFZHEG0ZVO9RrywUo1EzwreJwaWte4ucnwrcz3iRkgMXY-Okw3pGzqfRWuMmF53tYvgtGzlFdcRXSRlmra45Lv)
Это совершенно нетипичный кейс. Раньше было два варианта:
Иными словами, контекст так или иначе был. И мы все равно на вход передавали какие-то классы конфигурации. По большому счету мы выбирали тип контекста. Теперь же у нас только
Дано:
Вопрос: что еще можно туда передать?
Варианты:
![](https://lh3.googleusercontent.com/yO4UMw3j_Zj3QPV4uN92pB43pk9mTfog-QrU70ipJLL1fhp6NnkkycBbrph54eubzHzzQjOEc2J58di5QAk0cv2kU03HyWUQF0euLo6jp9R0cpG6yN-SYDiE5hJ3DDap4lrAvrQd)
Т.е. на самом деле все ответы верны. Любой из них можно заставить работать, даже
Единственное, что в документации не сказано, это в каком виде нам туда передавать. Но это уже из области тайного знания.
По-настоящему важен здесь
Наш Карлсон создает какой-то контекст на основе входных данных, которые мы ему передаем. Напоминаю, передаем мы ему, например, такие пять замечательных вариантов, которые все можно заставить работать с помощью
Что же делает для нас
![](https://lh3.googleusercontent.com/wMad1sKem8Zl3cwH3cM6uCt4vw0HcUHnn0Z6AI582-FHg4TTfIvpKnhGrste_f4iZ36jRmTOqR5H7ct-iP2ml6hfgWcaS3QExS4t4rmqF0poGh9Q2-6nhCxImQ-Brw6rPcSVQd7c)
Когда мы в main-е сами через
![](https://lh6.googleusercontent.com/NpsAwT2EADy0FVRCvTIrEGySdWfc9vEPhGPWuz8kc3Eo8BRK6TsAIzuo97uLiOcayBTOPPnn5butHhQ5D1__S7Nrs2QJ5MIpfm-QGvGtY5v941R0JSnW6gAGhHExnlUBO8gvWTMh)
А какие варианты есть, когда контекст строит Карлсон?
Он делает только два вида контекста: либо web-контекст (
![](https://lh5.googleusercontent.com/tHgHadUUCf_g7lJkJzPkrW57tQZmttXfnaNHseKExMmTsa_Pec-ZRRk5BGkPUl_R6z1ScYinG4BAc0FbZZqKsqB_NCQ6OTxwdF0xZU9zfQZSvE9MTJfB529CGg7baSndGzGUkkOY)
Выбор контекста основывается на наличии в classpath двух классов:
![](https://lh5.googleusercontent.com/zYOh16MaqML5BWAyaAt9dHjQHLjr2j_vNNDXp3TG5-SbS2E8VnpSeGlvRJtx-7W5bWd6ytciwY0EFB91UkZBM0x43sDCPFYKRd4iDJbspLrf7Zz4_BwnAAuZAV-Pzwf6aPzU5GGP)
То есть количество конфигураций не стало меньше. Чтобы построить контекст, мы можем указать все варианты конфигураций. Для построения контекста я могу передать groovy-скрипт или xml; могу указать, какие пакеты просканировать или передать класс, помеченный какими-то аннотациями. То есть у меня есть все возможности.
Однако это Spring Boot. Мы еще ни одного бина не создали, ни одного класса не написали, у нас есть только main, а в нем — наш Карлсон —
Если в этот контекст заглянуть, что там будет?
В нашем приложении после подключения пары стартеров было 436 бинов.
![](https://lh4.googleusercontent.com/dDfIMcA-7i4unSC4E64UTBQsQ6xQAesF_Up0KS5YXGGHiCc9tqa7D_5Yx_MXL_FMtddEEJyE8UlEBNknK064cbB4p7V_dDZVS3-5trpz1rUqrz2DcpeU6ErYYZ26x1vRaz6wjXhO)
Почти 500 бинов только для того, что начать писать.
![](https://lh5.googleusercontent.com/n5mpK_cnqqBKiWBrI_Tu1iAfvEJUkz6ixgihHy35jGV5cemFauD8mmA0aO74jTGqaVwDUWLMBoSmMLqIPJ1XSmbwHOYL6AmpeEBJpCOJncEaRDjG5wf6sca-Bd2ymsYilftELnwU)
Далее мы поймем, откуда взялись эти бины.
Но в первую очередь мы хотим сделать так же.
Магия стартеров, кроме того, что они нам решили все проблемы с зависимостями, заключаются в том, что мы подключили всего 3-4 стартера, и у нас 436 бинов. Подключили бы 10 стартеров, бинов было бы сильно больше 1000, потому что каждый стартер, кроме зависимостей, уже приносит конфигурации, в которых прописаны какие-то необходимые бины. Т.е. вы сказали, что хотите стартер для веба, значит нужен диспатчер сервлет и
Чтобы понять, как это работает, мы сегодня будем писать стартер, который тоже будет приносить во все приложения, которые этим стартером будут пользоваться, инфраструктурные бины.
![](https://lh4.googleusercontent.com/441g_tPwsiMC_jr3z3fggJKoDwp0uOZxGxMhOgEptn0c6_cQIvEy3rHNjOUNuPXApO78oRtfaXNul_We9YpVvBPEohQsGTZhWmrLkhzCZBnd3TWOXumbpJiJFY7CVzOYC2SFKwMx)
Давайте посмотрим на требование от заказчика. У Железного банка есть много разных приложений, запущенных в разных филиалах. Заказчики хотят, чтобы каждый раз, когда поднимается приложение, посылался ворон — информация о том, что приложение поднялось.
Начнем писать код в приложении конкретного Железного банка (Iron bank). Будем писать стартер, чтобы все приложения Iron Bank, которые будут зависеть от этого стартера, смогли автоматически посылать ворона. Мы помним, что стартеры позволяют нам автоматически подтягивать зависимости. А главное, мы не пишем практически никакой конфигурации.
Мы делаем listener, который слушает, что контекст обновился (последний event), после чего отправляет ворона. Будем слушать
Пропишем listener в конфигурации стартера. Пока там будет только listener, но завтра заказчик попросит еще какие-то инфраструктурные штуки, и мы их тоже пропишем в этой конфигурации.
Возникает вопрос: как сделать так, чтобы конфигурация нашего стартера автоматически подтянулась во все приложения, которые этим стартером пользуются?
На все случаи жизни есть «enable что-то».
![](https://lh3.googleusercontent.com/l95f3srqNyAHbSMOzhjBDnBKKVtXL_Rss0nrkkczgWeYqo_dN4MG1gZGFzxcbBiYgKfAi9QnmV2HiVAFLyvQA5YswRpxYlVHhbsPNMBrJfjr6iW0JyoxT745a-gsiHwlVxOVe10N)
Неужели, если я буду зависеть от 20 стартеров, мне придется ставить
![](https://lh6.googleusercontent.com/xMwYfCX_LBnvh9vGTk62bzAJJrC9RhQnocYU8bSsZGbg8_ZvZ5kLoGHN9RXjYqi21br1dvX6fM_8l7OEPzFuIKnqXhgr29_q8sFLBEdI9aWVrBXMFizKEoo3rW0slBVUS06pKew1)
На самом деле я хочу получить некую инверсию контроля на уровне зависимостей. Хочу подключать стартер (чтобы все заработало), и ничего не знать о том, как называются его внутренности. Поэтому мы будем использовать spring.factories.
![](https://lh3.googleusercontent.com/luVjJZtGgNAF3OPUHfHruup2A9YRWs9cvCxeSxwnq5jaMVAS8IhP8-AHCzMznEvLlvRu1SIlNjLOGmQx-hDVx_Z7jRMSWAOU8-LkFes9gOlUP5qb2KcjlbDbm3CxGzD7hT4lKJ_d)
Итак, что такое
В документации написано, что есть такой spring.factories, в котором нужно указать соответствие интерфейсов и того, что нужно по ним подгрузить — наших конфигураций. И все это волшебным образом появится в контексте, при этом на них отработают различные условия.
![](https://lh6.googleusercontent.com/hKN7AjL8kM2PSrDTWHE-YnJZglft5PEXHxpNFxzVVrdDkeej1g_8uuNZD9Nx6WIxJgzxchydhrLKsUQjZGUv-3YTo9o9kgjmO40SXTTMJfCHcQza_kZmurlKb2w9NN4SnRafSb-B)
Таким образом мы получаем инверсию контроля, что нам и нужно было.
![](https://lh5.googleusercontent.com/e4gGtcFywlFR4NULhgQ0pdyA2qkZddEBQvbywoQ9FC1hInCPocRf76oJ9Qw5YJ6wkOhCor2Sao3rVxnTNWDsbCTU6L8a4JdvxqVm6bkoyu3LY_GcioVM9sQkTWvfisLQh_8smVO6)
Попробуем реализовать. Вместо того, чтобы обращаться к кишкам стартера, который я подключил (эту конфигурацию взять, и эту…), все будет ровно наоборот. У стартера будет файл, который называется spring.factories. В этом файле мы прописываем, какая конфигурация у этого стартера должна быть активизирована у всех, кто его подгрузил. Чуть позже я объясню, как именно это работает в Spring Boot — в какой-то момент он начинает сканировать все jar-ы и искать файл spring.factories.
Теперь все, что нам остается сделать, это подключить стартер в проекте.
В maven аналогично — нужно прописать dependency.
Запускаем наше приложение. Ворон должен взлететь в тот момент, когда оно поднимется, хотя в самом приложении мы ничего не сделали. С точки зрения инфраструктуры мы, конечно, написали и сконфигурировали стартер. Но с точки зрения разработчика мы просто подключили зависимость и появилась конфигурация — ворон полетел. Все, как мы и хотели.
Это не магия. Инверсия контроля не должна быть магией. Также, как не должно быть магией использование Spring. Мы знаем, что это фреймворк в первую очередь для inversion of control. Как есть инверсия контроля для вашего кода, так есть и инверсия контроля для модулей.
Вспомните момент, когда мы руками строили контекст в main. Мы писали
Во-первых, внутри там стоит
Во-вторых, над ним стоит
На самом деле
Кроме этого тут есть еще вещи, которых не было раньше —
Главная задача
Нужно обратить внимание на этот класс. Он заканчивается на
![](https://lh4.googleusercontent.com/gQuE_0ta_fQCum0qZrc0rZsXk0UyQHpn4tzOH8k5EgAyefWTPdtLPT9DhpXbjN1RDhNaN4NfWfJfiaRthyHnGpn-zUcBLsgURChRKG65nyISlrsZ-xWHKkYhbID25P4JXQ-WiRHu)
Как он это делает?
В первую очередь использует незамысловатый утилитный класс, SpringFactoriesLoader, который смотрит на spring.factories и грузит все из то, что попросят. У него есть два метода, но они не сильно отличаются.
![](https://lh6.googleusercontent.com/UClhaeyhLY1BTYx_5SM_iiq4BhSK-zskioSge_p1dMgxDVoOfIHqgxWfVlNg_ZddSw-PR0pcPHCpXzOdiMhJvSW0TNcIi02k53sAMq19RspZ7q6JHkPJJ6nyXOkT-rNVSbTPvDwK)
Spring Factories Loader существовал еще в Spring 3.2, просто им никто не пользовался. Его, видимо, написали, как потенциальное развитие фреймворка. И вот он перерос в Spring Boot, где есть очень много механизмов пользующейся конвенцией spring.factories. Мы покажем далее, что еще, кроме конфигурации, можно прописывать в spring.factories — listener-ы, необычные процессоры и т.п.
Так работает инверсия контроля. Мы как бы соблюдаем open closed principle, в соответствии с которым не надо каждый раз где-то что-то менять. Каждый стартер несет очень много полезных вещей в проект (пока мы говорим только про конфигурации, которые он несет). И у каждого стартера может быть свой собственный файл, который называется spring.factories. При помощи него он рассказывает, что именно несет. А в Spring Boot есть много разных механизмов, которые умеют из всех стартеров приносить то, что рассказывают spring.factories.
Но есть нюанс во всей этой схеме. Если мы пойдем изучать, как это устроено в самом Spring, как пишут люди, которые придумали всю эту схему стартеров, то увидим, что у них есть одна зависимость
То есть вне зависимости от того, подключил я стартер или не подключил, когда я работаю со Spring Boot, всегда будет один из jar-ов (jar самого Spring Boot), в котором есть его личный spring.factories, где прописано 90 конфигураций. Каждая из этих конфигураций может содержать в себе множество других конфигураций, например,
Более того, потом там статически из класса вынимается какая-то мапа, и в этой мапе захардкожены загружаемые конфигурации (которых нет в этом spring.factories). Их уже будет не так просто найти.
Самое интересное, что на этапе загрузки они все действительно будут пытаться загрузиться.
![](https://lh4.googleusercontent.com/tQy_aGiDRZNGvwoH7MsTP7M-TZiLeIn4BTiybAlJnZ-sivGlWMJ85KT_FQtWRAYtg8VrvXqDe3ChRTR7LscB2r8GMU3p4PrhSw8jU-MzrCuJ--OVdRvYQSAMIVPSuZlc6t8yxLdD)
Они будут пытаться. Но:
![](https://lh6.googleusercontent.com/NsgtgP4JyHnK5r3ONfifLK0p6L6fZhhvpa_tgG11MC7k9dpwQhjYpNWwsFX18YtIHPP642L-3NYFqLk1UjCdqrvK0cHNwYyn1Iu29ZOushQHyWgpeMl9xLJvH1vx1BMH3L6gAZbd)
Подведем промежуточные итоги. Часть конфигураций — хорошие, добрые, правильные стартеры, которые соблюдают инверсию контроля и open closed principle — несут свои spring.factories, в которых прописаны их кишки. Мы будем делать именно так, мы в принципе по-другому не можем сделать.
Кроме этого есть еще часть конфигураций, прописанных в самом Spring Boot, которые грузятся всегда — их еще 90 штук. Также есть еще штук 30 конфигураций, которые просто захардкожены в Spring Boot.
Все это дело поднимается, а потом конфигурации начинают фильтроваться. В конце 2013 года был доклад о том, что нового в Spring 4, где рассказывалось, что появилась аннотация
Но есть нюансы. В первую очередь это приводит к ситуации, в которой бин может быть, а может не быть, в зависимости от каких-то настроек окружения.
![](https://lh3.googleusercontent.com/rBDemtD-7Gqj_aZC38_GUCLYBJTaHWqK0I29ivtw2VoyP67OCjz8NT2SddewUONKdP--8kIP6W_9h4YXs9D_eJYiOZ6L-GPz8yutYN59U58UBC8eSWwJhob2WomIYWfjDwT6S3BC)
Рассмотрим это на примере.
У заказчика появилось новое требование. Ворон — штука дорогая, их не очень много. Поэтому запускать их надо, только если мы знаем, что поднялся продакшн.
![](https://lh6.googleusercontent.com/RCMd13C8fUM9jhWjXHaJVH0OB62Th13VSykXiJn_IlTJ5wEb_1yCzmSJ0ZesIdnQ5JW5TXruAp6kDGaW8Frki2wJAf5iRllNGF1ClngP9lyw4J4MNVLEj5aCME67IVomJQzumrf-)
Соответственно listener, который запускает ворона, должен создаваться, только если это продакшн. Попробуем это сделать.
Идем в конфигурацию и пишем:
Как мы решаем, продакшн это или не продакшн? У меня была одна странная компания, которая говорила: «Если Windows на машине, значит не продакшн, а если не Windows, значит продакшн». У всех свои conditional.
Конкретно Железный банк сказал, что они хотят управлять этим в ручном режиме: когда поднимается сервис, должен выскакивать попап: «продакшн или нет». Такой condition не предусмотрен в Spring Boot.
Делаем старый-добрый попап:
Попробуем.
![](https://lh5.googleusercontent.com/KdFo4iqc8-y0SQgRjckuEdCTx3zNNvvPyE5G50l_qDZ3dIPWlpkWUKO4LOauSxvzECbll_wwvAr_HOTcyPiWFvWAdDyg4D_JtWdMteW6qZpnWgUUbaS5ilCNQuiJgdky70gNgUb-)
Поднимаем сервис, жмем в окне yes, и ворон летит (listener создается).
Запускаем еще раз, отвечаем No, ворон не летит.
Итак, аннотация
Итак,
Вопрос: логика, которая узнает, продакшн это или нет, сколько раз должна отработать?
Какая разница, сколько отработает? Ну может эта логика дорогая, требует времени, а время — это деньги.
В качестве иллюстрации мы придумали такой пример:
Здесь у нас есть два бина: один обычный, один конфигурационный. Оба помечены кондишн-аннотацией — они нужны, только если пришла зима.
Аннотация стоит два раза. Каждое обращение к гидрометцентру в мире Игры престолов дорогое — надо каждый раз платить деньги, чтобы узнать, какая погода.
![](https://lh5.googleusercontent.com/Y28p_-CnFJ5JBrJkeFAfujG8U7RlYH2lOvVsdlh7Zeslo9Cpo4jCsd9VWOOeQJe8OwErdkmiphjSoVh-Yc7ZBxLPlXJ7FqrBU6sg2kVlMXZ1FBg7Fv9u5zRiVbGH3EmZVVTPy3-S)
Если бы это работало с кешированием, логика вызывалась бы только один раз (то есть
Правильный ответ не очень четкий — 300 или 400. Тут на самом деле какая-то полная дичь. Мы очень долго ковырялись, чтобы сначала понять, что происходит. Как оно происходит — это отдельный вопрос.
Ситуация такая. Если кондишн стоит над классом сверху (класс
Если же бин прописан внутри конфигурации, то всегда один раз.
Поэтому данная загадка не имеет точного ответа, поскольку нужно выяснить, где прописана конфигурация. Если она прописана в стартере, то ее кондишн почему-то отработает два раза, а кондишн на бин в любом случае отработает один раз, получаем 300. Но если конфигурация прописана не в стартере, только ее кондишн трижды запустится, плюс еще один раз на бин. Получаем 400.
Возникает вопрос: а как это вообще работает и почему оно так? И ответ у меня только такой:
![](https://lh5.googleusercontent.com/gNMkCHMj_9M7auEL2BruoM99dW0aVNYdT_jmeWO8fn0D5-12UmFW7RrdiM61Bb8CwbE1hO1MfJuK-T1N4Yf1ep9bLvfjNlGXtg-4bMWB5RrSbVadxYonu6LlL61D8aSqY_hVRpAc)
Не важно, как оно работает. Важно понимать следующее: когда вы пишите свою кондишн-аннотацию, стоит в ней самостоятельно делать кэширование, причем через статический филд, чтобы логика не вызывалась много раз. Потому что даже если вы этой аннотацией воспользовались один раз, логика отработает больше, чем один раз.
Продолжаем развивать наш стартер. Надо как-то конкретизировать полет ворона.
![](https://lh4.googleusercontent.com/_lmVEL4SXFi2uxNnfgDEjqamv9OOjuX4_aRmQKLgo9G89G2YbeyDmETZuyWdAaWHmyns5-GtnesVQMwcgVHP3ROPPHN00RFmgcy1W8IEaOwt8FB1c1klIbUZjOqJCogMvFj-UTyD)
В каком файле мы прописываем вещи для стартера? Стартеры приносят конфигурацию, в которой есть бины. Как эти бины настроены? Откуда они берут data source, user и т.п. У них, естественно, есть дефолты на все случаи жизни, но как они позволяют это все переопределять? Есть два варианта:
Чем наш стартер хуже? Тот, кто им пользуется, тоже должен иметь возможность сообщить, по каким адресам лететь ворону — нам нужно сделать список получателей. Это первое.
Второе — мы хотим, чтобы листенер не создавался и ворон не отсылался, если человек не прописал у себя адресатов. Нам нужен дополнительный кондишн на создание listener-а, который посылает ворона. Т.е. сам стартер нужен, потому что в нем может быть много разных вещей, помимо ворона. Но если не написано, куда ворон должен летать, тот просто не создается.
И третье — мы тоже хотим автокомплит, чтобы люди, которые к себе подтянули наш стартер, получили комплит на все свойства, которые считывает стартер.
Для каждого их этих заданий у нас есть свой инструмент. Но в первую очередь нужно посмотреть на существующие аннотации. Может быть нас что-то устроит?
И действительно, тут есть штуки, которые нам помогут. В первую очередь
Мы должны сделать так, чтобы все property начали автокомплититься. Хорошо бы, чтобы это автокомплитилось не только у людей, которые в своем application.yml будут их прописывать, но и в нашем стартере.
Назовем наше property «ворон». Он должен знать, куда лететь.
IDEA нам сообщает, что здесь что-то не так:
![](https://lh3.googleusercontent.com/Yv-4b6WMnKEhHooPYQkctJSQD6HCcZQHzahuCf9lmbPe-XBrlQVQAk-bS_7Rfanv-OcRX0GbaDwnlXAn0n43WhUDaOb86pUe3RT00HGQ0JQ_nKeWVaIWEvP_XZ5_oDMScFTwWv4W)
В документации написано, что у нас не добавлена зависимость (в Maven была бы не отсылка к документации, а кнопочка «добавить зависимость»). Просто добавим ее в нужный проект.
Теперь по мнению IDEA, у нас все есть.
Объясню, что за зависимость мы добавили. Все знают, что такое annotation processor. В упрощенном виде это такая штука, которая на этапе компиляции может что-то делать. Например, у Lombok есть свой annotation processor, который на этапе компиляции генерит кучу много полезного кода — сеттеры, геттеры.
Откуда берется автокомплит на property, которые находятся в application properties? Есть JSON-файл, с которым IDEA умеет работать. В этом файле описаны все property, которые IDEA должна уметь автокомплитить. Если вы хотите, чтобы property, которые вы придумали для стартера, IDEA тоже могла автокомплитить, у вас есть два пути:
Еще нужно не забыть
Выглядит все не очень, но так нужно делать, чтобы он появлялся чуть раньше, чем остальные бины (потому что остальные бины используют его property, чтобы настраивать себя).
В итоге надо было поставить две аннотации:
И еще надо не забыть геттеры и сеттеры. Они тоже важны, иначе ничего не работает — action не лезет.
В результате мы имеем файл, который можно в принципе написать вручную. Но вручную писать никто не любит.
Мы сделали первую часть задачи — у нас появились какие-то properties. Но к этим properties пока еще никто не относится. Теперь их надо поставить как condition для создания нашего listener.
Мы добавили еще один кондишн — ворон должен создаваться только при условии, что где-то кто-то рассказал, куда лететь.
Теперь пропишем, куда лететь, в application.yml.
Осталось в логике прописать, чтобы он летит туда, куда ему сказали.
Для этого мы можем сгенерировать конструктор. В новом Spring есть constructor injection — это рекомендуемый путь. Евгений любит делать
Но это не бесплатно. Вы с одной стороны получаете проверяемое поведение, с другой стороны получаете некоторый геморрой.
Нигде не стоит никакой
Spring советует писать так или с помощью Lombok. Jurgen Holler, который с 2002 года пишет 80% кода Spring, советует ставить
Чем мы заплатили за этот подход? Нам пришлось добавить
Итак, вороны отсылаются. Мы выполнили задание, которое дало возможность пользователям нашего стартера иметь комплит в их конфигурациях, при этом у нас появился бин, который включается и отключается в зависимости от этих конфигураций.
![](https://lh6.googleusercontent.com/DqQrETsVDtPOaHoAE0usEUNPuO4wDWsk1Ob5lTqcRWKeYUX7aEV_6tjKY67N4G1q2h-HOYqqFod8Kus5bBqczPtbkyv8JzqmS1RNZw3IOhYzom7qSVy-ssyKZ5RO_dmQU9eFadya)
Случается так, что нужно кастомизировать поведение стартера. Например, у нас есть свой черный ворон. А нужен белый, который курит, и мы хотим отправлять именно его, чтобы люди видели на горизонте дым.
Перейдем от аллегории к реальной жизни. Стартер принес мне кучу инфраструктурных бинов, и это прекрасно. Но мне не нравится, как они настроены. Я полез в application properties, и чего-то там изменил, и теперь мне все нравится. Но бывают ситуации, когда настройки настолько сложные, что проще самому прописать data source, чем пытаться разобраться в application properties. Т.е.мы хотим в бине, полученном от стартера, прописать data source самостоятельно. Что тогда будет?
Я прописал что-то сам, а стартер мне принес свой data source. У меня их теперь два? Или один другой задавит (и какой?)?
Мы хотим продемонстрировать вам еще один кондишн, позволяющий стартеру приносить какой-то бин, только если у человека, использующего стартер, такого бина нет. Как оказалось, это совсем нетривиально.
Есть огромное количество кондишенов, которые уже сделали до нас:
В принципе,
Если вы откроете большинство стартеров, то вы увидите, что там каждый бин, каждая конфигурация обвешана такой пачкой аннотаций. Мы просто пытаемся сделать аналог.
При попытке запуска ворон не отправился, зато появился Event, который мы написали в нашем новом listener —
![](https://lh6.googleusercontent.com/HGnM3ml91k2K_sCcR6QUeDuzR7DER9ICwKOmg9Hd5mnVGGoiniuHuW2aKD6KxlyehvywviCzx1Y4HUihwHlmvrhMwPFtNsjcQmch4aqh3Tagd5qIQVbc2QA2cVXkrcVzaokIKLeu)
Здесь есть два важных момента.
Первый момент — мы заэкстендились от нашего существующего listener, а не написали какой-то там listener:
Второе — мы сделали его именно с помощью компонента. Если бы мы сделали его в Java-конфигурации, т.е. прописали бы этот же класс, как бин конфигурации, у нас ничего бы не заработало.
Если я уберу
Зачем вам все это надо знать? Со Spring Boot все обычно супер, но только вначале. Когда проект продвигается, появляется один стартер, второй, третий. Какие-то вещи вы начинаете писать руками, потому что даже самый лучший стартер не даст то, что вам нужно. И начинаются конфликты бинов. Поэтому хорошо, если у вас будет хотя бы общее представление о том, как можно сделать так, чтобы один бин не создавался, и как прописать бин у себя, чтобы стартер не принес конфликта (или у вас есть два стартера, которые приносят один и тот же бин, чтобы они не конфликтовали между собой). Чтобы решить конфликт, я пишу свой бин, который сделает так, что ни первый, ни второй не создастся.
Более того, конфликт бинов — это хорошая ситуация, поскольку вы его видите. Если же мы указали одинаковые имена бинов, у нас конфликта не будет. Один бин просто затрет другой. И вы будете долго разбираться, где же там то, что было. Например, если мы сделаем какой-нибудь dataSource
Кстати, если стартер несет то, что вам не надо, просто сделайте бин с таким же ID и все. Правда потом если стартер в какой-то версии изменит название метода, то всё, у вас бина опять станет два.
У нас есть
Есть мыло, есть веревка — вешаем на виселице. Есть стул и ток — логично сажать человека на стул. Есть гильотина и хорошее настроение — значит нужно рубить головы.
Как же мы будем казнить?
Вопрос: Как вообще аннотации типа
Предположим, у меня есть метод, который будет создавать виселицу. Но бин из виселицы должен создаться, только если есть мыло и веревка. А мыла нет. Как мне понять, что нет именно мыла или нет именно веревки. Что произойдет, если я попытаюсь считать аннотации с метода, а эти аннотации ссылаются на классы, которых нет? Могу ли я считать такие аннотации?
Варианты ответа:
Это будет работать с помощью ASM. Увидев, что через reflection — никак, Spring будет парсить байт-код, условно, вручную. Он считывает файл, чтобы не делать преждевременную загрузку этого файла, и понимает, что там есть
Но Juergen Hoeller рекомендует не завязываться на имена классов, прописывая кондишены, несмотря на то, что есть аннотация
![](https://lh5.googleusercontent.com/faF46V8cuOSjmwC79jY3uGMuKGjbsrv-MuFajcR1uugZu4fplrcVXg_-SO7tFdYlYFRPy6B9EYKMeSXOe3ERyoJkGNM_Y97CmjqX8UP2iG5CzFsQ8vC71ElpAX2m9PR2tKPCdGH3)
Нам понадобилась еще одна property — возможность включить или отключить ворона вручную. Чтобы вообще никого не посылать гарантированно. Это последний кондишн, который мы вам покажем.
Наш стартер кроме ворона ничего не дает. Поэтому вы можете спросить, зачем иметь возможность его включать / выключать, можно же его просто не брать? Но во второй части в этот стартер будут набиваться дополнительные полезные вещи. А конкретно ворон может не понадобиться — он дорогой, его можно выключить. При этом убирать конечную точку, куда его отправить, как-то не очень — выглядит как костыль.
Поэтому мы все сделаем через
И он нам ругается, что так делать нельзя: Duplicate annotation. Проблема в том, что если у нас аннотация с какими-то параметрами, она не repeatable. Мы не можем сделать это по двум property.
У нас есть методы этой аннотации, там есть String, и это массив — там можно указать несколько property.
И все хорошо, пока вы не пытаетесь кастомизировать значение для каждого элемента в этом массиве отдельно. У нас property
То есть вот так делать нельзя:
Выделенное здесь — это не массив.
Есть извращение, которое позволяет работать с несколькими property, правда, только с одним значением для них:
![](https://lh6.googleusercontent.com/f-wPcucDHsKB7qUaPGuWhr0NLh6gElCcQMJYerZUcl0zQRW3Ajl5m_yHYhx97026B52xLyF6MSMPtEH94XTnCrFYaxZCGQ1lgZThw1QPvncs582o2b6RTf8cmjc94RybRrwHJt47)
Выглядит это, откровенно говоря, странно. Но это работает. Давайте попробуем сделать конфигурацию — новый кондишн, который будет учитывать и
Мы ставим нашу аннотацию
Создаем его.
Дальше мы должны были имплементить какой-то кондишн, но мы можем так не делать, потому что у нас есть следующая аннотация:
У нас есть композитный
При этом внутри нужно создать конструктор.
Теперь мы должны создать здесь статические классы. Мы делаем какой-то класс (назовем его R).
Делаем наше значение
Чтобы это повторить, достаточно запомнить название класса. У Spring хорошие Java-доки. Можно не выходить из IDEA, читать Java-док и понимать, что нужно сделать.
Мы поставила наш
При отсутствии
Я не хочу ставить
Все.
application.yml:
Запускаем, и наш ворон летит.
Таким образом мы можем делать композитные аннотации и помещать в них сколько угодно уже существующих аннотаций, даже если они не repeatable. Это работает на любой Java. Это нас спасет.
Во второй части статьи, которая выйдет в ближайшие несколько дней, мы остановимся на профилях и тонкостях запуска приложения.
Минутка рекламы. 19-20 октября состоится конференция Joker 2018, на которой Евгений Борисов вместе с Барухом Садогурским выступят с докладом «Приключения Сеньора Холмса и Джуниора Ватсона в мире разработки ПО [Joker Edition]», а Кирилл Толкачев с Максимом Гореликовым представят доклад «Micronaut vs Spring Boot, или Кто тут самый маленький?». В целом, на Joker будет ещё множество интересных и заслуживающих пристального внимания докладов. Приобрести билеты можно на официальном сайте конференции.
А ещё у нас для вас есть небольшой опрос!
![](https://habrastorage.org/webt/wj/p5/pf/wjp5pfvfgcnp_mda9gcpuiln3zs.png)
В основе статьи — доклад Евгения и Кирилла с нашей конференции Joker 2017. Под катом — видео и текстовая расшифровка доклада.
Конференцию Joker спонсируют много банков, поэтому представим, что приложение, на котором мы будем изучать работу Spring boot и создаваемый нами стартер, связано именно с банком.
Итак, предположим, поступил заказ на некое приложение от «Железного банка Браавоса». Обычный банк просто переводит деньги туда-сюда. Например, так (для этого у нас предусмотрен API):
http://localhost:8080/credit\?name\=Targarian\&amount\=100
А в «Железном банке» перед тем, как перевести деньги, необходимо, чтобы API банка просчитал, сможет ли человек их вернуть. Может быть, он не переживет зиму и возвращать будет некому. Поэтому там предусмотрен сервис, который проверяет надежность.
Например, если мы попытаемся перевести деньги Таргариену, операция будет одобрена:
А вот если Старку, то нет:
Ничего удивительного: Старки слишком часто умирают. А зачем переводить деньги, если человек не переживет зиму?
Посмотрим, как это выглядит внутри.
@RestController
@RequiredArgsConstructor
public class IronBankController {
private final TransferMoneyService transferMoney;
private final MoneyDao moneyDao;
@GetMapping("/credit")
public String credit(@RequestParam String name, @RequestParam long amount) {
long resultedDeposit = transferMoney.transfer(name, amount);
if (resultedDeposit == -1) {
return "Rejected<br/>" + name + " <b>will`t</b> survive this winter";
}
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);
}
}
Так где же спрятана вся магия?
Дело в том, что разработчики не любят думать о зависимостях, настраивать конфигурации, особенно если это XML конфигурации, и думать о том, как запускается их приложение. Поэтому Spring Boot решает эти проблемы за нас. Нам же остается только задача написать приложение.
Зависимости
Первая проблема, которая у нас всегда была — это конфликт версий. Каждый раз, когда мы подключаем разные библиотеки, которые ссылаются на другие библиотеки, появляются конфликты зависимостей. Каждый раз, когда я читал в интернете, что мне надо добавить какой-нибудь 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". Там все это подробно объяснялось.
Сегодня среди больших компаний достаточно модно стало писать такие огромные блоки dependency-менеджмента, где они указывают все версии своих продуктов и все версии продуктов, которые используют их продукты, и которые не конфликтуют друг с другом. И это называется bom. Эту штуку можно импортировать в ваш блок dependency-менеджмента без наследования.
А вот так это делается в Gradle (как обычно, то же самое, только проще):
dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE'
}
}
Теперь давайте поговорим про сами зависимости.
Что мы пропишем в приложении? Dependency-менеджмент — хорошо, но мы хотим, чтобы у приложения были определенные способности, например, чтобы оно отвечало по HTTP, чтобы была БД или поддержка JPA. Поэтому все, что нам нужно сейчас — это получить три зависимости.
Раньше это выглядело так. Я хочу работать с БД и начинается: transaction менеджер какой-то нужен, соответственно нужно модуль spring-tx. Мне нужен какой-нибудь hibernate, поэтому требуется EntityManager, hibernate-core или еще что-то. Я все настраиваю через Spring, значит нужен spring 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, и т.п. — нужно было настраивать руками. Естественно, все они попадали в контекст.
В ходе доклада "Spring-потрошитель" мы в main-е создавали контекст, и если это был xml-ный контекст, он изначально был пустой. Если же мы строили контекст через
AnnotationConfigApplicationContext
, туда попадали некоторые beanpostprocessor-ы, которые умели настраивать бины согласно аннотациям, но контекст тоже был практически пустой.А вот сейчас в main-е есть
SpringApplication.run
и не видно никакого контекста:@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);
}
Вопрос: что еще можно туда передать?
Варианты:
RipperApplication.class
String.class
"context.xml"
new ClassPathResource("context.xml")
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; могу указать, какие пакеты просканировать или передать класс, помеченный какими-то аннотациями. То есть у меня есть все возможности.
Однако это Spring Boot. Мы еще ни одного бина не создали, ни одного класса не написали, у нас есть только main, а в нем — наш Карлсон —
SpringApplication.run
. На вход он получает класс, помеченный какой-то Spring Boot аннотацией.Если в этот контекст заглянуть, что там будет?
В нашем приложении после подключения пары стартеров было 436 бинов.
Почти 500 бинов только для того, что начать писать.
Далее мы поймем, откуда взялись эти бины.
Но в первую очередь мы хотим сделать так же.
Магия стартеров, кроме того, что они нам решили все проблемы с зависимостями, заключаются в том, что мы подключили всего 3-4 стартера, и у нас 436 бинов. Подключили бы 10 стартеров, бинов было бы сильно больше 1000, потому что каждый стартер, кроме зависимостей, уже приносит конфигурации, в которых прописаны какие-то необходимые бины. Т.е. вы сказали, что хотите стартер для веба, значит нужен диспатчер сервлет и
InternalResourceViewResolver
. Подключили стартер jpa — нужен EntityManagerFactory
бин. Все эти бины эти уже где-то в конфигурациях стартеров прописаны, и они магическим образом приходят в приложение без каких-то действий с нашей стороны.Чтобы понять, как это работает, мы сегодня будем писать стартер, который тоже будет приносить во все приложения, которые этим стартером будут пользоваться, инфраструктурные бины.
Железный закон 1.1. Всегда посылай ворона
Давайте посмотрим на требование от заказчика. У Железного банка есть много разных приложений, запущенных в разных филиалах. Заказчики хотят, чтобы каждый раз, когда поднимается приложение, посылался ворон — информация о том, что приложение поднялось.
Начнем писать код в приложении конкретного Железного банка (Iron bank). Будем писать стартер, чтобы все приложения Iron Bank, которые будут зависеть от этого стартера, смогли автоматически посылать ворона. Мы помним, что стартеры позволяют нам автоматически подтягивать зависимости. А главное, мы не пишем практически никакой конфигурации.
Мы делаем listener, который слушает, что контекст обновился (последний event), после чего отправляет ворона. Будем слушать
ContextRefreshEvent
.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.
Запускаем наше приложение. Ворон должен взлететь в тот момент, когда оно поднимется, хотя в самом приложении мы ничего не сделали. С точки зрения инфраструктуры мы, конечно, написали и сконфигурировали стартер. Но с точки зрения разработчика мы просто подключили зависимость и появилась конфигурация — ворон полетел. Все, как мы и хотели.
Это не магия. Инверсия контроля не должна быть магией. Также, как не должно быть магией использование Spring. Мы знаем, что это фреймворк в первую очередь для inversion of control. Как есть инверсия контроля для вашего кода, так есть и инверсия контроля для модулей.
@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
— они автоматически сканируются, поскольку процесс сканирования запускает ваша главная конфигурация. На самом деле
@SpringBootApplication
не делает ничего нового. Он просто собрал все best practice, которые были в приложениях на Spring, благодаря чему это теперь некоторая композиция аннотаций, в том числе и @ComponentScan
.Кроме этого тут есть еще вещи, которых не было раньше —
@EnableAutoConfiguration
. Именно этот класс я прописывал в spring.factories.@EnableAutoConfiguration
, если разобраться, несет с собой @Import
:@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
. В обычном Spring мы пишем Import(Some Configuration.class)
какой-то конфигурации и она загружается, как и все зависимые от нее. Это же ImportSelector
, это не конфигурация. ImportSelector
протаскивает все наши стартеры в контекст. Он обрабатывает аннотацию @EnableAutoConfiguration
из spring.factories, которая выбирает, какие конфигурации загрузить, и добавляет в контекст те бины, которые мы прописали в IronConfiguration. Как он это делает?
В первую очередь использует незамысловатый утилитный класс, SpringFactoriesLoader, который смотрит на spring.factories и грузит все из то, что попросят. У него есть два метода, но они не сильно отличаются.
Spring Factories Loader существовал еще в Spring 3.2, просто им никто не пользовался. Его, видимо, написали, как потенциальное развитие фреймворка. И вот он перерос в 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, в которых прописаны их кишки. Мы будем делать именно так, мы в принципе по-другому не можем сделать.
Кроме этого есть еще часть конфигураций, прописанных в самом Spring Boot, которые грузятся всегда — их еще 90 штук. Также есть еще штук 30 конфигураций, которые просто захардкожены в Spring Boot.
Все это дело поднимается, а потом конфигурации начинают фильтроваться. В конце 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, который на этапе компиляции генерит кучу много полезного кода — сеттеры, геттеры.
Откуда берется автокомплит на property, которые находятся в application properties? Есть JSON-файл, с которым IDEA умеет работать. В этом файле описаны все 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 — это рекомендуемый путь. Евгений любит делать
@Autowired
, чтобы все оказывалось в приложении через reflection. Я же люблю следовать тем конвенциям, которые предлагает Spring:@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, чем пытаться разобраться в application properties. Т.е.мы хотим в бине, полученном от стартера, прописать data source самостоятельно. Что тогда будет?
Я прописал что-то сам, а стартер мне принес свой 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
работать не будет. Но т.к. класс называется также, при попытке его создать мы можем написать ravenListener
— также как у нас было в нашей конфигурации. Выше мы акцентировали внимание на том, что имя бина в Java-конфигурации будет по названию метода. И в этом случае у нас создается бин, который называется 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 это считать нельзя. У вас будет exception, и вы не будете знать, нет ли у вас мыла или веревки — что там вообще случилось. Как работает reflection? Если вы попросите у метода все его аннотации, и хотя бы одна из аннотаций, которые над методом стоят, ссылается на отсутствующий класс, ничего вы не получите — у вас
ClassDefNotFound
. Это будет работать с помощью ASM. Увидев, что через reflection — никак, Spring будет парсить байт-код, условно, вручную. Он считывает файл, чтобы не делать преждевременную загрузку этого файла, и понимает, что там есть
@Conditional
с мылом, веревкой. Он уже может проверить наличие этих классов в контексте отдельно. Но ASM — это, как говориться, не про скорость. Это возможность считать класс, не загрузив его, и понять методную информацию.Но Juergen Hoeller рекомендует не завязываться на имена классов, прописывая кондишены, несмотря на то, что есть аннотация
OnMissingClass
, которая в качестве параметра может принимать название класса (String). Если следовать этой рекомендации, все работает быстрее, и ASM не нужен. Но, судя по исходникам, никто так не делает. Железный закон 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-док и понимать, что нужно сделать.
Мы поставила наш
@ConditionalOnRaven
. В принципе, туда можно завернуть и @ConditionalOnProduction
, и @ConditionalOnMissingBean
, но сейчас мы этого делать не будем. Просто посмотрим, что у нас получилось.@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
Запускаем, и наш ворон летит.
Таким образом мы можем делать композитные аннотации и помещать в них сколько угодно уже существующих аннотаций, даже если они не repeatable. Это работает на любой Java. Это нас спасет.
Во второй части статьи, которая выйдет в ближайшие несколько дней, мы остановимся на профилях и тонкостях запуска приложения.
Минутка рекламы. 19-20 октября состоится конференция Joker 2018, на которой Евгений Борисов вместе с Барухом Садогурским выступят с докладом «Приключения Сеньора Холмса и Джуниора Ватсона в мире разработки ПО [Joker Edition]», а Кирилл Толкачев с Максимом Гореликовым представят доклад «Micronaut vs Spring Boot, или Кто тут самый маленький?». В целом, на Joker будет ещё множество интересных и заслуживающих пристального внимания докладов. Приобрести билеты можно на официальном сайте конференции.
А ещё у нас для вас есть небольшой опрос!
Only registered users can participate in poll. Log in, please.
Вы пойдёте на Joker?
23.26% Да, уже купил билет10
4.65% Собираюсь пойти, коплю на билет2
11.63% Собираюсь пойти, жду, пока компания купит мне билет5
0% Еще не решил, пойду или нет0
53.49% Нет, я не пойду на Joker23
6.98% Собираюсь пойти, жду 1 октября, чтобы купить дороже3
43 users voted. 18 users abstained.