Создаем свой Spring Boot Starter

Стартеры Spring Boot - это, по сути, предварительно упакованные наборы зависимостей и сконфигурированных бинов для обеспечения определенной функциональности, например, доступа к базе данных или безопасности.

· 6 минуты на чтение
Создаем свой Spring Boot Starter

Один из мощных механизмов Spring Boot — возможность использования "стартеров" для быстрой настройки нового проекта с предварительно сконфигурированными зависимостями.

Стартеры Spring Boot — это, предварительно упакованные наборы зависимостей и сконфигурированных бинов для обеспечения определённой функциональности. Например, доступа к базе данных или безопасности.

Возьмём мою библиотеку GodFather Telegram, которая позволяет создавать ботов для Telegram. Без стартера вам пришлось бы создать бинов 15: бин для принятия входящих событий от телеграма, бин для отправки сообщений, бин для построения сценария бота, множество инфраструктурных бинов. Без подробной документации не обойтись.

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

У клиентов стартера остаётся возможность переопределить внутренние бины, если сценарии использования выходят за рамки дефолтных настроек. Также разработчик стартера может вынести множество настроек в файл application.properties.

В этой статье мы рассмотрим, как создать собственный стартер Spring Boot. Обсудим некоторые лучшие практики и советы по созданию.

Спонсор поста

Создаем свой стартер

Написание стартера будем рассматривать на примере моей библиотеки для создания ботов GodFather Telegram.

Для начала определимся с groupId и artifactId. Все официальные стартеры придерживаются следующей схемы именования spring-boot-starter-*, где * это конкретный тип приложения.

Сторонние стартеры не должны начинаться с spring-boot, поскольку они зарезервированы для официальных стартеров от разработчиков Spring. Сторонний стартер обычно начинается с названия проекта. Например, мой стартер называется telegram-bot-spring-boot-starter.

Можно реализовать стартер как дополнительный maven-модуль основного проекта, или как отдельный проект. Я предпочитаю делать отдельный проект.

Поэтому создаём пустой SpringBoot проект. Убедитесь, что в pom.xml присутствуют следующие зависимости:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Добавьте зависимости вашей библиотеки. В моём случае их три:

<dependency>
    <groupId>dev.struchkov.godfather.telegram</groupId>
    <artifactId>telegram-consumer-simple</artifactId>
    <version>${telegram.bot.version}</version>
</dependency>
<dependency>
    <groupId>dev.struchkov.godfather.telegram</groupId>
    <artifactId>telegram-core-simple</artifactId>
    <version>${telegram.bot.version}</version>
</dependency>
<dependency>
    <groupId>dev.struchkov.godfather.telegram</groupId>
    <artifactId>telegram-sender-simple</artifactId>
    <version>${telegram.bot.version}</version>
</dependency>

Теперь создадим обычный класс конфигурации Spring с нужными бинами.

@Configuration
public class TelegramBotAutoconfiguration {

    @Bean(AUTORESPONDER_EXECUTORS_SERVICE)
    public ExecutorService executorService(
            TelegramBotAutoresponderProperty autoresponderProperty
    ) {
        return Executors.newFixedThreadPool(autoresponderProperty.getThreads());
    }
    
    ... ... ... ... ...

    @Bean
    public TelegramService telegramService(TelegramConnect telegramConnect) {
        return new TelegramServiceImpl(telegramConnect);
    }

}

Смысла описывать всю конфигурацию нет, это просто бины необходимые для работы библиотеки.

Пока это ничем не примечательный модуль с обычным классом конфигурации, что делает его стартером? Стартером его сделает особый файл, создадим его.

В папке resources создаём папку META-INF, в ней папку spring, и в ней файл org.springframework.boot.autoconfigure.AutoConfiguration.imports.

Этот файл должен содержать перечисление конфигураций. В моём случае это одна конфигурация. Каждую конфигурацию указывайте с новой строки.

dev.struchkov.godfather.telegram.starter.config.TelegramBotAutoconfiguration
Spring Boot 3

Если вы используете Spring Boot 2.7 и ниже, то необходимо создать папку META-INF и в ней файл spring.factories.

Подробнее об этом читайте в "Spring Boot 3.0 Migration Guide".

SpringBoot автоматически найдёт этот файл, возьмёт перечисленные конфигурации и создаст указанные в них бины, добавив их в контекст. Так класс конфигурации превращается в класс автоконфигурации. Вот и вся магия 🪄

Проперти классы

Если вашей библиотеке требуются какие-то конфигурационные переменные, стоит позволить передавать их через application.yml. Например, в моей библиотеке это информация данные для подключения к Telegram: токен и имя бота.

Для этого создайте класс и аннотируйте его @ConfigurationProperties или добавьте существующий через конфигурацию:

@Configuration
public class TelegramBotPropertyConfiguration {

    @Bean
    @ConfigurationProperties("telegram.bot")
    public TelegramBotConfig telegramConfig() {
        return new TelegramBotConfig();
    }
    
}

А в pom.xml добавим зависимость, которая генерирует метаданные о классах приложения, аннотированных @ConfigurationProperties.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Этот процессор аннотаций создаст файл META-INF/spring-configuration-metadata.json, который содержит метаданные о параметрах конфигурации в классе TelegramBotConfig. Эти метаданные включают Javadoc о полях, поэтому убедитесь, что Javadoc понятен.

В Idea плагин Spring Assistant будет читать метаданные и обеспечивать подсказки для этих свойств.

Так это будет выглядеть в Idea

Также мы можем добавить некоторые свойства вручную, создав файл META-INF/additional-spring-configuration-metadata.json:

{
  "properties": [
    {
      "name": "telegram.bot.enable",
      "type": "java.lang.Boolean",
      "description": "Enables or disables Telegram Bot."
    }
  ]
}

Процессор аннотаций автоматически объединит содержимое этого файла с автоматически созданным файлом.

Conditionals

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

Например, в моем случае нет смысла поднимать бины, если пользователь не указал переменные подключения к боту в application.yml, потому что всё завязано на них.

Для решения этой проблемы существуют аннотации @Conditional...

@ConditionalOnProperty

Воспользуемся аннотацией @ConditionalOnProperty, которая проверяет наличие заполнения конкретной проперти в application.yml.

@Bean
@ConfigurationProperties("telegram.bot")
@ConditionalOnProperty(prefix = "telegram.bot", name = "token")
public TelegramBotConfig telegramConfig() {
    return new TelegramBotConfig();
}

Бин TelegramBotConfig будет создаваться, только если указана проперти telegram.bot.token.

Используя параметр аннотации value, можно указать конкретные значения проперти. В моем случае важно само наличие токена.

@ConditionalOnBean

Также воспользуемся @ConditionalOnBean, которая создаёт бин, когда в BeanFactory  присутствует указанный в @ConditionalOnBean бин.

@Bean
@ConditionalOnBean(TelegramBotConfig.class)
public TelegramDefaultConnect telegramDefaultConnect(TelegramBotConfig telegramConfig) {
    return new TelegramDefaultConnect(telegramConfig);
}

@ConditionalOnMissingBean позволяет создавать бин, если указанный бин отсутствует в BeanFactory.

@ConditionalOnClass

Создаёт бин, только если указанный класс есть в classpath. И противоположный ему @ConditionalOnMissingClass.

Рандомный блок

Несколько классов автоконфигурации

При добавлении нескольких классов автоконфигурации и использовании аннотаций @ConditionalOnBean вы столкнётесь с неожиданным поведением — условие не будет отрабатывать и бин не будет создаваться.

Это происходит, потому что Spring берёт конфигурации в произвольном порядке, и может сначала попробовать поднять бин помеченный @ConditionalOnBean, хотя бин в условии будет создан позже. Порядок классов автоконфигураций в файле никак не влияет на порядок создания бинов.

Чтобы этого избежать, используйте аннотации @AutoConfigureAfter или @AutoConfigureBefore над классом конфигурации, которые явно задают порядок создания бинов.

Опциональные бины

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

@Bean
@ConfigurationProperties("telegram.bot")
@ConditionalOnProperty(prefix = "telegram.bot", name = "token")
public TelegramBotConfig telegramConfig(
        ObjectProvider<ProxyConfig> proxyConfigProvider
) {
    final TelegramBotConfig telegramBotConfig = new TelegramBotConfig();

    final ProxyConfig proxyConfig = proxyConfigProvider.getIfAvailable();
    if (proxyConfig != null) {
        telegramBotConfig.setProxyConfig(proxyConfig);
    }

    return telegramBotConfig;
}

Бин ProxyConfig является опциональным. Если его не будет, мы всё равно хотим создать TelegramBotConfig. Если не использовать ObjectProvider, то приложение упадёт при старте, если бина ProxyConfig не будет.

Бины по умолчанию

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

@Bean
@ConditionalOnMissingBean(PersonSettingRepository.class)
public PersonSettingRepository personSettingRepository() {
    return new PersonSettingLocalRepository();
}

Обратите внимание, что в @ConditionalOnMissingBean мы указываем тот же класс, что и возвращаем. Таким образом, если пользователь стартера определит бин PersonSettingRepository, то дефолтный бин из автоконфигурации не будет создан.

Улучшаем время запуска

Классы конфигурации тоже можно аннотировать @Conditional.... И для каждого класса автоконфигурации Spring Boot оценивает условия, указанные в аннотации @Conditional..., чтобы решить, загружать ли автоконфигурацию и все необходимые в ней бины. В зависимости от размера и количества стартеров в приложении Spring Boot, это может быть очень дорогой операцией и повлиять на время запуска.

Существует ещё один процессор аннотаций, который генерирует метаданные об условиях всех автоконфигураций. Spring Boot считывает эти метаданные во время запуска и отфильтровывает конфигурации, условия которых не выполняются, без необходимости проверять эти классы.

Чтобы метаданные генерировались, нужно добавить процессор аннотаций в стартовый модуль:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>

Во время сборки метаданные будут сгенерированы в файл META-INF/spring-autoconfigure-metadata.properties, который будет выглядеть так:

dev.struchkov.godfather.telegram.starter.config.TelegramBotAutoconfiguration=
dev.struchkov.godfather.telegram.starter.config.TelegramBotAutoconfiguration.AutoConfigureAfter=dev.struchkov.godfather.telegram.starter.config.TelegramBotDataConfiguration
dev.struchkov.godfather.telegram.starter.config.TelegramBotAutoconfiguration.ConditionalOnBean=dev.struchkov.godfather.telegram.simple.core.TelegramConnectBot
dev.struchkov.godfather.telegram.starter.config.TelegramBotDataConfiguration=
dev.struchkov.godfather.telegram.starter.config.TelegramBotDataConfiguration.AutoConfigureAfter=dev.struchkov.godfather.telegram.starter.config.TelegramBotPropertyConfiguration

Использование стартера

Теперь, когда стартер готов, мы можем его добавить в наше другое приложение:

<dependency>
    <groupId>dev.struchkov.godfather.telegram</groupId>
    <artifactId>telegram-bot-spring-boot-starter</artifactId>
    <version>${godfather.telegram.version}</version>
</dependency>

Теперь нам доступны все сконфигурированные бины.

Заключение

Выделить определённые функции в модуль стартер, чтобы использовать их в любом приложении Spring Boot, — дело нескольких простых шагов.

Создайте автоконфигурацию, сделайте её настраиваемой и добавьте процессоры аннотаций, чтобы автоматически генерировать метаданные для повышения производительности и удобства использования.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇