Spring (Boot) lessons part 6 - Introduction to Spring-Boot.
Перед началом изучения Spring Boot желательно прочитать официальную документацию или краткие статьи.
В папке DOC sql-скрипты и др. полезные файлы:
- DOC/SpringBootArticleShort.txt - краткая статья о SpringBoot;
- DOC/SpringBootStarters.txt - кратко о SpringBoot стартерах;
Док. для изучения:
- Spring Boot Reference Documentation ;
- Spring Framework 6.1.5 Documentation ;
- Spring Framework 3.2.x Reference Documentation ;
- Getting Started Guides ;
- Developing with Spring Boot ;
Для начала проведем предварительную подготовку (в последствии, она существенно изменится):
Шаг 1. - в файле build.gradle добавим необходимые зависимости:
/* Подключим Spring-core и Spring-context. */
implementation 'org.springframework:spring-core:5.3.22'
implementation 'org.springframework:spring-context:5.3.22'
Шаг 2. - подключаем Jakarta Annotation API и стандартные аннотации JSR 330:
implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5'
implementation 'javax.inject:javax.inject:1'
Шаг 3. - создаем (переносим из прошлого проекта) файл настроек ApplicationConfiguration.java, в котором используя JAVA и аннотации настраиваем наши bean-ы.
Ниже и далее по мере углубления в Spring Boot будет происходить трансформация нашего build.gradle файла.
Lesson 24 - @Conditional
Самым распространенным способом управления контекстом Spring являются @Profile. Они позволяют быстро и просто регулировать создание bean-ов. Но иногда может потребоваться более тонкая настройка и это позволяет аннотирование классов, интерфейсов и методов через @Conditional - аннотация, которая указывает, что компонент имеет право на регистрацию только в том случае, если все указанные условия соответствуют.
Примеры (аннотирование класса):
@Configuration
@Conditional(IsDevEnvCondition.class)
class DevEnvLoggingConfiguration {
// ...
}
Или отдельного метода:
@Configuration
class DevEnvLoggingConfiguration {
@Bean
@Conditional(IsDevEnvCondition.class)
LoggingService loggingService() {
return new LoggingService();
}
}
Условие (condition) - это любое состояние, которое может быть определено программно до того, как определение компонента должно быть зарегистрировано (см. Interface Condition).
Интерфейс Condition - функциональный, поэтому его можно использовать в качестве цели назначения для лямбда-выражения или ссылки на метод. Единичное условие, которое должно быть выполнено для регистрации компонента. Условия проверяются непосредственно перед регистрацией определения компонента и имеют право наложить вето на регистрацию на основании любых критериев, которые могут быть определены на этом этапе.
Условия должны соответствовать тем же ограничениям, что и BeanFactoryPostProcessor, и не должны взаимодействовать с экземплярами компонента. Для более детального контроля над условиями, которые взаимодействуют с bean-компонентами @Configuration, рассмотрите возможность реализации интерфейса ConfigurationCondition.
Несколько условий для данного класса или данного метода будут упорядочены в соответствии с семантикой интерфейса Ordered Spring и аннотации @Order. Подробности см. в разделе AnnotationAwareOrderComparator.
Аннотация @Conditional может использоваться любым из следующих способов:
- как аннотация уровня типа для любого класса, прямо или косвенно аннотированного с помощью @Component, включая классы @Configuration;
- как мета-аннотации, с целью создания пользовательских стереотипных аннотаций;
- как аннотация уровня метода для любого метода @Bean;
Если класс @Configuration помечен как @Conditional, все методы @Bean, аннотации @Import и аннотации @ComponentScan, связанные с этим классом, будут подчиняться этим условиям.
- Наследование аннотаций @Conditional не поддерживается.
- Любые условия супер-классов или переопределенных методов учитываться не будут.
- Чтобы обеспечить соблюдение этой семантики, сам @Conditional не объявляется как @Inherited, кроме того, любая составленная пользователем аннотация, мета-аннотированная с помощью @Conditional, не должна объявляться как @Inherited.
Краткая документация: DOC/ConditionalAnnotationSpring.txt и DOC/ConditionalONTable.txt
Создадим свои условия:
-
Шаг 1. - Создадим конфигурационный файл для управления bean-ами при взаимодействии с БД (имитация) - JpaConfiguration.java;
-
Шаг 2. - Создадим файл условий condition/JpaCondition.java;
-
Шаг 3. - Реализуем метод *.matches() в нашем классе условий JpaCondition, из которого получим: true - в случае удачной загрузки драйверов PostgreSQL (и тогда сработает наш класс конфигурации JpaConfiguration), false - в случае неудачной загрузки драйвера БД PostgreSQL (и тогда не будет применена конфигурация JpaConfiguration);
-
Шаг 4. - Чтобы все прошло нормально пропишем необходимую зависимость с PostgreSQL в build.gradle (без нее приложение не найдет драйвер БД и мы не сможем запустить JpaConfiguration исходя из наших же условий):
implementation 'org.postgresql:postgresql'
И так, в итоге, у нас есть:
- JpaConfiguration.java - некий класс конфигурации для связи с БД через JPA. Он аннотирован как @Configuration - конфигурационный bean, а также мы пометили его как @Conditional(JpaCondition.class).
- JpaCondition.java - класс, который определяет, будет ли активирован конфигурационный bean в зависимости от выполнения условий, в нашем случае это - загружен ли драйвер PostgreSQL или нет.
Оба класса снабжены простой логикой для отображения на экране результата работы демо-приложения AppRunner.java. Т.е. мы сможем увидеть находится ли bean JpaConfiguration в контексте или нет.
Считается, что это довольно просто и для этого нужно:
- Выбрать версию Spring Boot;
- Добавить или создать Spring Boot Starter;
- Добавить нужные свойства;
- Добавить или удалить нужные Spring Boot Starters Bean-ы; Хотя шаги примерно одинаковые, реализовать их можно несколькими способами.
*** Шаг 1 - Настраиваем версию Spring ***
Идем в официальную документацию - Spring Boot Gradle Plugin Reference Guide.
Для настройки нашего Gradle нам нужен plugin: id 'org.springframework.boot' version '3.1.3' - он добавляет необходимые задачи в Gradle и имеет обширную взаимосвязь с другими plugin-ами. Так же нам понадобится менеджер зависимостей: 'io.spring.dependency-management', который позволяет решать проблемы несовместимости различных версий и модулей Spring-а (см. build.gradle, замечу, могут быть подключены не последние версии).
plugins {
id 'org.springframework.boot' version '3.1.3'
id "io.spring.dependency-management" version '1.0.11.RELEASE'
}
После подключения plugin-a в Gradle Tasks появились новые задачи с префиксом 'boot' - это вспомогательные задачи для создания, build-а и запуска нашего Spring Boot приложения.
Документация по менеджеру зависимостей Spring - Dependency Management Plugin.
*** Шаг 2 - Добавляем наш первый Spring Starter ***
Заглянем в официальную документацию - Spring Boot Reference Documentation (6.1.5. Starters)
Видим, что основным корневым (основным, Core) стартером является - 'spring-boot-starter' - он включающий поддержку авто-конфигурации, логирование и YAML (так же см. DOC/SpringBootStarters.txt).
Подключим его в нашем build.gradle:
implementation 'org.springframework.boot:spring-boot-starter'
При этом версии мы не указываем, все необходимые данные о совместимых версиях будут браться из плагинов загруженных на первом шаге.
Уже на этом этапе многие зависимости уходят из build.gradle, т.к. они будут транзитивно подтягиваться при помощи заданных плагинов и SpringBoot Starter-a. Также наиболее подходящая версия зависимости для подключенной БД, тоже будет получена из плагинов Spring. Так же нам хватит аннотаций самого Spring-a.
И так по сравнению в предыдущими версиями build.gradle слегка похудел.
*** Шаг 3 - Создаем точку входа в Spring приложение ***
В корне нашего проекта создаем FirstSpringAppRunner.java. Сам класс должен быть аннотирован - @SpringBootApplication. Все, можем его запустить и посмотреть, как ведут себя наши bean-ы созданные и настроенные совершенно под другие нужны.
Второй вариант создания Spring Boot приложения реализуется, либо средствами IDE, например, IntelliJ IDEA версии Ultimate, либо через сайт https://start.spring.io/, который использует та же среда разработки (кому как удобно).
В прошлом уроке мы сделали наше первое приложение. Первая особенность - точка входа должна находится в рутовой директории. Т.е. наш проект, например, находится в 'spring.oldboy', тут же мы ищем все наши bean-ы если в конфигурационных файлах нет указания на другие параметры, и тут же в корне находится 'запускаемый файл' нашего приложения - FirstSpringAppRunner.java.
Далее, все остальные пакеты, классы и т.д., дополняющие и расширяющие наш проект, которые будут использоваться в нашем приложении в будущем, и которые могут быть bean-ами, должны находится внутри зависимого пакета, в корне которого лежит наш запускающий приложение класс.
Нужно помнить, что наше приложение решает конкретные задачи и ограничено СВОИМ ПАКЕТОМ и только о нем идет речь, не о неком глобальном проекте, где пакетов и приложений соответственно может быть несколько и точек входа тоже несколько.
- Аннотация @SpringBootApplication, который помечен наш 'запускающий приложение класс' (у нас это FirstSpringAppRunner.java) должна быть единственной на все наше приложение. Она может ставиться только над классом.
- Важно соблюдать структуру приложения и расположения классов bean-ов внутри пакета где располагается 'точка входа в приложение', поскольку именно в этом пакете ComponentScan будет проводить поиск bean-ов.
- Автоматически будут загружаться файлы с именем application.properties (*.yaml или *.yml)
Если заглянуть в @SpringBootApplication:
/* Может помечать только классы */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
/* Т.е. это фактически метка файла конфигурации, с особнностью - уникальность на весь проект */
@SpringBootConfiguration
/*
Автоматическое конфигурирование нашего приложения, одноко используя методы *.exclude() или
*.excludeName() мы можем изьят из конфигурирования ненужные нам классы.
*/
@EnableAutoConfiguration
/*
Настройка CompanentScan-а по-умолчанию нас устраивает и мы легко можем убрать ее из других
классов помеченных как @Configuration. Еще раз, классов с аннотацией @Configuration может быть
несколько и только с @SpringBootConfiguration один единственный.
В данном случае сканирование bean-ов будет происходит внутри того пакета, в котором расположен
наш класс 'точка входа', в данном примере это FirstSpringAppRunner.java.
*/
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
И так, мы имеем небольшой запускаемый файл и большой набор папок и настроек, которые определяют логику работы нашего приложения.
Для подключения аннотаций Lombok мы можем воспользоваться зависимостями для Gradle:
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.28'
annotationProcessor 'org.projectlombok:lombok:1.18.28'
testCompileOnly 'org.projectlombok:lombok:1.18.28'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.28'
}
С официального сайта разработчика - https://projectlombok.org/setup/gradle, либо же мы можем подтянуть plugin, чтобы не интегрировать 4-и строки в наш build.gradle:
plugins {
id "io.freefair.lombok" version "8.3"
}
С сайта Gradle - https://plugins.gradle.org/plugin/io.freefair.lombok.
Теперь мы можем убрать 'ванильный' Java код из некоторых наших классов и использовать Lombok аннотации для создания конструкторов, геттеров, сеттеров и т.п.
В других учебных проектах мы уже касались работы с Lombok (краткий обзор) - Fast_Lombok.txt
Если с основной массой простых классов расстановка аннотаций решает вопрос сокращения рутинного кода, то в более сложных конфигурациях связанного кода придется донастроить работу Lombok используя файл конфигурации lombok.config (см. https://projectlombok.org/features/configuration). Данный файл создается нами и помещается в корень всего Gradle проекта, а не только Spring приложения.
В нем мы используя специальный: config.stopBubbling = true - ключ, сообщающий Lombok, что это наш корневой каталог. Затем мы можем создать lombok.config файлы в любых подкаталогах (обычно представляющих проекты или исходные пакеты) с различными настройками. Но, пока, обойдемся корневым. В данном файле указываем:
lombok.copyableannotations += org.springframework.beans.factory.annotation.Value
lombok.copyableannotations += org.springframework.beans.factory.annotation.Qualifier
Тут мы указали какие аннотации мы хотим, перенести с полей в сгенерированные конструкторы. У нас это аннотации от Spring (@Value и @Qualifier). Важно указать полный путь к используемым Spring аннотациям.
Док. для изучения:
В Spring-e есть масса возможностей передавать свойства и настройки при помощи файлов 'properties' имеющих зарезервированное название или расширение, см. External Application Properties
Создадим файл свойств (с тестовым параметром) - resources/spring.properties, используя не хитрый код в FirstSpringAppRunner.java:
String sp = SpringProperties.getProperty("test.message");
System.out.println(sp);
Получим в консоли то, что поместили в параметр "test.message". См. док. Class SpringProperties.
Spring Boot использует особый PropertySource порядок, предназначенный для разумного переопределения значений. Более поздние источники свойств могут переопределять значения, определенные в более ранних. Т.е. например, настройки переданные через аргументы командной строки (пункт 11) затрут (будут применены преимущественно) настройки переданные через данные конфигурационного файла - application.properties (пункт 3) см. ниже.
Нужно четко понимать, ни один из параметров не игнорируется, он просто меняется (при совпадении ключа) в соответствии с приоритетом передающего источника.
Источники по приоритету рассматриваются в следующем порядке см. Externalized Configuration:
- Свойства по умолчанию (задаются настройкой SpringApplication.setDefaultProperties).
- @PropertySource аннотации к нашим @Configuration классам. !!! Внимание, такие источники свойств не добавляются в Environment до тех пор, пока контекст приложения не будет обновлен. Слишком поздно настраивать определенные свойства, например logging.* и spring.main.* которые считываются до начала обновления !!!
- Данные конфигурации (например, application.properties файлы).
- A RandomValuePropertySource, имеющий свойства только в random.*.
- Переменные среды ОС.
- Свойства системы Java ( System.getProperties()).
- Атрибуты JNDI из java:comp/env.
- ServletContext параметры инициализации.
- ServletConfig параметры инициализации.
- Свойства из SPRING_APPLICATION_JSON (встроенный JSON, встроенный в переменную среды или системное свойство).
- Аргументы командной строки.
- properties атрибут в наших тестах. Доступны @SpringBootTest и тестовые аннотации для тестирования определенной части нашего приложения.
- @DynamicPropertySource аннотации в наших тестах.
- @TestPropertySource аннотации к нашим тестам.
- Свойства глобальных настроек Devtools в $HOME/.config/spring-boot каталоге, когда devtools активен.
Файлы конфигурационных данных рассматриваются в следующем порядке:
- Свойства приложения, упакованные внутри нашего jar (application.properties и варианты YAML).
- Свойства приложения, специфичные для профиля, упакованные внутри вашего jar (application-{profile}.properties и варианты YAML).
- Свойства приложения за пределами упакованного jar-файла (application.properties и варианты YAML).
- Свойства приложения, специфичные для профиля, за пределами упакованного jar-файла (application-{profile}.properties и варианты YAML).
Исследуем поведение системы (например, настройку параметра db.pool.size). У нас есть обычный application.properties и в порядке приоритетности он имеет 'рейтинг' = 1. Создадим дополнительно application-qa.properties, в данном случае это файл свойств специфичный для профиля, а он имеет 'рейтинг' = 2 см. выше. И значит данные из более приоритетного источника заменят данные из менее приоритетного с тем же ключом. Запускаем и проверяем.
Док. для изучения:
Смотреть краткий обзор: DOC/YAML_SHORT_REVIEW.txt
При добавлении:
implementation 'org.springframework.boot:spring-boot-starter'
в наш build.gradle, мы автоматом подхватили и зависимость 'org.yaml: snakeyaml: 1.33' - библиотека для работы с файлами *.yaml или *.yml.
На момент прохождения данного урока у нас есть два файла расширения *.properties со следующим содержанием: application.properties ->
db.username=postgres
db.password=pass
db.pool.size=18
db.driver=PostgresDriver
db.url=postgres:5432
db.hosts=localhost,127.0.0.1
spring.profiles.active=development,qa
application-qa.properties ->
db.pool.size=36
Перепишем их в YAML формате, т.е. кроме смены расширения они естественно изменять и внутренний код см. application.yml и application-qa.yml. Для проверки работоспособности настроек с использованием YAML можно запустить FirstSpringAppRunner.java в любом из режимов.
Однако не стоит заблуждаться насчет файла ApplicationConfiguration.java и работоспособности его аннотаций:
@Configuration
@PropertySource("classpath:properties_for_lesson_24/application.properties")
@PropertySource("classpath:properties_for_lesson_24/application-qa.properties")
@ComponentScan(basePackages = "spring.oldboy")
Он продолжает влиять на формирование контекста и на работоспособность не только AppRunner.java, но и FirstSpringAppRunner.java. Так же, существует мнение, что использовать 'винегрет' из файлов настроек различного расширения *.properties и *.yaml в одном проекте, вещь весьма сомнительная - т.е. лучше, либо то, либо другое и не нарушать принятую структуру Spring Boot приложения.
Lesson 30 - @ConfigurationProperties.
Spring Boot предоставляет нам возможность представлять содержимое файлов *.yaml или *.yml не как примитивы, а как объекты или 'конфигурационные объекты', и далее превратить его в bean:
- DatabaseProperties.java - в таком виде мы можем передавать его в любое место нашего приложения и получать из него данные.
И так, первый вариант, как сделать из нашего класса свойств базы данных BEAN прост и уже описан:
- Создаем сам класс (см. DatabaseProperties.java);
- Делаем его POJO, содержащим геттеры и сеттеры, а так же конструктор без параметров;
- В одном из конфигурационных файлов (например, JpaConfiguration.java) создаем метод возвращающий наш класс и аннотируем его как @Bean, а так же проводим соответствие с application.yml (файл параметров содержащих настройки нашей БД и т.д.) используя аннотацию @ConfigurationProperties(prefix = "db")
Тестируем получившуюся конфигурацию контейнера в FirstSpringAppRunner.java.
Но, существует еще способы!
Второй вариант, мы НЕ БУДЕМ реализовывать в коде, а опишем тут. И так, для начала мы удалим наш bean из JpaConfiguration.java. Но bean из DatabaseProperties нам все еще нужен - заставим Spring просканировать наш класс - аннотируем его @Component, естественно аннотация @ConfigurationProperties(prefix = "db") нам тоже понадобится:
@Data
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "db")
public class DatabaseProperties {
private String username;
private String password;
private String driver;
private String url;
private String hosts;
private PoolProperties pool;
private List<PoolProperties> pools;
private Map<String, Object> properties;
@Data
@NoArgsConstructor
public static class PoolProperties {
/* Нужные нам поля */
private Integer size;
private Integer timeout;
}
}
В таком варианте мы также получим необходимый нам bean.
Третий вариант, очень похож на второй с той лишь разницей, что мы убираем из нашего класса DatabaseProperties аннотацию @Component. А поскольку мы 'удалили метод аннотированный как @Bean' и из JpaConfiguration.java:
@Bean
@ConfigurationProperties(prefix = "db")
public DatabaseProperties databaseProperties() {
return new DatabaseProperties();
}
И не разместили его ни в одном из доступных configuration классов, но, нам все еще нужно, чтобы Spring его просканировал и сделал из него BEAN. Для этого нам придется аннотировать наш класс 'точку входа в приложение' - FirstSpringAppRunner.java как @ConfigurationPropertiesScan:
package spring.oldboy;
@SpringBootApplication
@ConfigurationPropertiesScan
public class FirstSpringAppRunner {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(FirstSpringAppRunner.class, args);
/* some learning code */
}
}
И тогда Spring будет сканировать в пакете 'spring.oldboy' все классы помеченные как @ConfigurationProperties
Все три способа создания BEAN-a из нашего класса DatabaseProperties работают, однако конфигурация класса в исходном виде делает его изменяемым (immutable). Если же мы захотим сделать его неизменяемым то, нам придется применить обычный класс 'record' см. ImmutableDatabaseProperties.java