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.
Добавить комментарий