Налаштування програми — Spring Configuration Metadata
Налаштування програми за допомогою @ConfigurationProperties
, як альтернатива використанню @Value
.
В статті
- Налаштування і зміни функціоналу програми через application.properties з використанням ConfigurationProperties
- Інтеграція application.properties з IDE
- Перевірка установок
Про відмінності між двома підходами сказано тут — 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.звітування.outputEncoding>UTF-8</project.звітування.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.
Ось їх реалізація
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 проекту, після чого в проекті з’явиться файл targetclassesMETA-INFspring-configuration-metadata.json
. Власне його IDE використовує для редагування у файлі application.properties. Його структуру я вкажу на засланні в матеріалах. Цей файл буде створений на основі класу AppProperties. Якщо тепер відкрити файл application.properties і почати вводити «demo», то середовище почне показувати доступні властивості
При спробі ввести неправильний тип IDE повідомить
Навіть якщо залишити як є і спробувати стартувати додаток, то буде цілком виразна помилка
Додавання додаткових метаданих
Додаткові метадані, це тільки для зручності роботи з додатком.properties в IDE, якщо це не треба, можна не робити. Для цього є можливість вказати в додатковому файлі підказки (hints) та ін. інформацію для середовища. Для цього скопирую створений файл spring-configuration-metadata.json в srcmainresourcesMETA-INF
і переименую його в additional-spring-configuration-metadata.json
. У цьому файлі мене буде цікавити тільки секція «hints»: []
У ній можна буде перерахувати наприклад допустимі значення для demo.vehicle
"hints": [
{
"name": "demo".vehicle",
"values": [
{
"value": "make A car",
"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.
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.
Validation property
До полів класу AppProperties можна додати перевірки доступні в рамках JSR 303. Про це я писав тут. Вийде, що перевіряється, зручний файл конфігурації програми.
Висновок в консолі
Структура проекту
Повний файл 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": "make A car",
"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"
}
}
]
}
]
}