Разделение инфраструктуры и бизнес-логики, на примере своего spring boot starter'а



На сегодняшний день spring stack - практически “стандарт” в мире java разработки, огромное количество IT-компаний разрабатывают свои приложения с использованием этих технологий. Сегодня мы напишем свой starter для spring-boot приложения.

Те, кто создают свои проекты с использованием spring-boot, знают, что с его приходом появилась прикольная фича - ConfigurationProperties, которая позволяет “мапить” ваш environment на Java объекты. Подробно об этом можно почитать в официальной документации.

Используя @ConfigurationProperties, можно получить более понятный и читаемый код. Например, у вас есть конфиг приложения application.yml, который описывает настройку интеграции с сервисом «Почта России»:
app:
    russianpost:
        apiUrl: https://tracking.russianpost.ru/fc
        user: user
        password: password

До spring-boot мы инжектили подобные параметры, используя аннотацию @Value, и получали нечто подобное:
@Component
public class RussianPostServiceClient {
    @Value("${app.russianpost.apiUrl}")
    private String apiUrl;

    @Value("${app.russianpost.user}")
    private String user;

    @Value("${app.russianpost.password}")
    private String password;
}

Представьте, если ваш клиент в конфигурации будет еще иметь такие параметры, как readTimeout, connectionTimeout или любые другие. Размер класса RussianPostServiceClient будет расти, а его читаемость падать. Если нам потребуются эти параметры в другом классе, то опять придётся их получать через @Value:
@Component
public class SomeOtherComponent {
    @Value("${app.russianpost.apiUrl}")
    private String apiUrl;
}

При использовании @ConfigurationProperties это выглядит так: мы создаем класс Java, на полях которого будут мапиться параметры из конфига, а Spring всё сделает за нас:
@Data
@ConfigurationProperties("app.russianpost")
public class RussianPostProperties {
    private String apiUrl;
    private String user;
    private String password;
}

Далее нам необходимо зарегистрировать BeanDefinition данного класса для получения бина из контекста Spring’а. Это делается при помощи аннотации @EnableConfigurationProperties.
@Configuration
@EnableConfigurationProperties(RussianPostProperties.class)
public class PropertyConfiguration {

}

Теперь мы можем инжектить наш объект в любой другой, управляемый Spring’ом, например, RussianPostServiceClient
@Component
public class RussianPostServiceClient {
    @Autowired
    private RussianPostProperties properties;
}

Выглядит намного лучше, чем использование @Value, не правда ли?
Теперь RussianPostProperties находится в контексте, и мы можем инжектить его в любой другой бин. Также мапинг через @ConfigurationProperties позволяет произвести валидацию конфига, например, указать, что поля apiUrl, user, password - обязательны.

На примере такого маленького приложения всё выглядит очень неплохо, но в приложениях посерьёзней подобных конфигов, которые надо мапить на классы, гораздо больше. В итоге ваш конфиг превращается в:
@Configuration
@EnableConfigurationProperties({
    RussianPostProperties.class,
    SomeProperty1.class,
    SomeProperty2.class,
    SomeProperty3.class,
    SomeProperty4.class,
    .......
})

За этим надо постоянно следить и не забывать добавлять следующий Property class. Либо вы можете объявить ваш Property class как компонент, добавив аннотацию @Component над классом RussianPostProperties, тогда @EnableConfigurationProperties не понадобится, она применится автоматически. Сам Spring не рекомендует этого делать. Объяснения тут.

Чтобы не писать ни того, ни другого, я написал Spring Boot starter. Вместе со Spring Boot’ом пришла концепция starter’ов. Это некая конвенция управления зависимостями в spring-boot проектах.

Вашему проекту нужна работа с базой данных? Добавляем в проект зависимость
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

и автоматически получаем преднастроенные конфигурации для работы с БД (spring data, hibernate, а также системы управления версионностью БД, при наличии таковых в зависимостях проекта). Это тоже косвенно позволяет делать ваш проект немного «чище». Раньше приходилось подключать намного большее количество зависимостей.

Сам starter, по сути - обычный проект (необязательно spring-boot проект), который содержит в себе какую-то конфигурацию, набор конфигураций или просто зависимости. В этом проекте есть одна особенность - файл resources/META-INF/spring.factories. В нём мы прописываем свою конфигурацию, которая должна быть автоматически зарегистрирована в нашем контексте.

Spring.factories - это внутренний протокол spring’а. Когда запускается spring-boot приложение, происходит сканирование classpath’a на наличие spring.factories. Этим занимается SpringFactoriesLoader. Он пройдётся по всем jar’ам и соберёт все компоненты, которые там прописаны. Это будет сделано автоматически, но в момент построения контекста приложения вам нужно только добавить соответствующий starter как зависимость. В spring.factories можно указывать различные инфраструктурные компоненты spring’a, которые отработают на соответствующем этапе приложения, например, ApplicationListener, ContextLoadInitializer, EnvironmentPostProcessor и тд.

Итак, напишем наш starter. Вся логика в классе ConfigurationPropertiesAutoConfiguration, который реализовывает специальный интерфейс BeanDefinitionRegistryPostProcessor - инфраструктурный интерфейс, позволяющий добавить пользовательские beanDefinition’ы. Код очень простой: сначала ищем класс, аннотированный @SpringBootApplication, затем производим сканирование классов, которые аннотированы @ConfigurationProperties, и их регистрация:
@ConditionalOnProperty(value = "configuration.properties.register.enabled", havingValue = "true", matchIfMissing = true)
public class ConfigurationPropertiesAutoConfiguration implements BeanDefinitionRegistryPostProcessor {

    public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) throws BeansException {
        Reflections reflections = new Reflections("");
        Class sbaClass = Iterables.getFirst(reflections.getTypesAnnotatedWith(SpringBootApplication.class), null);
        
        if(sbaClass != null) {
            reflections = new Reflections(sbaClass.getPackage().getName());
            for (Class cl : reflections.getTypesAnnotatedWith(ConfigurationProperties.class)) {
                ConfigurationProperties annotation = cl.getAnnotation(ConfigurationProperties.class);
                String beanName = annotation.value() + "-" + cl.getName();
                registry.registerBeanDefinition(beanName, new AnnotatedGenericBeanDefinition(cl));
            }
        }
    }

    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }

}

Также я добавил @ConditionalOnProperty для возможности выключения автоконфигурации. Теперь добавим нашу автоконфигурацию в spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 
    ru.rnemykin.spring.boot.ConfigurationPropertiesAutoConfiguration
Осталось собрать проект и использовать в виде зависимости:
<dependency>
    <groupId>ru.rnemykin.spring</groupId>
    <artifactId>auto-configuration-properties-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

И всё. Spring автоматически подтянет нашу конфигурацию и зарегистрирует наш BeanDefinitionRegistryPostProcessor, который будет обрабатывать классы, помеченные @ConfigurationProperties.
Таким образом, мы вынесли инфраструктурную логику в отдельный модуль от бизнес-логики. Как и в случае с конфигурацией, такого инфраструктурного кода в реальном проекте достаточно много. Он “нагружает” ваш проект, к тому же некоторые решения можно было бы переиспользовать в других проектах. В случае, когда такой код вынесен в отдельный проект, это делается просто указанием зависимости, иначе пришлось бы его дублировать.

Данный проект носит ознакомительный характер, но при желании им можно воспользоваться. Код доступен на GitHub.

Комментариев нет

Технологии Blogger.