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 .