coding_rnemykin
java
spring
spring-boot
spring-boot-starter
Разделение инфраструктуры и бизнес-логики, на примере своего 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 для возможности выключения автоконфигурации. Теперь добавим нашу автоконфигурацию в spring.factories:
И всё. Spring автоматически подтянет нашу конфигурацию и зарегистрирует наш BeanDefinitionRegistryPostProcessor, который будет обрабатывать классы, помеченные @ConfigurationProperties.
Таким образом, мы вынесли инфраструктурную логику в отдельный модуль от бизнес-логики. Как и в случае с конфигурацией, такого инфраструктурного кода в реальном проекте достаточно много. Он “нагружает” ваш проект, к тому же некоторые решения можно было бы переиспользовать в других проектах. В случае, когда такой код вынесен в отдельный проект, это делается просто указанием зависимости, иначе пришлось бы его дублировать.
Данный проект носит ознакомительный характер, но при желании им можно воспользоваться. Код доступен на GitHub.
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.
Добавить комментарий