Настройка приложения с помощью @ConfigurationProperties, как альтернатива использованию @Value.

В статье

  • Настройка и изменение функционала приложения через application.properties с использованием ConfigurationProperties
  • Интеграция application.properties с IDE
  • Проверка значений настроек

image

Про отличия между двумя подходами сказано здесь — ConfigurationProperties vs. Value
На картинке выше основной состав и принцип работы. Доступные компоненты системы, это Spring компоненты, просто классы, различные константы, переменные и пр. можно указать в файле application.properties, при этом еще на момент указания средой разработки будут предложены варианты, сделаны проверки. При старте приложения указанные значения будут проверенны на соответствие типа, ограничениям и если все удовлетворяет, то будет выполнен старт приложения. Например очень удобно настраивать функционал приложения из списка доступных Spring компонент, ниже покажу как.
Класс свойств

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

AppProperties.java
@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, как префикс к свойству.

Класс приложения SpringApplication и pom.xml проекта
@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 бина

Spring контекст (context.xml)
<?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.

image

Вот их реализация

DemoService
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», то среда начнет показывать доступные свойства

image

При попытке ввести неверный тип IDE сообщит

image

Даже если оставить как есть и попытаться стартовать приложение, то будет вполне внятная ошибка

image
Добавление дополнительных метаданных

Дополнительные метаданные, это только для удобства работы с 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 получу список допустимых значений

image

При вводе отличного от предложенного, но того же типа, редактор подсветит, но приложение в этом случае будет стартовать, так как это не строгое ограничение, а всего лишь подсказка.

image

Ранее в проекте я объявил два сервиса 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 из определения).

image

Теперь удобно выбирать нужный сервис, менять функционал приложения. Есть один момент для объектных настроек, Spring надо немного помочь конвертировать строку которая указана в настройке, в объект. Для этого делается небольшой класс наследник от Converter.

ServiceConverter
@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.

image
Validation property
К полям класса AppProperties можно добавить проверки доступные в рамках JSR 303. Про это я писал здесь. Получится проверяемый, удобный файл конфигурации приложения.

Вывод в консоли

image

Структура проекта

image

Полный файл additional-spring-configuration-metadata.json

additional-spring-configuration-metadata
{
  "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)


  1. tbl
    25.11.2018 00:06

    XML-конфигурация контекста, автовайринг в поля — в итоге странный учебник, учащий старым практикам работы со спрингом.


    1. arylkov Автор
      25.11.2018 05:23

      Где сказано, что xml legacy. inject в privite поля это legacy, а вот в setXxx нет.


  1. IoannGolovko
    25.11.2018 05:23

    1. Пример с xml — очень плохо.

    2. Еще не хватает объяснения, откуда появляется файл additional-spring-configuration-metadata.json и как он связан с spring-boot-configuration-processor.

    3. Статьи на хабре читает много новичков; и инжект сервисов в properties, вы шутите?

    И какой смысл использовать всю эту кашу с метаданными вне стартеров?


    1. arylkov Автор
      25.11.2018 05:31

      — Где сказано, что xml legacy?
      — additional-spring-configuration-metadata — делаем сами, если нужны hints, это клон spring-configuration-metadata.
      — ограничений на типы которые можно указать в properties практически нет, сделано это как я понимаю не случайно. И типы Providers для этого сделаны тоже: class-reference, spring-bean-reference и др. Видимо разработчики spring видят в этом смысл.


      1. IoannGolovko
        26.11.2018 01:57

        Ответ ниже прилетел


  1. alek_sys
    25.11.2018 12:40
    +1

    Спасибо за статью! Пара моментов, больше для читателей, из того, что не упомянуто:


    1. Самое важное — @ConfigurationProperties это не только про загрузку свойств из application.properties файла. Возможности Spring Boot Configuration гораздо шире — поддерживается 17 (!) разных источников свойств в строгом приоритете. Можно определить дефолт в application.properties и перекрыть его через переменную окружения, JVM properties, профиль, тестовые свойства и т.п. Что дает очень мощные возможности для переконфигурирования приложения в нужном окружении и сильно упрощает конфигурацию. А в дополнение — список источников еще и можно расширять, например, добавить Hashicorp Vault как бэкенд.
    2. ConfigurationProperties аннотация — это часть Spring Boot, а не Spring Core
    3. @SpringBootApplication аннотация включает ComponentScan, так что XML конфигурацию (да и любую конфигурацию) можно убрать и просто аннотировать классы @Component. Хотя некоторые разработчики предпочитают явную конфигурацию над автоматическим поиском компонентов.
    4. Конфигурировать можно не только классы properties, а вообще любой бин — если совместить @Bean + @ConfigurationProperties или @Component + @ConfigurationProperties. По сути, все, что делает @EnableConfigurationProperties — это регистрирует бин указанного типа.
    5. Field и Property injection это, все же, моветон, хотя и не запрещено.


    1. arylkov Автор
      25.11.2018 14:47

      Спасибо! Интересно


  1. IoannGolovko
    26.11.2018 01:57

    1. Как минимум, здесь отсутствует типобезопасность.

    2. Замечание не на тему того, кто чей клон, а откуда этот файл берется. Его генерирует определенный annotation processor. Кому нужно делать rebuild проекта ради какой-то подсветки? Вся эта каша нужна в библиотеках и стартерах. Или в 2018 кто-то еще зашивает конфиги в поставку?

    3. Это нужно не для инжекта бизнес логики в конфиг! А для создания сложных библиотечных конфигураций, завязанных на стандартных интерфейсах из jsl или кастомной абстракции. В примере из статьи присутствует явное нарушение single responsibility principle. ConfigurationProperties нужно использовать либо в автоконфигурации, либо в сервисе, которому эти конфиги нужны (второе решение — так себе). Код из примера можно понять таким образом (а ведь он именно и написан в таком стиле), что надо все бины прятать в конфиги и через конфиги получать к бинам доступ. А потом кто-то жалуется на невозможность зарезолвить циклические зависимости и тратит по неделе на фикс простейшего бага.