@ConfigurationProperties
, как альтернатива использованию @Value
. В статье
- Настройка и изменение функционала приложения через application.properties с использованием ConfigurationProperties
- Интеграция application.properties с IDE
- Проверка значений настроек
Про отличия между двумя подходами сказано здесь — ConfigurationProperties vs. Value
На картинке выше основной состав и принцип работы. Доступные компоненты системы, это Spring компоненты, просто классы, различные константы, переменные и пр. можно указать в файле application.properties, при этом еще на момент указания средой разработки будут предложены варианты, сделаны проверки. При старте приложения указанные значения будут проверенны на соответствие типа, ограничениям и если все удовлетворяет, то будет выполнен старт приложения. Например очень удобно настраивать функционал приложения из списка доступных Spring компонент, ниже покажу как.
Класс свойств
Для создания настройки приложения с использованием ConfigurationProperties, можно начать с класса свойств. В нем собственно указаны свойства, компоненты системы которые хотим настраивать.
@ConfigurationProperties(prefix = "demo")
@Validated
public class AppProperties {
private String vehicle;
@Max(value = 999, message = "Value 'Property' should not be greater than 999")
private Integer value;
private Map<String,Integer> contexts;
private StrategyEnum strategyEnum;
private Resource resource;
private DemoService service;
public String getVehicle() {
return vehicle;
}
public void setVehicle(String vehicle) {
this.vehicle = vehicle;
}
public Map<String, Integer> getContexts() {
return contexts;
}
public void setContexts(Map<String, Integer> contexts) {
this.contexts = contexts;
}
public StrategyEnum getStrategyEnum() {
return strategyEnum;
}
public void setStrategyEnum(StrategyEnum strategyEnum) {
this.strategyEnum = strategyEnum;
}
public Resource getResource() {
return resource;
}
public void setResource(Resource resource) {
this.resource = resource;
}
public DemoService getService() {
return service;
}
public void setService(DemoService service) {
this.service = service;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
@Override
public String toString() {
return "MyAppProperties{" +
"\nvehicle=" + vehicle +
"\n,contexts=" + contexts +
"\n,service=" + service +
"\n,value=" + value +
"\n,strategyEnum=" + strategyEnum +
'}';
}
}
В классе prefix=«demo» будет использоваться в application.properties, как префикс к свойству.
@SpringBootApplication
@EnableConfigurationProperties({AppProperties.class})
@ImportResource(value= "classpath:context.xml")
public class DemoConfigProcessorApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(DemoConfigProcessorApplication.class, args);
AppProperties properties = context.getBean(AppProperties.class);
String perform = properties.getService().perform(properties.getVehicle());
System.out.println("perform: " + perform);
System.out.println(properties.toString());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demoConfigProcessor</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demoConfigProcessor</name>
<description>Demo project for Spring Boot Configuration Processor</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Тут я объявил два spring бина
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="service1" class="com.example.demoConfigProcessor.MyDemoService1">
<description>Description MyDemoService 1</description>
</bean>
<bean id="service2" class="com.example.demoConfigProcessor.MyDemoService2">
<description>Description MyDemoService 2</description>
</bean>
</beans>
В классе AppProperties я указал ссылку на некоторый доступный сервис приложения, его я буду менять в application.properties, у меня будет две его реализации и я буду подключать одну из них в application.properties.
Вот их реализация
public interface DemoService {
String perform(String value);
}
public class MyDemoService1 implements DemoService {
@Override
public String perform(String value) {
return "Service №1: perform routine maintenance work on <" + value +">";
}
}
public class MyDemoService2 implements DemoService {
@Override
public String perform(String value) {
return "Service №2: perform routine maintenance work on <" + value +">";
}
}
Вот этого уже теперь достаточно что бы начать настраивать application.properties. Но всякий раз когда вносятся изменения в класс с ConfigurationProperties, надо сделать rebuild проекта, после чего в проекте появится файл
\target\classes\META-INF\spring-configuration-metadata.json
. Собственно его IDE использует для редактирования в файле application.properties. Его структуру я укажу в ссылке в материалах. Этот файл будет создан на основе класса AppProperties. Если теперь открыть файл application.properties и начать вводить «demo», то среда начнет показывать доступные свойстваПри попытке ввести неверный тип IDE сообщит
Даже если оставить как есть и попытаться стартовать приложение, то будет вполне внятная ошибка
Добавление дополнительных метаданных
Дополнительные метаданные, это только для удобства работы с application.properties в IDE, если это не надо, можно не делать. Для этого есть возможность указать в дополнительном файле подсказки (hints) и др. информацию для среды. Для этого скопирую созданный файл spring-configuration-metadata.json в
\src\main\resources\META-INF\
и переименую его в additional-spring-configuration-metadata.json
. В этом файле меня будет интересовать только секция «hints»: [] В ней можно будет перечислить например допустимые значения для demo.vehicle
"hints": [
{
"name": "demo.vehicle",
"values": [
{
"value": "car make A",
"description": "Car brand A is allowed."
},
{
"value": "car make B",
"description": "Car brand B is allowed."
}
]
}]
В поле «name» указано св-во «demo.vehicle», а в «values» список допустимых значений. Теперь если сделать rebuild проекта и перейти в файл application.properties, то при вводе demo.vehicle получу список допустимых значений
При вводе отличного от предложенного, но того же типа, редактор подсветит, но приложение в этом случае будет стартовать, так как это не строгое ограничение, а всего лишь подсказка.
Ранее в проекте я объявил два сервиса MyDemoService1 и MyDemoService2 оба они имплементируют интерфейс DemoService, теперь можно настроить чтобы application.properties были доступны только сервисы имплементирующие этот интерфейс и соответственно в AppProperties классе инициализировался выбранный. Для этого есть Providers их можно указать в additional-spring-configuration-metadata. Провайдеры есть нескольких типов их можно посмотреть в документации, я покажу пример для одного, —
spring-bean-reference
. Этот тип показывает имена доступных bean-компонентов в текущем проекте. Список ограничивается базовым классом или интерфейсом. Пример Providers для DemoService:
"hints": [
{
"name": "demo.service",
"providers": [
{
"name": "spring-bean-reference",
"parameters": {
"target": "com.example.demoConfigProcessor.DemoService"
}
}
]
}
]
После чего в application.properties для параметра demo.service будет доступен выбор двух сервисов, можно посмотреть их описание (description из определения).
Теперь удобно выбирать нужный сервис, менять функционал приложения. Есть один момент для объектных настроек, Spring надо немного помочь конвертировать строку которая указана в настройке, в объект. Для этого делается небольшой класс наследник от Converter.
@Component
@ConfigurationPropertiesBinding
public class ServiceConverter implements Converter<String, DemoService> {
@Autowired
private ApplicationContext applicationContext;
@Override
public DemoService convert(String source) {
return (DemoService) applicationContext.getBean(source);
}
}
На диаграмме классов проекта видно как эти сервисы отделены от основного приложения и доступны через AppProperties.
Validation propertyК полям класса AppProperties можно добавить проверки доступные в рамках JSR 303. Про это я писал здесь. Получится проверяемый, удобный файл конфигурации приложения.
Вывод в консоли
Структура проекта
Полный файл additional-spring-configuration-metadata.json
{
"groups": [
{
"name": "demo",
"type": "com.example.demoConfigProcessor.AppProperties",
"sourceType": "com.example.demoConfigProcessor.AppProperties"
}
],
"properties": [
{
"name": "demo.contexts",
"type": "java.util.Map<java.lang.String,java.lang.Integer>",
"sourceType": "com.example.demoConfigProcessor.AppProperties"
},
{
"name": "demo.vehicle",
"type": "java.lang.String",
"sourceType": "com.example.demoConfigProcessor.AppProperties"
}
],
"hints": [
{
"name": "demo.vehicle",
"values": [
{
"value": "car make A",
"description": "Car brand A is allowed."
},
{
"value": "car make B",
"description": "Car brand B is allowed."
}
]
},
{
"name": "demo.service",
"providers": [
{
"name": "spring-bean-reference",
"parameters": {
"target": "com.example.demoConfigProcessor.DemoService"
}
}
]
}
]
}
Материалы Configuration Metadata
Комментарии (8)
IoannGolovko
25.11.2018 05:231. Пример с xml — очень плохо.
2. Еще не хватает объяснения, откуда появляется файлadditional-spring-configuration-metadata.json
и как он связан сspring-boot-configuration-processor
.
3. Статьи на хабре читает много новичков; и инжект сервисов в properties, вы шутите?
И какой смысл использовать всю эту кашу с метаданными вне стартеров?arylkov Автор
25.11.2018 05:31— Где сказано, что xml legacy?
— additional-spring-configuration-metadata — делаем сами, если нужны hints, это клон spring-configuration-metadata.
— ограничений на типы которые можно указать в properties практически нет, сделано это как я понимаю не случайно. И типы Providers для этого сделаны тоже: class-reference, spring-bean-reference и др. Видимо разработчики spring видят в этом смысл.
alek_sys
25.11.2018 12:40+1Спасибо за статью! Пара моментов, больше для читателей, из того, что не упомянуто:
- Самое важное —
@ConfigurationProperties
это не только про загрузку свойств из application.properties файла. Возможности Spring Boot Configuration гораздо шире — поддерживается 17 (!) разных источников свойств в строгом приоритете. Можно определить дефолт в application.properties и перекрыть его через переменную окружения, JVM properties, профиль, тестовые свойства и т.п. Что дает очень мощные возможности для переконфигурирования приложения в нужном окружении и сильно упрощает конфигурацию. А в дополнение — список источников еще и можно расширять, например, добавить Hashicorp Vault как бэкенд. ConfigurationProperties
аннотация — это часть Spring Boot, а не Spring Core@SpringBootApplication
аннотация включаетComponentScan
, так что XML конфигурацию (да и любую конфигурацию) можно убрать и просто аннотировать классы@Component
. Хотя некоторые разработчики предпочитают явную конфигурацию над автоматическим поиском компонентов.- Конфигурировать можно не только классы properties, а вообще любой бин — если совместить
@Bean
+@ConfigurationProperties
или@Component
+@ConfigurationProperties
. По сути, все, что делает@EnableConfigurationProperties
— это регистрирует бин указанного типа. - Field и Property injection это, все же, моветон, хотя и не запрещено.
- Самое важное —
IoannGolovko
26.11.2018 01:571. Как минимум, здесь отсутствует типобезопасность.
2. Замечание не на тему того, кто чей клон, а откуда этот файл берется. Его генерирует определенный annotation processor. Кому нужно делать rebuild проекта ради какой-то подсветки? Вся эта каша нужна в библиотеках и стартерах. Или в 2018 кто-то еще зашивает конфиги в поставку?
3. Это нужно не для инжекта бизнес логики в конфиг! А для создания сложных библиотечных конфигураций, завязанных на стандартных интерфейсах из jsl или кастомной абстракции. В примере из статьи присутствует явное нарушение single responsibility principle.ConfigurationProperties
нужно использовать либо в автоконфигурации, либо в сервисе, которому эти конфиги нужны (второе решение — так себе). Код из примера можно понять таким образом (а ведь он именно и написан в таком стиле), что надо все бины прятать в конфиги и через конфиги получать к бинам доступ. А потом кто-то жалуется на невозможность зарезолвить циклические зависимости и тратит по неделе на фикс простейшего бага.
tbl
XML-конфигурация контекста, автовайринг в поля — в итоге странный учебник, учащий старым практикам работы со спрингом.
arylkov Автор
Где сказано, что xml legacy. inject в privite поля это legacy, а вот в setXxx нет.