Charger les propriétés de démarrage du printemps à partir d’un fichier JSON

Charger les propriétés d'amorçage du printemps à partir d'un fichier JSON

1. introduction

Utiliser des propriétés de configuration externes est un modèle assez courant.

L'une des questions les plus courantes concerne la possibilité de modifier le comportement de notre application dans plusieurs environnements, tels que le développement, les tests et la production, sans avoir à modifier l'artefact de déploiement.

Dans ce didacticiel,we’ll focus onhow you can load properties from JSON files in a Spring Boot application.

2. Chargement des propriétés dans Spring Boot

Spring et Spring Boot ont un support solide pour le chargement de configurations externes - vous pouvez trouver un excellent aperçu des bases dansthis article.

Depuisthis support mainly focuses on .properties and .yml files – working with JSON typically needs extra configuration.

Nous supposerons que les fonctionnalités de base sont bien connues - et nous nous concentrerons ici sur les aspects spécifiques deJSON.

3. Charger les propriétés via la ligne de commande

Nous pouvons fournir les donnéesJSON dans la ligne de commande dans trois formats prédéfinis.

Tout d'abord, nous pouvons définir la variable d'environnementSPRING_APPLICATION_JSON dans un shellUNIX:

$ SPRING_APPLICATION_JSON='{"environment":{"name":"production"}}' java -jar app.jar

Les données fournies seront renseignées dans les SpringEnvironment. Avec cet exemple, nous obtiendrons une propriétéenvironment.name avec la valeur "production".

De plus, nous pouvons charger nosJSON en tant queSystem property,  par exemple:

$ java -Dspring.application.json='{"environment":{"name":"production"}}' -jar app.jar

La dernière option consiste à utiliser un argument simple en ligne de commande:

$ java -jar app.jar --spring.application.json='{"environment":{"name":"production"}}'

Avec les deux dernières approches, la propriétéspring.application.json sera remplie avec les données données en tant queString non analysés.

Ce sont les options les plus simples pour charger les donnéesJSON dans notre application. The drawback of this minimalistic approach is the lack of scalability.

Le chargement d'une quantité énorme de données dans la ligne de commande peut être fastidieux et source d'erreurs.

4. Charger les propriétés via l'annotationPropertySource

Spring Boot fournit un puissant écosystème pour créer des classes de configuration par le biais d'annotations.

Tout d'abord, nous définissons une classe de configuration avec quelques membres simples:

public class JsonProperties {

    private int port;

    private boolean resend;

    private String host;

   // getters and setters

}

Nous pouvons fournir les données au format standardJSON dans un fichier externe (appelons-leconfigprops.json):

{
  "host" : "[email protected]",
  "port" : 9090,
  "resend" : true
}

Nous devons maintenant connecter notre fichier JSON à la classe de configuration:

@Component
@PropertySource(value = "classpath:configprops.json")
@ConfigurationProperties
public class JsonProperties {
    // same code as before
}

Nous avons un couplage lâche entre la classe et le fichier JSON. Cette connexion est basée sur des chaînes et des noms de variables. Par conséquent, nous n'avons pas de vérification à la compilation, mais nous pouvons vérifier les liaisons avec des tests.

Étant donné que les champs doivent être renseignés par la structure, nous devons utiliser un test d'intégration.

Pour une configuration minimaliste, nous pouvons définir le point d’entrée principal de l’application:

@SpringBootApplication
@ComponentScan(basePackageClasses = { JsonProperties.class})
public class ConfigPropertiesDemoApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class).run();
    }
}

Nous pouvons maintenant créer notre test d'intégration:

@RunWith(SpringRunner.class)
@ContextConfiguration(
  classes = ConfigPropertiesDemoApplication.class)
public class JsonPropertiesIntegrationTest {

    @Autowired
    private JsonProperties jsonProperties;

    @Test
    public void whenPropertiesLoadedViaJsonPropertySource_thenLoadFlatValues() {
        assertEquals("[email protected]", jsonProperties.getHost());
        assertEquals(9090, jsonProperties.getPort());
        assertTrue(jsonProperties.isResend());
    }
}

En conséquence, ce test générera une erreur. Même le chargement desApplicationContext échouera avec la cause suivante:

ConversionFailedException:
Failed to convert from type [java.lang.String]
to type [boolean] for value 'true,'

Le mécanisme de chargement connecte avec succès la classe au fichier JSON via l'annotationPropertySource. Mais la valeur de la propriétéresend est évaluée comme «true,” (avec une virgule), qui ne peut pas être convertie en booléen.

Therefore, we have to inject a JSON parser into the loading mechanism. Heureusement, Spring Boot est fourni avec la bibliothèque Jackson et nous pouvons l'utiliser viaPropertySourceFactory.

5. Utilisation dePropertySourceFactory pour analyser JSON

Nous devons fournir unPropertySourceFactory personnalisé avec la capacité d'analyser les données JSON:

public class JsonPropertySourceFactory
  implements PropertySourceFactory {

    @Override
    public PropertySource createPropertySource(
      String name, EncodedResource resource)
          throws IOException {
        Map readValue = new ObjectMapper()
          .readValue(resource.getInputStream(), Map.class);
        return new MapPropertySource("json-property", readValue);
    }
}

Nous pouvons fournir cette usine pour charger notre classe de configuration. Pour cela, nous devons référencer l'usine à partir de l'annotationPropertySource:

@Configuration
@PropertySource(
  value = "classpath:configprops.json",
  factory = JsonPropertySourceFactory.class)
@ConfigurationProperties
public class JsonProperties {

    // same code as before

}

En conséquence, notre test passera. De plus, cette usine source de propriété analysera volontiers également les valeurs de la liste.

Alors maintenant, nous pouvons étendre notre classe de configuration avec un membre de la liste (et avec les getters et les setters correspondants):

private List topics;
// getter and setter

Nous pouvons fournir les valeurs d'entrée dans le fichier JSON:

{
    // same fields as before
    "topics" : ["spring", "boot"]
}

Nous pouvons facilement tester la liaison des valeurs de liste avec un nouveau scénario de test:

@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenLoadListValues() {
    assertThat(
      jsonProperties.getTopics(),
      Matchers.is(Arrays.asList("spring", "boot")));
}

5.1. Structures imbriquées

La gestion des structures JSON imbriquées n’est pas une tâche facile. En tant que solution la plus robuste, le mappeur de la bibliothèque Jackson mappera les données imbriquées en unMap. 

Nous pouvons donc ajouter un membreMap à notre classeJsonProperties avec des getters et des setters:

private LinkedHashMap sender;
// getter and setter

Dans le fichier JSON, nous pouvons fournir une structure de données imbriquée pour ce champ:

{
  // same fields as before
   "sender" : {
     "name": "sender",
     "address": "street"
  }
}

Nous pouvons maintenant accéder aux données imbriquées via la carte:

@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenNestedLoadedAsMap() {
    assertEquals("sender", jsonProperties.getSender().get("name"));
    assertEquals("street", jsonProperties.getSender().get("address"));
}

6. Utilisation d'unContextInitializer personnalisé

Si nous souhaitons avoir plus de contrôle sur le chargement des propriétés, nous pouvons utiliser desContextInitializers personnalisés.

Cette approche manuelle est plus fastidieuse. Mais, par conséquent, nous aurons le contrôle total du chargement et de l'analyse des données.

Nous utiliserons les mêmes données JSON qu'auparavant, mais nous les chargerons dans une classe de configuration différente:

@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {

    private String host;

    private int port;

    private boolean resend;

    // getters and setters

}

Notez que nous n'utilisons plus l'annotationPropertySource. Mais à l'intérieur de l'annotationConfigurationProperties, nous avons défini un préfixe.

Dans la section suivante, nous allons étudier comment charger les propriétés dans l'espace de noms‘custom'.

6.1. Charger les propriétés dans un espace de noms personnalisé

Pour fournir l'entrée pour la classe de propriétés ci-dessus, nous allons charger les données du fichier JSON et après l'analyse, nous remplirons les SpringEnvironment avecMapPropertySources:

public class JsonPropertyContextInitializer
 implements ApplicationContextInitializer {

    private static String CUSTOM_PREFIX = "custom.";

    @Override
    @SuppressWarnings("unchecked")
    public void
      initialize(ConfigurableApplicationContext configurableApplicationContext) {
        try {
            Resource resource = configurableApplicationContext
              .getResource("classpath:configpropscustom.json");
            Map readValue = new ObjectMapper()
              .readValue(resource.getInputStream(), Map.class);
            Set set = readValue.entrySet();
            List propertySources = set.stream()
               .map(entry-> new MapPropertySource(
                 CUSTOM_PREFIX + entry.getKey(),
                 Collections.singletonMap(
                 CUSTOM_PREFIX + entry.getKey(), entry.getValue()
               )))
               .collect(Collectors.toList());
            for (PropertySource propertySource : propertySources) {
                configurableApplicationContext.getEnvironment()
                    .getPropertySources()
                    .addFirst(propertySource);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Comme on peut le constater, cela nécessite un peu de code complexe, mais c’est le prix de la flexibilité. Dans le code ci-dessus, nous pouvons spécifier notre propre analyseur et décider quoi faire avec chaque entrée.

Dans cette démonstration, nous plaçons simplement les propriétés dans un espace de noms personnalisé.

Pour utiliser cet initialiseur, nous devons le connecter à l'application. Pour une utilisation en production, nous pouvons ajouter ceci dans lesSpringApplicationBuilder:

@EnableAutoConfiguration
@ComponentScan(basePackageClasses = { JsonProperties.class,
  CustomJsonProperties.class })
public class ConfigPropertiesDemoApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class)
            .initializers(new JsonPropertyContextInitializer())
            .run();
    }
}

Notez également que la classeCustomJsonProperties a été ajoutée auxbasePackageClasses.

Pour notre environnement de test, nous pouvons fournir notre initialiseur personnalisé à l'intérieur de l'annotationContextConfiguration:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ConfigPropertiesDemoApplication.class,
  initializers = JsonPropertyContextInitializer.class)
public class JsonPropertiesIntegrationTest {

    // same code as before

}

Après le câblage automatique de notre classeCustomJsonProperties, nous pouvons tester la liaison de données à partir de l'espace de noms personnalisé:

@Test
public void whenLoadedIntoEnvironment_thenFlatValuesPopulated() {
    assertEquals("[email protected]", customJsonProperties.getHost());
    assertEquals(9090, customJsonProperties.getPort());
    assertTrue(customJsonProperties.isResend());
}

6.2. Aplatissement des structures imbriquées

Le framework Spring fournit un mécanisme puissant pour lier les propriétés aux objets membres. Le fondement de cette fonctionnalité est le préfixe de nom dans les propriétés.

Si nous étendons nosApplicationInitializer personnalisés pour convertir les valeurs deMap en une structure d'espace de noms, alors le framework peut charger notre structure de données imbriquée directement dans un objet correspondant.

La classeCustomJsonProperties améliorée:

@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {

   // same code as before

    private Person sender;

    public static class Person {

        private String name;
        private String address;

        // getters and setters for Person class

   }

   // getters and setters for sender member

}

LesApplicationContextInitializer améliorés:

public class JsonPropertyContextInitializer
  implements ApplicationContextInitializer {

    private final static String CUSTOM_PREFIX = "custom.";

    @Override
    @SuppressWarnings("unchecked")
    public void
      initialize(ConfigurableApplicationContext configurableApplicationContext) {
        try {
            Resource resource = configurableApplicationContext
              .getResource("classpath:configpropscustom.json");
            Map readValue = new ObjectMapper()
              .readValue(resource.getInputStream(), Map.class);
            Set set = readValue.entrySet();
            List propertySources = convertEntrySet(set, Optional.empty());
            for (PropertySource propertySource : propertySources) {
                configurableApplicationContext.getEnvironment()
                  .getPropertySources()
                  .addFirst(propertySource);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static List
      convertEntrySet(Set entrySet, Optional parentKey) {
        return entrySet.stream()
            .map((Map.Entry e) -> convertToPropertySourceList(e, parentKey))
            .flatMap(Collection::stream)
            .collect(Collectors.toList());
    }

    private static List
      convertToPropertySourceList(Map.Entry e, Optional parentKey) {
        String key = parentKey.map(s -> s + ".")
          .orElse("") + (String) e.getKey();
        Object value = e.getValue();
        return covertToPropertySourceList(key, value);
    }

    @SuppressWarnings("unchecked")
    private static List
       covertToPropertySourceList(String key, Object value) {
        if (value instanceof LinkedHashMap) {
            LinkedHashMap map = (LinkedHashMap) value;
            Set entrySet = map.entrySet();
            return convertEntrySet(entrySet, Optional.ofNullable(key));
        }
        String finalKey = CUSTOM_PREFIX + key;
        return Collections.singletonList(
          new MapPropertySource(finalKey,
            Collections.singletonMap(finalKey, value)));
    }
}

En conséquence, notre structure de données JSON imbriquée sera chargée dans un objet de configuration:

@Test
public void whenLoadedIntoEnvironment_thenValuesLoadedIntoClassObject() {
    assertNotNull(customJsonProperties.getSender());
    assertEquals("sender", customJsonProperties.getSender()
      .getName());
    assertEquals("street", customJsonProperties.getSender()
      .getAddress());
}

7. Conclusion

L'infrastructure Spring Boot fournit une approche simple pour charger des données JSON externes via la ligne de commande. En cas de besoin, nous pouvons charger des données JSON via desPropertySourceFactory correctement configurés.

Bien que le chargement des propriétés imbriquées puisse être résolu, mais nécessite des précautions supplémentaires.

Comme toujours, le code est disponibleover on GitHub.