Featured

8/recent

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

27.07.2018


На сегодняшний день 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.