Как создать свой Spring Boot 3 — стартер?

от автора

Что вообще такое starter?

Допустим, Вы разрабатываете несколько приложений или микросервисов на Java. Каждое из них уникальное, и содержит свою собственную бизнес логику. Однако, в каждом из них может быть необходимость использовать общую логику. Например, логику аутентификации, как это часто бывает в мире микросервисов.

Есть несколько способов реализовать общую логику в нескольких приложениях:

  1. можно реализовать её в каждом из приложений

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

  3. можно вынести её в отдельную библиотеку, и подключать её как отдельную зависимость

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

Второй подход часто применяется в случае, когда разработка систем ведётся в монорепозиториях. С одной стороны, это означает, что изменив общую логику, разработчику необходимо изменить и все места в системе (все микросервисы и приложения, использующие общую логику). С другой стороны, без знания контекста использования общей логики в разных местах — вносить изменения рискованно.

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

Так что же такое — starter’ы в Spring Boot? Это и есть отдельные библиотеки, со своим релизным циклом, которые позволяют использовать фишки самого Spring’а. Конкретно, в них могут содержаться заранее сконфигурированные Spring бины, которые решают какую-либо задачу (например, интеграция с чем-либо).

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

Примеры starter’ов

Возможно, Вы и не задумывались, но каждый раз, когда Вы пользуетесь Spring Initializr’ом, добавляя зависимости в проект, Вы добавляете starter’ы. Например:

  • spring-boot-starter-web — для работы с Spring Web и MVC

  • spring-boot-starter-security — для организации защищённого доступа к ресурсам сервиса

  • или spring-boot-starter-test — для возможности написания тестов с подъёмом Spring контейнера

Как создать свой starter?

Это не сложно.

Давайте разберём на примере — создадим стартер для получения информации по погоде в выбранной локации.

Полный код проектов стартера и клиентского приложения доступен на GitHub

1. Создадим новый Maven проект

Для этого воспользуемся прелестями автогенерации Maven:

mvn archetype:generate

2. Добавим необходимые зависимости в pom.xml

Следующие строки нужно вставить в dependencies секцию pom.xml:

<dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter</artifactId>       <version>3.2.4</version>       <optional>true</optional> </dependency>

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

Любые стартеры, как и обычные java-библиотеки, могут иметь собственные зависимости. Добавим ещё одну зависимость, которая позволит нам запрашивать погодные данные из сервиса OpenWeatherMap.

<dependency>     <groupId>com.github.prominence</groupId>     <artifactId>openweathermap-api</artifactId>     <version>2.3.0</version> </dependency>

3. Добавим сервисы

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

Для этих целей добавим в стартер следующий класс:

// Именно этот класс мы будем использовать в клиентском приложении для получения информации по погоде public class WeatherService {      /**      * Название города, для которого мы будем получать информацию по погоде.      */     private final String defaultCity;     /*      * OpenWeatherMap клиент из библиотеки openweathermap-api.      */     private final OpenWeatherMapClient client;      public WeatherService(String defaultCity, OpenWeatherMapClient client) {         this.defaultCity = defaultCity;         this.client = client;     }      /*      * Получить текстовое описание погоды для выбранного города.      * @return строка с описанием текущей погоды      */     public String getTemperature() {         CurrentWeatherRequester requester = client.currentWeather();          SingleResultCurrentWeatherRequestTerminator terminator = requester.single()                 .byCityName(defaultCity)                 .unitSystem(UnitSystem.METRIC) // позволяет получать замеры в Цельсиях                 .retrieve();          Temperature temperature = terminator.asJava().getTemperature();          return temperature.toString();     } } 

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

Для нормальной работы WeatherService потребуется откуда-то взять название города, да и OpenWeatherMapClient без SDK-ключа не получить. Поэтому, далее займёмся настройками.

4. Добавим конфигурацию

Добавим default.yaml файл (название может быть любое) в директорию resources:

openweathermap-starter:   # SDK-ключ для OpenWeatherMap     sdk-key: bd5e378503939ddaee76f12ad7a97608   # Город, для которого стартер будет поставлять погодные данные   city: Moscow

Ключ SDK любезно предоставлен Интернетом. На момент написания статьи — работает. Собственный SDK-ключ можно получить на сайте OpenWeatherMap.

Свяжем нашу конфигурацию с кодом с помощью нового класса — OpenWeatherMapProperties:

@ConfigurationProperties(prefix = "openweathermap-starter") public class OpenWeatherMapProperties {     private String sdkKey;     private String city;      public String getSdkKey() {         return sdkKey;     }      public void setSdkKey(String sdkKey) {         this.sdkKey = sdkKey;     }      public String getCity() {         return city;     }      public void setCity(String city) {         this.city = city;     } }

Затем, необходимо добавить класс авто-конфигурации в наш стартер. Класс авто-конфигурации используется Spring’ом на старте приложения, для того, чтобы проинициализировать имеющиеся в стартере бины и зарезолвить их зависимости. Создадим класс OpenWeatherMapAutoConfiguration:

@Configuration @EnableConfigurationProperties(OpenWeatherMapProperties.class) public class OpenWeatherMapAutoConfiguration {     @Bean     @ConditionalOnMissingBean     public WeatherService weatherService(OpenWeatherMapProperties properties) {         return new WeatherService(properties.getCity(), new OpenWeatherMapClient(properties.getSdkKey()));     } } 

Класс авто-конфигурации необходимо прописать в специальном файле. Для этого необходимо в директории resourcesсоздать папку META-INF, в ней создать ещё одну — spring, и уже в ней разместить файл под названием org.springframework.boot.autoconfigure.AutoConfiguration.imports со следующим содержимым:

ru.fullstackguy.config.OpenWeatherMapAutoConfiguration

Уже на данном этапе, мы можем выполнить сборку стартера, установить его в клиентское приложение и, задав конфигурацию в application.yaml приложения, начать получать погодную информацию.

Однако, чтобы дефолтные настройки стартера из default.yaml использовались в приложении, потребуется выполнить ещё два действия.

Чтобы Spring знал о дефолтных настройках стартера, необходимо их зачитать. Для этого нужно добавить ещё один класс в проект стартера — EnvPostProcessor:

public class EnvPostProcessor implements EnvironmentPostProcessor {     private final YamlPropertySourceLoader propertySourceLoader;      public EnvPostProcessor() {         propertySourceLoader = new YamlPropertySourceLoader(); // Yaml..Loader зачитает для нас конфигурацию из default.yaml     }      @Override     public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {         var resource = new ClassPathResource("default.yaml"); // определяем default.yaml как локальный ресурс         PropertySource<?> propertySource = null;         try {             // и просим Yaml...Loader зачитать настройки из файла             propertySource = propertySourceLoader.load("openweathermap-starter", resource).get(0);         } catch (IOException e) {             throw new RuntimeException(e);         }         // прочитанные настройки проставляются в настройки окружения Spring'а         environment.getPropertySources().addLast(propertySource);     } } 

Обратите внимание, класс EnvPostProcessor не отмечен никакими специальными аннотациями. Именно по этой причине, чтобы Spring о нём узнал, нам понадобиться добавить файл spring.factories в директорию META-INF со следующим содержимым:

org.springframework.boot.env.EnvironmentPostProcessor=ru.fullstackguy.config.EnvPostProcessor

После всех этих действий содержимое папки resources выглядит вот так:

% tree src/main/resources src/main/resources ├── META-INF │   ├── spring │   │   └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │   └── spring.factories └── default.yaml

Собираем стартер

Вот, собственно, и всё.

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

Чтобы проверить, что всё работает, давайте соберём получившийся проект командой:

mvn clean install

После сборки проекта, наш стартер будет добавлен в локальный maven репозиторий и таким образом, он станет доступен для использования в локальной разработке других проектов.

Использование starter’а

Для проверки работоспособности получившегося стартера, создадим новый Spring Boot проект с помощью Spring Initializr.

Добавим зависимость на наш стартер, добавив следующие строки в блок <dependencies>:

<dependency>     <groupId>ru.fullstackguy</groupId>     <artifactId>openweathermap-starter</artifactId>     <version>1.0-SNAPSHOT</version> </dependency> 

После этих действий, мы можем использовать сервисные классы стартера в проекте клиентского приложения:

@Autowired private final WeatherService weatherService; ... var temperature = weatherService.getTemperature(); 

По умолчанию, стартер будет использовать собственные настройки из default.yaml. Однако, если нам необходимо перенастроить стартер, мы вольны переопределить настройки прямо в application.yaml клиентского приложения. Например, вот пример application.yaml файла из моего клиентского приложения:

spring:   application:     name: openweathermap-app  openweathermap-starter:   city: Murmansk

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

Заключение

В данной статье мы разобрали самый простой сценарий создания собственного Spring Boot стартера. Как Вы могли заметить, процесс достаточно не сложный. Самое главное не забывать о том, что каждый starter стоит описывать в собственном пространстве — как пакетном (например, ru.fullstackguy.openweathermapstarter), так и конфигурационном: openweathermap-starter. Это поможет избежать проблем, связанных с пересечением сервисов и конфигураций между несколькими стартерами.

Список материалов

Видео-версия этой статьи

Оригинал этой статьи, а также многие другие на разные темы об IT и разработке, можно найти на моём сайте #fullstackguy.


ссылка на оригинал статьи https://habr.com/ru/articles/806217/


Комментарии

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

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