Загрузить свойства Spring Boot из файла JSON

Загрузить Spring Boot Properties из файла JSON

1. Вступление

Использование внешних свойств конфигурации является довольно распространенной моделью.

И один из наиболее распространенных вопросов - это возможность изменить поведение нашего приложения в нескольких средах, таких как разработка, тестирование и производство, без необходимости изменения артефакта развертывания.

В этом руководствеwe’ll focus onhow you can load properties from JSON files in a Spring Boot application.

2. Загрузка свойств в Spring Boot

Spring и Spring Boot имеют сильную поддержку загрузки внешних конфигураций - вы можете найти отличный обзор основ вthis article.

Посколькуthis support mainly focuses on .properties and .yml files – working with JSON typically needs extra configuration.

Мы предполагаем, что основные функции хорошо известны - и сосредоточимся здесь на конкретных аспектахJSON.

3. Загрузить свойства через командную строку

Мы можем предоставить данныеJSON в командной строке в трех предопределенных форматах.

Во-первых, мы можем установить переменную окруженияSPRING_APPLICATION_JSON в оболочкеUNIX:

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

Предоставленные данные будут заполнены в SpringEnvironment. В этом примере мы получим свойствоenvironment.name со значением «production».

Кроме того, мы можем загрузить нашJSON какSystem property, , например:

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

Последний вариант - использовать простой аргумент командной строки:

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

В последних двух подходах свойствоspring.application.json будет заполнено заданными данными как неанализируемыеString.

Это самые простые варианты загрузки данныхJSON в наше приложение. The drawback of this minimalistic approach is the lack of scalability.

Загрузка огромного количества данных в командной строке может быть громоздкой и подверженной ошибкам.

4. Загрузить свойства через аннотациюPropertySource

Spring Boot предоставляет мощную экосистему для создания классов конфигурации с помощью аннотаций.

Прежде всего, мы определяем класс конфигурации с несколькими простыми членами:

public class JsonProperties {

    private int port;

    private boolean resend;

    private String host;

   // getters and setters

}

Мы можем предоставить данные в стандартном форматеJSON во внешнем файле (назовем егоconfigprops.json):

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

Теперь нам нужно подключить наш JSON-файл к классу конфигурации:

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

У нас есть слабая связь между классом и файлом JSON. Это соединение основано на строках и именах переменных. Поэтому у нас нет проверки во время компиляции, но мы можем проверить привязки с помощью тестов.

Поскольку поля должны быть заполнены платформой, нам нужно использовать интеграционный тест.

Для минималистической установки мы можем определить основную точку входа в приложение:

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

Теперь мы можем создать наш интеграционный тест:

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

В результате этот тест выдаст ошибку. Даже загрузкаApplicationContext не удастся по следующей причине:

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

Механизм загрузки успешно связывает класс с файлом JSON через аннотациюPropertySource. Но значение свойстваresend оценивается как «true,” (с запятой), которое не может быть преобразовано в логическое значение.

Therefore, we have to inject a JSON parser into the loading mechanism. К счастью, Spring Boot поставляется с библиотекой Джексона, и мы можем использовать ее черезPropertySourceFactory.

5. ИспользованиеPropertySourceFactory для синтаксического анализа JSON

Мы должны предоставить настраиваемыйPropertySourceFactory с возможностью анализа данных 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);
    }
}

Мы можем предоставить эту фабрику для загрузки нашего класса конфигурации. Для этого мы должны сослаться на фабрику из аннотацииPropertySource:

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

    // same code as before

}

В итоге наш тест пройдет. Кроме того, эта фабрика источника свойств будет также успешно анализировать значения списка.

Итак, теперь мы можем расширить наш класс конфигурации с помощью члена списка (и с соответствующими методами получения и установки):

private List topics;
// getter and setter

Мы можем предоставить входные значения в файле JSON:

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

Мы можем легко проверить привязку значений списка с помощью нового теста:

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

5.1. Вложенные структуры

Работа с вложенными структурами JSON - непростая задача. В качестве более надежного решения средство отображения библиотеки Джексона отобразит вложенные данные вMap. 

Итак, мы можем добавить членMap в наш классJsonProperties с помощью методов получения и установки:

private LinkedHashMap sender;
// getter and setter

В файле JSON мы можем предоставить вложенную структуру данных для этого поля:

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

Теперь мы можем получить доступ к вложенным данным через карту:

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

6. Использование пользовательскогоContextInitializer

Если мы хотим иметь больший контроль над загрузкой свойств, мы можем использовать пользовательскийContextInitializers.

Этот ручной подход более утомителен. Но в результате мы получим полный контроль над загрузкой и анализом данных.

Мы будем использовать те же данные JSON, что и раньше, но загрузим в другой класс конфигурации:

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

    private String host;

    private int port;

    private boolean resend;

    // getters and setters

}

Обратите внимание, что мы больше не используем аннотациюPropertySource. Но внутри аннотацииConfigurationProperties мы определили префикс.

В следующем разделе мы исследуем, как мы можем загрузить свойства в пространство имен‘custom'.

6.1. Загрузить свойства в настраиваемое пространство имен

Чтобы предоставить входные данные для класса свойств выше, мы загрузим данные из файла JSON и после анализа заполним SpringEnvironment значениемMapPropertySources:.

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

Как мы видим, он требует немного сложного кода, но это цена гибкости. В приведенном выше коде мы можем указать наш собственный анализатор и решить, что делать с каждой записью.

В этой демонстрации мы просто помещаем свойства в настраиваемое пространство имен.

Чтобы использовать этот инициализатор, мы должны связать его с приложением. Для производственного использования мы можем добавить это вSpringApplicationBuilder:

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

Также обратите внимание, что классCustomJsonProperties был добавлен вbasePackageClasses.

Для нашей тестовой среды мы можем предоставить наш настраиваемый инициализатор внутри аннотацииContextConfiguration:

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

    // same code as before

}

После автоматического подключения нашего классаCustomJsonProperties мы можем протестировать привязку данных из пользовательского пространства имен:

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

6.2. Сглаживание вложенных структур

Среда Spring предоставляет мощный механизм для привязки свойств к элементам объектов. Основой этой функции являются префиксы имен в свойствах.

Если мы расширим наш собственныйApplicationInitializer, чтобы преобразовать значенияMap в структуру пространства имен, то инфраструктура сможет загрузить нашу вложенную структуру данных непосредственно в соответствующий объект.

Расширенный классCustomJsonProperties:

@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

}

РасширенныйApplicationContextInitializer:

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

В результате наша вложенная структура данных JSON будет загружена в объект конфигурации:

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

7. Заключение

Платформа Spring Boot предоставляет простой подход для загрузки внешних данных JSON через командную строку. В случае необходимости мы можем загрузить данные JSON через правильно настроенныйPropertySourceFactory.

Хотя загрузка вложенных свойств решаема, но требует особого внимания.

Как всегда доступен кодover on GitHub.