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

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

Что вообще такое 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. Это поможет избежать проблем, связанных с пересечением сервисов и конфигураций между несколькими стартерами.

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

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

Источник

211 views·16 shares