![image](https://habrastorage.org/webt/g5/vp/gz/g5vpgzgoyh2hlalmrajcysuwvwo.jpeg)
Отрывок. 3. Стиль конфигурации двенадцатифакторных приложений
В этой главе будет рассмотрен порядок реализации конфигурации приложения.
Определим ряд словарных терминов. Когда речь заходит о конфигурации в Spring, чаще всего имеется в виду ввод в среду Spring различных реализаций контекста приложения — ApplicationContext, что помогает контейнеру понять, как связать bean-компоненты. Такую конфигурацию можно представить в виде файла в формате XML, который должен быть подан в ClassPathXmlApplicationContext, или классов Java, аннотированных способом, позволяющим быть предоставленными объекту AnnotationConfigApplicationContext. И конечно же, при изучении последнего варианта мы станем ссылаться на конфигурацию Java.
Но в этой главе мы собираемся рассмотреть конфигурацию в том виде, в котором она определена в манифесте 12-факторного приложения. В данном случае она касается буквальных значений, способных изменяться от одной среды окружения к другой: речь идет о паролях, портах и именах хостов или же о флагах свойств. Конфигурация игнорирует встроенные в код магические константы. В манифест включен отличный критерий правильности настройки конфигурации: может ли кодовая база приложения быть открытым источником в любой момент без раскрытия и компрометации важных учетных данных? Эта разновидность конфигурации относится исключительно к тем значениям, которые изменяются от одной среды окружения к другой, и не относится, например, к подключению bean-компонентов Spring или конфигурации маршрутов Ruby.
Поддержка во фреймворке Spring
В Spring стиль конфигурации, соответствующий 12 факторам, поддерживается с появления класса PropertyPlaceholderConfigurer. Как только определяется его экземпляр, он заменяет литералы в XML-конфигурации значениями, извлеченными из файла с расширением .properties. В среде Spring класс PropertyPlaceholderConfigurer предлагается с 2003 года. В Spring 2.5 появилась поддержка пространства имен XML, а вместе с тем и поддержка в данном пространстве подстановки свойств. Это позволяет проводить подстановку в XML-конфигурации литеральных значений определений bean-компонентов значениями, назначенными ключам во внешнем файле свойств (в данном случае в файле simple.properties, который может фигурировать в пути к классам или быть внешним по отношению к приложению).
Конфигурация в стиле 12 факторов нацелена на устранение ненадежности имеющихся магических строк, то есть значений наподобие адресов баз данных и учетных записей для подключения к ним, портов и т.д., жестко заданных в скомпилированном приложении. Если конфигурация вынесена за пределы приложения, то ее можно заменить, не прибегая для этого к новой сборке кода.
Класс PropertyPlaceholderConfigurer
Посмотрим образец использования класса PropertyPlaceholderConfigurer, XML-определений bean-компонентов Spring и вынесенного за пределы приложения файла с расширением .properties. Нам нужно просто вывести значение, имеющееся в данном файле свойств. Это поможет сделать код, показанный в примере 3.1.
Пример 3.1. Файл свойств: some.properties
configuration.projectName=Spring Framework
Это принадлежащий Spring класс ClassPathXmlApplicationContext, таким образом, мы используем пространство имен XML из контекста Spring и указываем на наш файл some.properties. Затем в определениях bean-компонентов задействуем литералы в форме ${configuration.projectName}, и Spring в ходе выполнения заменит их значениями из нашего файла свойств (пример 3.2).
Пример 3.2. XML-файл Spring-конфигурации
<context:property-placeholder location="classpath:some.properties"/> (1)
<bean class="classic.Application">
<property name="configurationProjectName"
value="${configuration.projectName}"/>
</bean>
(1) classpath: местоположение, ссылающееся на файл в текущем откомпилированном блоке кода (.jar, .war и т. д.). Spring поддерживает множество альтернативных вариантов, включая file: и url:, позволяющих файлу существовать обособленно от блока кода.
И наконец, рассмотрим, как выглядит класс Java, благодаря которому возможно свести все это воедино (пример 3.3).
Пример 3.3. Класс Java, который должен быть сконфигурирован со значением свойства
package classic;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Application {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("classic.xml");
}
public void setConfigurationProjectName(String pn) {
LogFactory.getLog(getClass()).info("the configuration project name is " + pn);
}
}
В первом примере используется XML-формат конфигурации bean-компонентов Spring. В Spring 3.0 и 3.1 ситуация для разработчиков, применяющих конфигурацию Java, значительно улучшилась. В этих выпусках были введены аннотация Value и абстракция Environment.
Абстракция Environment и Value
Абстракция Environment представляет в ходе выполнения кода его косвенное отношение к той среде окружения, в которой он запущен, и позволяет приложению ставить вопрос («Какой разделитель строк line.separator на данной платформе?») о свойствах среды. Абстракция действует в качестве отображения из ключей и значений. Сконфигурировав в Environment источник свойств PropertySource, можно настроить то место, откуда эти значения будут считываться. По умолчанию Spring загружает системные ключи и значения среды, такие как line.separator. Системе Spring можно предписать загрузку ключей конфигурации из файла в том же порядке, который мог бы использоваться в ранних выпусках решения Spring по подстановке свойств с помощью аннотации @PropertySource.
Аннотация Value предоставляет способ внедрения значений среды окружения в конструкторы, сеттеры, поля и т. д. Эти значения могут быть вычислены с помощью языка выражений Spring Expression Language или синтаксиса подстановки свойств при условии регистрации PropertySourcesPlaceholderConfigurer, как сделано в примере 3.4.
Пример 3.4. Регистрация PropertySourcesPlaceholderConfigurer
package env;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
(1)
@Configuration
@PropertySource("some.properties")
public class Application {
private final Log log = LogFactory.getLog(getClass());
public static void main(String[] args) throws Throwable {
new AnnotationConfigApplicationContext(Application.class);
}
(2)
@Bean
static PropertySourcesPlaceholderConfigurer pspc() {
return new PropertySourcesPlaceholderConfigurer();
}
(3)
@Value("${configuration.projectName}")
private String fieldValue;
(4)
@Autowired
Application(@Value("${configuration.projectName}") String pn) {
log.info("Application constructor: " + pn);
}
(5)
@Value("${configuration.projectName}")
void setProjectName(String projectName) {
log.info("setProjectName: " + projectName);
}
(6)
@Autowired
void setEnvironment(Environment env) {
log.info("setEnvironment: " + env.getProperty("configuration.projectName"));
}
(7)
@Bean
InitializingBean both(Environment env,
@Value("${configuration.projectName}") String projectName) {
return () -> {
log.info("@Bean with both dependencies (projectName): " + projectName);
log.info("@Bean with both dependencies (env): "
+ env.getProperty("configuration.projectName"));
};
}
@PostConstruct
void afterPropertiesSet() throws Throwable {
log.info("fieldValue: " + this.fieldValue);
}
}
(1) Аннотация @PropertySource — сокращение наподобие property-placeholder, настраивающее PropertySource из файла с расширением .properties.
(2) PropertySourcesPlaceholderConfigurer нужно зарегистрировать в качестве статического bean-компонента, поскольку он является реализацией BeanFactoryPostProcessor и должен вызываться на ранней стадии жизненного цикла инициализации в Spring bean-компонентов. При использовании в Spring XML-конфигурации bean-компонентов этот нюанс не просматривается.
(3) Можно отдекорировать поля аннотацией Value (но не делайте этого, иначе код не пройдет тестирование!)…
(4) …или аннотацией Value можно отдекорировать параметры конструктора…
(5) …или воспользоваться методами установки…
(6) …или внедрить объект Spring Environment и выполнить разрешение ключа вручную.
(7) Параметры с аннотацией Value можно использовать также в поставщике аргументов методов Bean в Java-конфигурации Spring.
В этом примере значения загружаются из файла simple.properties, а затем в нем имеется значение configuration.projectName, предоставляемое различными способами.
Профили
Кроме всего прочего, абстракция Environment вводит профили. Это позволяет приписывать метки (профили) в целях группировки bean-компонентов. Профили следует использовать для описания bean-компонентов и bean-графов, изменяющихся от среды к среде. Одновременно могут активироваться сразу несколько профилей. Bean-компоненты, не имеющие назначенных им профилей, активируются всегда. Bean-компоненты, имеющие профиль default, активируются только в том случае, если у них нет других активных профилей. Атрибут profile можно указать в определении bean-компонента в XML либо в классах тегов, классах конфигурации, отдельно взятых bean-компонентах или в методах Bean-поставщика с помощью Profile.
Профили позволяют описывать наборы bean-компонентов, которые должны быть созданы в одной среде несколько иначе, чем в другой. В локальном разработочном dev-профиле можно, к примеру, воспользоваться встроенным источником данных H2 javax.sql.DataSource, а затем, когда активен prod-профиль, переключиться на источник данных javax.sql.DataSource для PostgreSQL, получаемый с помощью JNDI-поиска или путем чтения свойств из переменной среды в Cloud Foundry. В обоих случаях ваш код будет работоспособен: вы получаете javax.sql.DataSource, но решение о том, какой конкретный экземпляр задействовать, принимается с помощью активации одного профиля или нескольких (пример 3.5).
Пример 3.5. Демонстрация того, что классы @Configuration могут загружать различные файлы конфигурации и предоставлять различные bean-компоненты на основе
активного профиля
package profiles;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
@Configuration
public class Application {
private Log log = LogFactory.getLog(getClass());
@Bean
static PropertySourcesPlaceholderConfigurer pspc() {
return new PropertySourcesPlaceholderConfigurer();
}
(1)
@Configuration
@Profile("prod")
@PropertySource("some-prod.properties")
public static class ProdConfiguration {
@Bean
InitializingBean init() {
return () -> LogFactory.getLog(getClass()).info("prod InitializingBean");
}
}
@Configuration
@Profile({ "default", "dev" })
(2)
@PropertySource("some.properties")
public static class DefaultConfiguration {
@Bean
InitializingBean init() {
return () -> LogFactory.getLog(getClass()).info("default InitializingBean");
}
}
(3)
@Bean
InitializingBean which(Environment e,
@Value("${configuration.projectName}") String
projectName) {
return () -> {
log.info("activeProfiles: '"
+ StringUtils.arrayToCommaDelimitedString(e.getActiveProfiles()) +
"'");
log.info("configuration.projectName: " + projectName);
};
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext();
ac.getEnvironment().setActiveProfiles("dev"); (4)
ac.register(Application.class);
ac.refresh();
}
}
(1) Этот класс конфигурации и все имеющиеся в нем определения Bean будут вычислены только в случае активности prod-профиля.
(2) Данный класс конфигурации и все имеющиеся в нем определения Bean будут вычислены, только если активен dev-профиль или не активен ни один профиль, включая dev.
(3) Этот компонент InitializingBean просто записывает текущий активный профиль и вводит значение, которое в конечном итоге было внесено в файл свойств.
(4) Активировать профиль (или профили) программным способом довольно просто.
Spring откликается еще на несколько других методов активации профилей, использующих токен spring_profiles_active или spring.profiles.active. Профиль можно установить с помощью переменной среды (например, SPRING_PROFILES_ACTIVE), JVM-свойства (‑Dspring.profiles.active=… ), параметра инициализации сервлет-приложения или программным способом.
Конфигурация Bootiful
Spring Boot существенно улучшает ситуацию. Среда изначально автоматически загрузит свойства из иерархии заранее известных мест. Аргументы командной строки переопределяют значения свойств, полученных из JNDI, которые переопределяют свойства, полученные из System.getProperties(), и т. д.
— Аргументы командной строки.
— Атрибуты JNDI из java:comp/env.
— Свойства System.getProperties().
— Переменные среды операционной системы.
— Внешние файлы свойств в файловой системе: (config/)?application.(yml.properties).
— Внутренние файлы свойств в архиве (config/)?application.(yml.properties).
— Аннотация @PropertySource в классах конфигурации.
— Исходные свойства из SpringApplication.getDefaultProperties().
В случае активности профиля будут автоматически считаны данные из файлов конфигурации, основанных на имени профиля, например из такого файла, как src/main/resources/application-foo.properties, где foo — текущий профиль.
Если библиотека SnakeYAML упомянута в путях к классам (classpath), то будут также автоматически загружены YAML-файлы, следуя в основном тому же соглашению.
На странице спецификации YAML указано, что «YAML является удобным для человеческого восприятия стандартом сериализации данных для всех языков программирования». YAML — иерархическое представление значений. В обычных файлах с расширением .properties иерархия обозначается с помощью точки («.»), а в YAML-файлах используется символ новой строки и дополнительный уровень отступа. Было бы неплохо воспользоваться этими файлами, чтобы избежать необходимости указывать общие корни при наличии сильно разветвленных деревьев конфигурации.
Содержимое файла с расширением .yml показано в примере 3.6.
Пример 3.6. Файл свойств application.yml. Данные изложены в иерархическом порядке
configuration:
projectName : Spring Boot
management:
security:
enabled: false
Кроме того, среда Spring Boot существенно упрощает получение правильного результата в общих случаях. Она превращает аргументы -D в переменные процесса и среды java, доступные в качестве свойств. Она даже проводит их нормализацию, при которой переменная среды $CONFIGURATION_PROJECTNAME (КОНФИГУРАЦИЯ_ИМЯПРОЕКТА) или аргумент -D в форме ‑Dconfiguration.projectName (конфигурация.имя_проекта) становятся доступными с помощью ключа configuration.projectName (конфигурация.имя_проекта) точно так же, как ранее был доступен токен spring_profiles_active.
Значения конфигурации являются строками и при их достаточном количестве могут стать неудобочитаемыми при попытке убедиться, что такие ключи не стали сами по себе магическими строками в коде. В Spring Boot вводится тип компонента @ConfigurationProperties. При аннотировании POJO — Plain Old Java Object (обычный старый объект Java) — с помощью @ConfigurationProperties и указании префикса среда Spring предпримет попытку отобразить все свойства, начинающиеся с этого префикса, на POJO-свойства. В показанном ниже примере значение для configuration.projectName будет отображено на экземпляр POJO, который весь код затем может внедрить и разыменовать для типобезопасного чтения значений. Как следствие, у вас будет только отображение из (String) ключа в одном месте (пример 3.7).
Пример 3.7. Автоматическое разрешение свойств из src/main/resources/application.yml
package boot;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
(1)
@EnableConfigurationProperties
@SpringBootApplication
public class Application {
private final Log log = LogFactory.getLog(getClass());
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Autowired
public Application(ConfigurationProjectProperties cp) {
log.info("configurationProjectProperties.projectName = "
+ cp.getProjectName());
}
}
(2)
@Component
@ConfigurationProperties("configuration")
class ConfigurationProjectProperties {
private String projectName; (3)
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
}
(1) Аннотация @EnableConfigurationProperties предписывает среде Spring отображать свойства на POJO-объекты, аннотированные с помощью @ConfigurationProperties.
(2) Аннотация @ConfigurationProperties показывает среде Spring, что этот bean-компонент должен использоваться как корневой для всех свойств, начинающихся с configuration., с последующими токенами, отображаемыми на свойства объекта.
(3) Поле projectName будет в конечном итоге иметь значение, присвоенное ключу свойства configuration.projectName.
Spring Boot активно применяет механизм @ConfigurationProperties, чтобы дать пользователям возможность переопределять элементарные составляющие системы. Можно заметить, что ключи свойств позволяют вносить изменения, например, путем добавления зависимости org.springframework.boot:spring-boot-starter-actuator в веб-приложение на основе Spring Boot с последующим посещением страницы 127.0.0.1:8080/configprops.
Конечные точки актуатора более подробно будут рассмотрены в главе 13. Они заперты и требуют по умолчанию имени пользователя и пароля. Меры безопасности можно отключить (но только чтобы взглянуть на эти точки), указав management.security.enabled=false в файле application.properties (или в application.yml).
Вы получите список поддерживаемых свойств конфигурации на основе типов, представленных в путях к классам (classpath) во время выполнения. По мере наращивания количества типов Spring Boot будут показываться дополнительные свойства. В этой конечной точке также станут отображаться свойства, экспортированные вашими POJO-объектами, имеющими аннотацию @ConfigurationProperties.
Централизованная регистрируемая конфигурация с использованием сервера конфигурации Spring Cloud
Пока все хорошо, однако нужно, чтобы дела шли еще успешнее. Мы по-прежнему не ответили на вопросы, касающиеся общих случаев применения:
- после изменений, внесенных в конфигурацию приложения, потребуется перезапуск;
- отсутствует прослеживаемость: как определить изменения, введенные в эксплуатацию, и при необходимости выполнить их откат?
- конфигурация децентрализована; не сразу видно, куда следует вносить изменения, чтобы изменить тот или иной аспект;
- отсутствует установочная поддержка для кодирования и декодирования в целях безопасности.
Сервер конфигурации Spring Cloud
Проблему централизации конфигурации можно решить, сохранив конфигурацию в одном каталоге и указав всем приложениям на него. Можно также установить управление версиями этого каталога, используя Git или Subversion. Тогда будет получена поддержка, необходимая для проверки и регистрирования. Но последние два требования по-прежнему не будут выполнены, поэтому нужно нечто более изощренное. Обратимся к серверу конфигурации Spring Cloud. Платформа Spring Cloud предлагает сервер конфигурации и клиента для этого сервера.
Сервер Spring Cloud Config представляет собой REST API, к которому будут подключаться наши клиенты, чтобы забирать свою конфигурацию. Сервер также управляет хранилищем конфигураций с управлением версиями. Он посредник между нашими клиентами и хранилищем конфигурации и таким образом находится в выгодной позиции, позволяющей внедрять средства обеспечения безопасности подключений со стороны клиентов к сервису и подключений со стороны сервиса к хранилищу конфигураций с управлением версиями. Клиент Spring Cloud Config предоставляет клиентским приложениям новую область видимости, refresh, дающую возможность конфигурировать компоненты Spring заново, не прибегая к перезапуску приложения.
Технологии, подобные серверу Spring Cloud Config, играют важную роль, но влекут дополнительные рабочие издержки. В идеале эта обязанность должна быть переложена на платформу и автоматизирована. При использовании Cloud Foundry в каталоге сервисов можно найти сервис Config Server, действия которого основаны на применении сервера Spring Cloud Config.
Рассмотрим простой пример. Сначала настроим сервер Spring Cloud Config. К одному такому сервису могут иметь доступ сразу несколько приложений Spring Boot. Вам нужно где-то и как-то заставить его функционировать. Затем останется только оповестить все наши сервисы о том, где найти сервис конфигурации. Он работает как некий посредник для ключей конфигурации и значений, которые он считывает из Git-хранилища по сети или с диска. Добавьте к сборке вашего приложения Spring Boot строку org.springframework.cloud: spring-cloud-config-server, чтобы ввести сервер Spring Cloud Config (пример 3.8).
Пример 3.8. Чтобы встроить в сборку сервер конфигурации, воспользуйтесь аннотацией @EnableConfigServer
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
(1)
@SpringBootApplication
@EnableConfigServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
(1) Использование аннотации @EnableConfigServer приводит к установке сервера Spring Cloud Config.
В примере 3.9 показана конфигурация для сервиса конфигурации.
Пример 3.9. Конфигурация сервера конфигурации src/main/resources/application.yml
server.port=8888
spring.cloud.config.server.git.uri=\
github.com/cloud-native-java/config-server-configuration-repository (1)
(1) Указание на работающее Git-хранилище, имеющее локальный характер либо доступное по сети (например, на GitHub (https://github.com/)) и используемое сервером Spring Cloud Config.
Здесь сервису конфигурации Spring Cloud предписывается выполнить поиск файлов конфигурации для отдельно взятых клиентов в Git-хранилище на GitHub. Мы указали на это хранилище, но подошла бы ссылка на любой действующий Git URI. Разумеется, он даже не обязан относится к Git-системе, можно воспользоваться Subversion или даже неуправляемыми каталогами (хотя мы настоятельно не рекомендуем делать это). В данном случае URI хранилища жестко задан, но нет ничего, что помешает получить его из аргумента -D, аргумента — или из переменной среды окружения.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 20% по купону — Java