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 Listtopics; // 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 LinkedHashMapsender; // 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 .