Настройка приложения — Spring Configuration Metadata

    Настройка приложения с помощью @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

      0

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

        0
        Где сказано, что xml legacy. inject в privite поля это legacy, а вот в setXxx нет.
        0
        1. Пример с xml — очень плохо.

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

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

        И какой смысл использовать всю эту кашу с метаданными вне стартеров?
          0
          — Где сказано, что xml legacy?
          — additional-spring-configuration-metadata — делаем сами, если нужны hints, это клон spring-configuration-metadata.
          — ограничений на типы которые можно указать в properties практически нет, сделано это как я понимаю не случайно. И типы Providers для этого сделаны тоже: class-reference, spring-bean-reference и др. Видимо разработчики spring видят в этом смысл.
            0
            Ответ ниже прилетел
          +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 это, все же, моветон, хотя и не запрещено.
            0
            Спасибо! Интересно
            0
            1. Как минимум, здесь отсутствует типобезопасность.

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

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

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое