Contenu

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}) :

1
2
@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 :

1
2
maxds.ma-super-propriete=test
maxds.mon-autre-super-propriete=2

Il ressemble à ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "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 :

1
2
3
4
5
6
7
8
9
@ConfigurationProperties("maxds")
public class MaSuperConfig {

    private String maSuperPropriete;
    private Integer monAutreSuperPropriete;

    // Getters
    // Setters
}

Si d’aventure vous utilisez Lombok, ç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 :

1
2
3
4
5
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

Au prochain build du projet, un fichier spring-configuration-metadata.json sera automatiquement généré dans target/classes/META-INF/ :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 :

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

On a maintenant accès à plusieurs annotations intéressantes, dont voici un petit aperçu :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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 :

1
2
maxds.ma-super-propriete=Max
maxds.mon-autre-super-propriete=38

on a le message d’erreur suivant au démarrage de l’application :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
***************************
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
***************************
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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.