Spring Boot-Eigenschaften aus einer JSON-Datei laden

Laden von Spring Boot-Eigenschaften aus einer JSON-Datei

1. Einführung

Die Verwendung externer Konfigurationseigenschaften ist ein weit verbreitetes Muster.

Eine der häufigsten Fragen ist die Möglichkeit, das Verhalten unserer Anwendung in mehreren Umgebungen (z. B. Entwicklung, Test und Produktion) zu ändern, ohne das Bereitstellungsartefakt ändern zu müssen.

In diesem Tutorial werdenwe’ll focus onhow you can load properties from JSON files in a Spring Boot application.

2. Laden von Eigenschaften in Spring Boot

Spring und Spring Boot unterstützen das Laden externer Konfigurationen - Sie finden einen guten Überblick über die Grundlagen inthis article.

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

Wir gehen davon aus, dass die Grundfunktionen bekannt sind - und konzentrieren uns hier auf die spezifischen Aspekte vonJSON.

3. Laden Sie die Eigenschaften über die Befehlszeile

Wir könnenJSON Daten in der Befehlszeile in drei vordefinierten Formaten bereitstellen.

Zunächst können wir die UmgebungsvariableSPRING_APPLICATION_JSON in einerUNIX-Shell festlegen:

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

Die bereitgestellten Daten werden in die SpringEnvironment eingetragen. In diesem Beispiel erhalten wir eine Eigenschaftenvironment.name mit dem Wert "Produktion".

Außerdem können wir unsereJSON alsSystem property,  laden, zum Beispiel:

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

Die letzte Option ist die Verwendung eines einfachen Befehlszeilenarguments:

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

Bei den letzten beiden Ansätzen wird die Eigenschaftspring.application.jsonmit den angegebenen Daten als nicht analysierteString gefüllt.

Dies sind die einfachsten Optionen zum Laden vonJSON-Daten in unsere Anwendung. The drawback of this minimalistic approach is the lack of scalability.

Das Laden einer großen Datenmenge in die Befehlszeile kann umständlich und fehleranfällig sein.

4. Laden Sie die Eigenschaften überPropertySource Annotation

Spring Boot bietet ein leistungsfähiges Ökosystem zum Erstellen von Konfigurationsklassen durch Anmerkungen.

Zunächst definieren wir eine Konfigurationsklasse mit einigen einfachen Elementen:

public class JsonProperties {

    private int port;

    private boolean resend;

    private String host;

   // getters and setters

}

Wir können die Daten im StandardformatJSONin einer externen Datei bereitstellen (nennen wir sieconfigprops.json):

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

Jetzt müssen wir unsere JSON-Datei mit der Konfigurationsklasse verbinden:

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

Wir haben eine lose Kopplung zwischen der Klasse und der JSON-Datei. Diese Verbindung basiert auf Zeichenfolgen und Variablennamen. Daher haben wir keine Überprüfung der Kompilierungszeit, aber wir können die Bindungen mit Tests überprüfen.

Da die Felder vom Framework ausgefüllt werden sollen, müssen wir einen Integrationstest durchführen.

Für ein minimalistisches Setup können wir den Haupteinstiegspunkt der Anwendung definieren:

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

Jetzt können wir unseren Integrationstest erstellen:

@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());
    }
}

Infolgedessen erzeugt dieser Test einen Fehler. Selbst das Laden derApplicationContext schlägt mit der folgenden Ursache fehl:

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

Der Lademechanismus verbindet die Klasse erfolgreich mit der JSON-Datei über die AnnotationPropertySource. Der Wert für die Eigenschaftresend wird jedoch als „true,” (mit Komma) ausgewertet, der nicht in einen Booleschen Wert konvertiert werden kann.

Therefore, we have to inject a JSON parser into the loading mechanism. Glücklicherweise wird Spring Boot mit der Jackson-Bibliothek geliefert und wir können sie überPropertySourceFactory verwenden.

5. Verwenden vonPropertySourceFactory zum Analysieren von JSON

Wir müssen ein benutzerdefiniertesPropertySourceFactory bereitstellen, mit dem JSON-Daten analysiert werden können:

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

Wir können diese Factory bereitstellen, um unsere Konfigurationsklasse zu laden. Dazu müssen wir die Factory aus der Annotation vonPropertySourcereferenzieren:

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

    // same code as before

}

Infolgedessen wird unser Test bestanden. Darüber hinaus analysiert diese Eigenschaftsquellenfactory auch gerne Listenwerte.

Nun können wir unsere Konfigurationsklasse um ein Listenmitglied (und die entsprechenden Getter und Setter) erweitern:

private List topics;
// getter and setter

Wir können die Eingabewerte in der JSON-Datei bereitstellen:

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

Wir können die Bindung von Listenwerten einfach mit einem neuen Testfall testen:

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

5.1. Verschachtelte Strukturen

Der Umgang mit verschachtelten JSON-Strukturen ist keine leichte Aufgabe. Als robustere Lösung ordnet der Mapper der Jackson-Bibliothek die verschachtelten Daten einemMap. zu

So können wir unsererJsonProperties-Klasse einMap-Mitglied mit Getter und Setter hinzufügen:

private LinkedHashMap sender;
// getter and setter

In der JSON-Datei können wir eine verschachtelte Datenstruktur für dieses Feld bereitstellen:

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

Jetzt können wir über die Karte auf die verschachtelten Daten zugreifen:

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

6. Verwenden eines benutzerdefiniertenContextInitializer

Wenn Sie mehr Kontrolle über das Laden von Eigenschaften haben möchten, können Sie benutzerdefinierteContextInitializersverwenden.

Dieser manuelle Ansatz ist langwieriger. Infolgedessen haben wir jedoch die volle Kontrolle über das Laden und Parsen der Daten.

Wir verwenden dieselben JSON-Daten wie zuvor, laden sie jedoch in eine andere Konfigurationsklasse:

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

    private String host;

    private int port;

    private boolean resend;

    // getters and setters

}

Beachten Sie, dass wir die AnnotationPropertySourcenicht mehr verwenden. In der AnnotationConfigurationPropertieshaben wir jedoch ein Präfix definiert.

Im nächsten Abschnitt werden wir untersuchen, wie wir die Eigenschaften in den Namespace von‘custom'laden können.

6.1. Laden Sie Eigenschaften in einen benutzerdefinierten Namespace

Um die Eingabe für die oben genannte Eigenschaftenklasse bereitzustellen, laden wir die Daten aus der JSON-Datei und füllen nach dem Parsen die SpringEnvironment mitMapPropertySources:

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

Wie wir sehen können, erfordert es einen recht komplexen Code, aber dies ist der Preis für Flexibilität. Im obigen Code können wir unseren eigenen Parser angeben und entscheiden, was mit jedem Eintrag geschehen soll.

In dieser Demonstration fügen wir die Eigenschaften einfach in einen benutzerdefinierten Namespace ein.

Um diesen Initialisierer zu verwenden, müssen wir ihn mit der Anwendung verbinden. Für die Verwendung in der Produktion können wir dies inSpringApplicationBuilder hinzufügen:

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

Beachten Sie außerdem, dass die KlasseCustomJsonProperties zubasePackageClasses hinzugefügt wurde.

Für unsere Testumgebung können wir unseren benutzerdefinierten Initialisierer innerhalb der AnnotationContextConfigurationbereitstellen:

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

    // same code as before

}

Nach der automatischen Verkabelung unsererCustomJsonProperties-Klasse können wir die Datenbindung über den benutzerdefinierten Namespace testen:

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

6.2. Abflachen verschachtelter Strukturen

Das Spring-Framework bietet einen leistungsstarken Mechanismus zum Binden der Eigenschaften in Objektmitglieder. Die Grundlage für diese Funktion sind die Namenspräfixe in den Eigenschaften.

Wenn wir unsere benutzerdefiniertenApplicationInitializer erweitern, um dieMap-Werte in eine Namespace-Struktur zu konvertieren, kann das Framework unsere verschachtelte Datenstruktur direkt in ein entsprechendes Objekt laden.

Die erweiterteCustomJsonProperties-Klasse:

@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

}

Die erhöhtenApplicationContextInitializer:

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

Infolgedessen wird unsere verschachtelte JSON-Datenstruktur in ein Konfigurationsobjekt geladen:

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

7. Fazit

Das Spring Boot-Framework bietet einen einfachen Ansatz zum Laden externer JSON-Daten über die Befehlszeile. Bei Bedarf können wir JSON-Daten über ordnungsgemäß konfiguriertePropertySourceFactory laden.

Das Laden verschachtelter Eigenschaften ist zwar lösbar, erfordert jedoch zusätzliche Sorgfalt.

Wie immer ist der Codeover on GitHub verfügbar.