Рубрики
Без рубрики

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

Узнайте, как загрузить свойства конфигурации для Spring Boot из файла JSON

Автор оригинала: baeldung.

1. введение

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

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

В этом уроке мы сосредоточимся на том, как вы можете загружать свойства из JSON-файлов в приложение Spring Boot .

2. Свойства загрузки в пружинном ботинке

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

Поскольку эта поддержка в основном фокусируется на .properties и . yml files – работа с JSON обычно требует дополнительной настройки .

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

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

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

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

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

Предоставленные данные будут заполнены в Spring Environment . В этом примере мы получим свойство environment.name со значением “производство”.

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

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

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

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

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

Это самые простые варианты загрузки данных JSON в наше приложение. Недостатком этого минималистичного подхода является отсутствие масштабируемости.

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

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”, (с запятой), которое не может быть преобразовано в логическое.

Поэтому мы должны ввести парсер JSON в механизм загрузки. К счастью, 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 в наш класс Json Properties с геттерами и сеттерами:

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. Использование пользовательского контекстинициализатора

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

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

Мы будем использовать те же данные 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 и после синтаксического анализа заполним Spring Environment с помощью 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();
    }
}

Кроме того, обратите внимание, что класс Custom Json Properties был добавлен в класс basePackageClasses .

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

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

    // same code as before

}

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

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

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

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

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

Расширенные Пользовательские Свойства Json класс:

@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 framework обеспечивает простой подход к загрузке внешних данных JSON через командную строку. В случае необходимости мы можем загрузить данные JSON через правильно настроенный PropertySourceFactory .

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

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