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

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

  • Сайт Spring Initlzr - на котором можно сгенерировать Spring проекты, а так же, добавить туда имеющиеся starter’ы Spring’а в удобном пользовательском интерфейсе

  • Хороший список имеющихся Spring starter’ов - список не полный, но подробный

  • Репозиторий с проектами, разработанными в данной статье

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

Источник: https://habr.com/ru/articles/806217/


Интересные статьи

Интересные статьи

Коллектив учёных из «Сколтеха», Института биохимической физики имени Н. М. Эмануэля РАН и других научных организаций России и Израиля изучил, как добавление примесей в термоэлектрический мат...
Одной из основных причин разработки микросервисов является то, что они обеспечивают четкие границы модулей. Однако минусы микросервисов настолько огромны, что это все равно, что отрубить себ...
Четкое разделение бизнес логики с другими сквозными задачами является обязательным условием для создания чистого и читабельного кода. И говоря о сквозных задачах я имею ввиду управление транзакциями, ...
Привет, Хабр! представляю вашему вниманию перевод статьи How to create a 2D game with Python and the Arcade library автора Paul Vincent Craven Как создать 2D игру с Python и аркадной библиотекой...
На встрече московского сообщества Java-разработчиков jug.msk.ru, состоявшейся 28 июня 2019 года традиционно в офисе компании КРОК, Кирилл Толкачёв и Максим Гореликов представили свой доклад о Spr...