Что вообще такое starter?
Допустим, Вы разрабатываете несколько приложений или микросервисов на Java. Каждое из них уникальное, и содержит свою собственную бизнес логику. Однако, в каждом из них может быть необходимость использовать общую логику. Например, логику аутентификации, как это часто бывает в мире микросервисов.
Есть несколько способов реализовать общую логику в нескольких приложениях:
можно реализовать её в каждом из приложений
можно вынести её в отдельный модуль, который при сборке будет автоматически включён в качестве части приложения
можно вынести её в отдельную библиотеку, и подключать её как отдельную зависимость
Первый подход плох тем, что на каждое изменение в общей логике, нужно эти изменения скопировать в каждое приложение, в котором эта самая общая логика используется. Это рабочий, но плохоуправляемый и трудноподдерживаемый вариант для коммерческой разработки.
Второй подход часто применяется в случае, когда разработка систем ведётся в монорепозиториях. С одной стороны, это означает, что изменив общую логику, разработчику необходимо изменить и все места в системе (все микросервисы и приложения, использующие общую логику). С другой стороны, без знания контекста использования общей логики в разных местах - вносить изменения рискованно.
Третий подход - это подход, при котором общая логика выпускается в виде отдельной подключаемой библиотеки, со своим собственным версионированием и релизным циклом. Это самый гибкий подход, поскольку позволяет разработчикам каждого приложения или микросервиса решать вопрос об обновлении зависимости с общей логикой самостоятельно.
Так что же такое - starter’ы в Spring Boot? Это и есть отдельные библиотеки, со своим релизным циклом, которые позволяют использовать фишки самого Spring’а. Конкретно, в них могут содержаться заранее сконфигурированные Spring бины, которые решают какую-либо задачу (например, интеграция с чем-либо).
Иными словами - каждый стартер представляет собой “строительный блок” с кодом и собственной конфигурацией, которая, в последствие, может быть изменена уже в приложении.
Примеры starter’ов
Возможно, Вы и не задумывались, но каждый раз, когда Вы пользуетесь Spring Initializr’ом, добавляя зависимости в проект, Вы добавляете starter’ы. Например:
spring-boot-starter-web
- для работы с Spring Web и MVCspring-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
. Это поможет избежать проблем, связанных с пересечением сервисов и конфигураций между несколькими стартерами.
Список материалов
Сайт Spring Initlzr - на котором можно сгенерировать Spring проекты, а так же, добавить туда имеющиеся starter’ы Spring’а в удобном пользовательском интерфейсе
Хороший список имеющихся Spring starter’ов - список не полный, но подробный
Оригинал этой статьи, а также многие другие на разные темы об IT и разработке, можно найти на моём сайте #fullstackguy.
Комментарии (7)
Kinski
08.04.2024 17:14+2Чтобы Spring знал о дефолтных настройках стартера, необходимо их зачитать.
Можно объявить значения по умолчанию в файле OpenWeatherMapProperties, чтобы не городить огород с PostProcessor.
anverbogatov Автор
08.04.2024 17:14+1Всё верно - можно. Но тогда, для разработчика, работающего со стартером, чтобы просто посмотреть дефолтные настройки стартера потребуется разобраться в его коде, найти правильное место и, зная правила маппинга названий пропертей на поля конфигурационного класса, прописать их в своём клиентском приложении.
Гораздо проще, когда у стартера есть готовый yaml файл с конфигом, который можно скопипастить и проставить свои значения.
Кстати, действительно - совсем не обязательно городить PostProcessor. Бывают случаи когда yaml файл вообще добавляется для примера настроек и значений, с которыми работает стартер. То есть конфигурация из него не учитывается, а лежит чисто для облегчения копи-паста. Тоже рабочий вариант.
Kinski
08.04.2024 17:14Да, Вы все верно пишите. Но что файл с дефолтным конфигом, что определение значений по умолчанию, все равно находится в коде стартера. И разработчику все равно придётся копаться в исходниках.
Самый лучший вариант - внести описание настроек и значений по умолчанию в документацию)
atues
Вот тут https://habr.com/ru/companies/piter/articles/506872/ был обзор отличной книги в которой есть целая глава, посвященная созданию собственных стартеров. Правда, для 2-й версии бута
anverbogatov Автор
Спасибо за информацию! Когда разбирался со спрингом, я использовал книги от Manning (Spring in Action, Spring Security in Action, Spring Microservices in Action) и ни в одной не было информации по созданию своего стартера.