Что вообще такое 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.

#fullstackguy в Telegram. Подписаться!