# Pourquoi (et comment) utiliser @ConfigurationProperties
# Introduction
Le plus simple pour `mapper` des propriétés issues d'un fichier **application.properties** (ou **application.yml**) reste d'utiliser l'annotation `@Value(${ma.super.propriete})` :
```java
@Value("${ma.super.propriete}")
private String maSuperPropriete;
```
C'est plutôt direct, et relativement bien intégré à Intellij (il nous interpole sa valeur et propose même de l'autocomplétion dans le `@Value` si c'est un fichier **.properties**). Mais on pourrait aller plus loin. En premier lieu, aucune autocomplétion n'est disponible dans le fichier de propriétés. En second lieu, on ne peut pas réellement faire de validation de données sur le contenu renseigné dans ce fichier. On va donc voir un moyen d'aller un petit peu plus loin et de pallier à ces problèmes.
# Autcomplétion et propriétés
## Le principe de base
Pour proposer de l'autocomplétion dans les fichiers de `properties`, IntelliJ se base sur un fichier **additional-spring-configuration-metadata.json** que l'on peut placer dans les ressources du projet.
Si on a les propriétés suivantes dans notre **application.properties** :
```properties
maxds.ma-super-propriete=test
maxds.mon-autre-super-propriete=2
```
Il ressemble à ça :
```json
{
"properties": [
{
"name": "maxds.mon-autre-super-propriete",
"type": "java.lang.String",
"description": "Description for maxds.mon-autre-super-propriete."
},
{
"name": "maxds.ma-super-propriete",
"type": "java.lang.Integer",
"description": "Description for maxds.ma-super-propriete."
}
] }
```
> Pour information, IntelliJ vous propose tout seul de générer ce fichier. Dans le fichier de propriétés, faites `alt + entrée` sur les propriétés qu'il ne connait pas.
On y retrouve donc nos deux propriétés et leur type associés. Une description peut-être fournie, pour mieux s'y retrouver dans ce fichier. Il est d'ailleurs dommage que cette dernière ne se retrouve pas dans le fichier de `properties` via le raccourci pour afficher la doc (`ctrl + q` sous IntelliJ).
Voici donc le principe de base. Cependant, l'injection se fait toujours via `@Value("${ma.super.propriete}")`, ce qui n'est pas pratique en cas de `refactoring`. Et le fichier de metadata est fastidieux à maintenir à la main. On va donc voir comment automatiser tout ça.
## Les @ConfigurationProperties
Il existe une annotation `@ConfigurationProperties` dans Spring Boot qui permet de gérer les propriétés sous forme d'une classe Java. En gros nos propriétés de tout à l'heure se retrouveraient sous cette forme :
```java
@ConfigurationProperties("maxds")
public class MaSuperConfig {
private String maSuperPropriete;
private Integer monAutreSuperPropriete;
// Getters
// Setters
}
```
> Si d'aventure vous utilisez [Lombok](https://projectlombok.org/), ça rend ça encore plus lisible.
Notez la valeur dans l'annotation, qui définit le préfixe de l'annotation. Nos propriétés sont ainsi directement liées à cette classe Java et on peut facilement en lire les valeurs, sans utiliser `@Value("${ma.super.propriete}")`.
Pour pouvoir utiliser cette classe, on peut l'annoter avec `@Component` et ainsi l'injecter directement là où ça nous intéresse. On peut également éviter l'utilisation de `@Component` en utilisant une `@EnableConfigurationProperties(MaSuperConfig.class)` sur une classe configuration (on peut par exemple annoter notre `@SpringBootApplication` avec), mais cette annotation doit prendre en paramètre l'ensemble des classes de propriétés. Et enfin, si on souhaite moins s'embêter, et déléguer à Spring le scan de toutes nos classes, il existe l'annotation `@ConfigurationPropertiesScan` (disponible depuis Spring Boot 2.2). Elle est aussi à utiliser sur une classe de configuration.
Avec ça, nous avons simplifié l'accès à nos propriétés. Il nous faut cependant toujours gérer le **additional-spring-configuration-metadata.json** à la main.
## `spring-boot-configuration-processor`, ou comment Spring Boot nous sauve la mise
Il existe heureusement une dépendance Spring Boot qui va nous macher le travail :
```xml
org.springframework.boot
spring-boot-configuration-processor
true
```
Au prochain build du projet, un fichier **spring-configuration-metadata.json** sera automatiquement généré dans `target/classes/META-INF/` :
```json
{
"groups": [
{
"name": "maxds",
"type": "fr.maxds.configuration.config.MaSuperConfig",
"sourceType": "fr.maxds.configuration.config.MaSuperConfig"
}
],
"properties": [
{
"name": "maxds.ma-super-propriete",
"type": "java.lang.String",
"sourceType": "fr.maxds.configuration.config.MaSuperConfig"
},
{
"name": "maxds.mon-autre-super-propriete",
"type": "java.lang.Integer",
"sourceType": "fr.maxds.configuration.config.MaSuperConfig"
}
],
"hints": []
}
```
Il ressemble dans les grandes lignes à ce que l'on avait avant. On a cependant en plus un attribut `sourceType` qui référence notre classe Java de configuration.
Rajoutons dorénavant une petite documentation sur chacune des propriétés :
```java
@ConfigurationProperties("maxds")
public class MaSuperConfig {
/**
* Une propriété de Type "Super"
*/
private String maSuperPropriete;
/**
* Une propriété de Type "Super"
*/
private Integer monAutreSuperPropriete;
// Getters
// Setters
}
```
Faites un petit tour dans votre fichier de propriété, utilisez `ctrl + q` sous IntelliJ et observez le résultat ! Sympa, non ?
Passons dorénavant à l'étape suivante, rajoutons un peu de contrôle sur ces propriétés.
# Validation de données
Comme vous avez sûrement dû le remarquer, on a par défaut une validation liée au type des propriétés. Si vous essayez de placer une String dans un Integer, l'IDE va vous le faire remarquer et l'application ne démarrera pas :
```
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'maxds.mon-autre-super-propriete' to java.lang.Integer:
Property: maxds.mon-autre-super-propriete
Value: this is a String
Origin: class path resource [application.properties]:2:33
Reason: failed to convert java.lang.String to java.lang.Integer
Action:
Update your application's configuration
Process finished with exit code 0
```
Mais on peut affiner le contrôle, de la même façon qu'on validerait le `body` d'un contrôleur REST. On va donc commencer par rajouter l'Api de validation Java et son implémentation de référence : Hibernate Validator. Comme souvent, il y a un `starter` pour ça :
```xml
org.springframework.boot
spring-boot-starter-validation
```
On a maintenant accès à plusieurs annotations intéressantes, dont voici un petit aperçu :
```java
@Validated
@ConfigurationProperties("maxds")
public class MaSuperConfig {
/**
* Une propriété de Type "Super"
*/
@NotEmpty
@Pattern(regexp = "^(?:M(?:AX|ax)|max)$")
private String maSuperPropriete;
/**
* Une propriété de Type "Super"
*/
@Min(42)
@Max(43)
@NotNull
private Integer monAutreSuperPropriete;
// Getters
// Setters
}
```
Après avoir renseigné les propriétés suivantes :
```properties
maxds.ma-super-propriete=Max
maxds.mon-autre-super-propriete=38
```
on a le message d'erreur suivant au démarrage de l'application :
```
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'maxds' to fr.maxds.configproperties.config.MaSuperConfig failed:
Property: maxds.monAutreSuperPropriete
Value: 38
Origin: class path resource [application.properties]:2:33
Reason: doit être supérieur ou égal à 42
Action:
Update your application's configuration
Process finished with exit code 0
```
> Je voulais attirer votre attention sur l'annotation `@Validated` en haut du code Java. Sans cette annotation, la validation ne sera pas lancée !
On va maintenant pousser un peu plus loin notre réflexion.
# Pour aller plus loin
## Des classes de propriétés plus complexes
La première chose qui pourrait venir à l'esprit serait de pouvoir gérer des niveaux de propriétés imbriqués. Pour ne pas pouvoir utiliser `maxds.general.ma-super-propriete` ou `maxds.technique.spring.ma-super-propriete` ? Eh bien, il y a deux façons de procéder.
La première possibilité consiste à utiliser une *inner class* dans la classe de propriétés. Ainsi, si on veut ajouter le niveau `general` dans nos propriétés, on aurait la chose suivante :
```java
@Validated
@ConfigurationProperties("maxds")
public class MaSuperConfig {
/**
* Une propriété de Type "Super"
*/
@NotEmpty
@Pattern(regexp = "^(?:M(?:AX|ax)|max)$")
private String maSuperPropriete;
private General general;
//Getters
//Setters
private static class General {
/**
* Une propriété de Type "Super"
*/
@Min(42)
@Max(43)
@NotNull
private Integer monAutreSuperPropriete;
//Getters
//Setters
}
}
```
> On a ici gardé `maSuperPropriete` au premier niveau.
On est en présence de quelque chose qui fonctionne, mais qui peut vite devenir assez chargé, surtout si l'on a beaucoup de propriétés imbriquées (avec chacune les accesseurs). Personnellement, je préfère la seconde solution :
```java
@Validated
@ConfigurationProperties("maxds")
public class MaSuperConfig {
/**
* Une propriété de Type "Super"
*/
@NotEmpty
@Pattern(regexp = "^(?:M(?:AX|ax)|max)$")
private String maSuperPropriete;
@Valid
@NestedConfigurationProperty
private General general;
//Getters
//Setters
}
```
Et **General.java** :
```java
public class General {
/**
* Une propriété de Type "Super"
*/
@Min(42)
@Max(43)
@NotNull
private Integer monAutreSuperPropriete;
//Getters
//Setters
}
```
Alors il y a deux éléments à remarquer ici. Tout d'abord, dans `MaSuperConfig`, j'ai rajouté `@Valid` et `@NestedConfigurationProperty`. La première, permet de valider le contenu de attributs de `General` (c'est le prolongement de ce qu'on a demandé avec `@Validated`). La seconde, indique à *Spring* que cette classe doit être considérée comme une classe propriété à part entière.
## Des propriétés immutables
Depuis le début de cet article, on se borne à rajouter des accesseurs et des modifiés sur nos propriétés. Sauf que vous avez sûrement remarqué qu'on ne souhaite pas modifier nos propriétés, seulement les lire.
Essayez de supprimer les accesseurs et vous aurez :
```
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'maxds' to fr.maxds.configproperties.config.MaSuperConfig:
Property: maxds.ma-super-propriete
Value:
Origin: class path resource [application.properties]:2:0
Reason: No setter found for property: ma-super-propriete
Action:
Update your application's configuration
Process finished with exit code 0
```
Eh oui, il faut bien que le *framework* puisse valoriser vos classes au moment du démarrage de l'application ! Heureusement, depuis Spring Boot 2.2, il est possible d'y remédier en rajoutant un constructeur et l'annotation `@ConstructorBinding`.
On va donc obtenir l'exemple suivant :
```java
@Validated
@ConfigurationProperties("maxds")
@ConstructorBinding
public class MaSuperConfig {
public MaSuperConfig(String maSuperPropriete, Integer monAutreSuperPropriete) {
this.maSuperPropriete = maSuperPropriete;
this.monAutreSuperPropriete = monAutreSuperPropriete;
}
/**
* Une propriété de Type "Super"
*/
@NotEmpty
@Pattern(regexp = "^(?:M(?:AX|ax)|max)$")
private final String maSuperPropriete;
/**
* Une propriété de Type "Super"
*/
@Min(42)
@Max(43)
@NotNull
private final Integer monAutreSuperPropriete;
//Getter
}
```
# Conclusion
Dans cet article nous avons étudié un peu plus en profondeur l'utilisation de `@ConfigurationProperties` et le confort que ça peut nous apporter lors de nos développements. Comme d'habitude un projet de démonstration est disponible à cette [adresse](https://gitlab.com/maxds-public/spring-boot-configurationproperties-demo).