Загрузить 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.