Перейти к основному содержимому

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

· 8 мин. чтения

1. Введение

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

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

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

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

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

Поскольку эта поддержка в основном ориентирована на .properties и . yml — для работы с 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 будет заполнено заданными данными в виде неразобранной строки 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" : "mailer@mail.com",
"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("mailer@mail.com", 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 поставляется с библиотекой Jackson, и мы можем использовать ее через 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<String> 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 в наш класс JsonProperties с помощью геттеров и сеттеров:

private LinkedHashMap<String, ?> 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 мы определили префикс.

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

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

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

public class JsonPropertyContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {

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<Map.Entry> set = readValue.entrySet();
List<MapPropertySource> 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("mailer@mail.com", 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<ConfigurableApplicationContext> {

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<Map.Entry> set = readValue.entrySet();
List<MapPropertySource> propertySources = convertEntrySet(set, Optional.empty());
for (PropertySource propertySource : propertySources) {
configurableApplicationContext.getEnvironment()
.getPropertySources()
.addFirst(propertySource);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

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

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

@SuppressWarnings("unchecked")
private static List<MapPropertySource>
covertToPropertySourceList(String key, Object value) {
if (value instanceof LinkedHashMap) {
LinkedHashMap map = (LinkedHashMap) value;
Set<Map.Entry> 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 .

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

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