Одним из важных шагов для обеспечения возможности настройки программных приложений является эффективное управление конфигурацией. Современные платформы предоставляют готовые функции для вывода параметров конфигурации.
Для некоторых параметров конфигурации имеет смысл не запускать приложение, если они недействительны.
Spring Boot предлагает нам удобный способ валидации параметров конфигурации. Мы собираемся связать входные значения со @ConfigurationProperties и использовать Bean Validation для их валидации.
Код примера
Эта статья сопровождается примером рабочего кода на GitHub.
Зачем нам нужно проверять параметры конфигурации?
Выполнение правильной валидации наших параметров конфигурации иногда может быть критическим.
Давайте рассмотрим следующий сценарий:
Мы просыпаемся рано утром от звонка расстроенного клиента, который жалуется на то, что не получил очень важные сообщения на свою электронную почту из разработанного нами приложения для сложного анализа данных. Мы выпрыгиваем из постели, чтобы решить проблему.
В конце концов, мы осознаем причину — это опечатка в адресе электронной почты, который мы определили в конфигурации:
app.properties.report-email-address = manager.analysisapp.com
«Разве я не проверил это? А ну понятно. Мне нужно было реализовать вспомогательный класс для чтения и проверки данных конфигурации, но мне было очень лениво в тот момент. Ааа, не важно, это же исправлено сейчас.»
Я жил по такому сценарию, не один раз.
Итак, вот мотивация этой статьи. Давайте попробуем найти практическое решение этой проблемы.
Валидация свойств при запуске
Привязка наших параметров конфигурации к объекту — это простой способ поддерживать их. Таким образом, мы можем извлечь выгоду из безопасности типов и найти ошибки раньше.
Spring Boot имеет аннотацию @ConfigurationProperties для реализации привязки свойств, определенных в файлах application.properties или application.yml.
Однако для их проверки нам нужно выполнить еще пару шагов.
Сначала давайте взглянем на наш файл application.properties:
app.properties.name = Analysis Application
app.properties.send-report-emails = true
app.properties.report-type = HTML
app.properties.report-interval-in-days = 7
app.properties.report-email-address = manager@analysisapp.com
Затем мы добавляем аннотацию @Validated в наш класс @ConfigurationProperties вместе с некоторыми аннотациями Bean Validation в полях:
@Validated
@ConfigurationProperties(prefix="app.properties")
class AppProperties {
@NotEmpty
private String name;
private Boolean sendReportEmails;
private ReportType reportType;
@Min(value = 7)
@Max(value = 30)
private Integer reportIntervalInDays;
@Email
private String reportEmailAddress;
// getters / setters
}
Чтобы Spring Boot загрузил наш класс AppProperties, мы аннотируем этот @Configuration класс с помощью @EnableConfigurationProperties:
Если мы сейчас запустим приложение Spring Boot с (недействительным) адресом электронной почты из приведенного выше примера, приложение не запустится:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException:
Failed to bind properties under 'app.properties' to
io.reflectoring.validation.AppProperties failed:
Property: app.properties.reportEmailAddress
Value: manager.analysisapp.com
Reason: must be a well-formed email address
Action:
Update your application's configuration
Зависимость API валидации бинов
Чтобы использовать аннотации для валидации bean-компонентов, мы должны включить зависимость javax.validation.validation-api в classpath нашего приложения.
Кроме того, мы также можем определить некоторые значения по умолчанию, инициализируя поля AppProperties:
@Validated
@ConfigurationProperties(prefix="app.properties")
class AppProperties {
// ...
private Boolean sendReportEmails = Boolean.FALSE;
private ReportType reportType = ReportType.HTML;
// ...
}
Даже если мы не определим значения для свойств send-report-emails и report-type в application.properties, теперь мы получим значения по умолчанию Boolean.FALSE и ReportType.HTML соответственно.
Валидация вложенных объектов конфигурации
Некоторые свойства имеет смысл объединить во вложенный объект.
Итак, давайте создадим ReportProperties для группировки свойств, связанных с нашим очень важным отчетом:
class ReportProperties {
private Boolean sendEmails = Boolean.FALSE;
private ReportType type = ReportType.HTML;
@Min(value = 7)
@Max(value = 30)
private Integer intervalInDays;
@Email
private String emailAddress;
// getters / setters
}
Затем мы выполним рефакторинг наших AppProperties, чтобы включить получившийся вложенный объект ReportProperties вместо отдельных свойств:
@Validated
@ConfigurationProperties(prefix="app.properties")
class AppProperties {
@NotEmpty
private String name;
@Valid
private ReportProperties report;
// getters / setters
}
Мы должны быть внимательными и не забыть поместить аннотацию Valid перед нашим полем вложенного отчета.
Это сообщает Spring, что необходимо проверить свойства вложенных объектов.
Наконец, мы должны изменить префикс свойств, связанных с отчетом, на report.* и в нашем файле application.properties:
...
app.properties.report.send-emails = true
app.properties.report.type = HTML
app.properties.report.interval-in-days = 7
app.properties.report.email-address = manager@analysisapp.com
Таким образом, свойства с префиксом app.properties будут по-прежнему привязаны к классу AppProperties, а свойства с префиксом app.properties.report будут привязаны к объекту ReportProperties в поле report.
Валидация с использованием Bean Factory Methods
Мы также можем запустить проверку, связав файл свойств с методом фабрики Bean с помощью аннотации @ConfigurationProperties:
@Configuration
class AppConfiguration {
// ...
@Bean
@Validated
@ConfigurationProperties(prefix = "app.third-party.properties")
public ThirdPartyComponentProperties thirdPartyComponentProperties() {
return new ThirdPartyComponentProperties();
}
// ...
}
Это особенно полезно, когда мы хотим привязать свойства к компонентам, определенным в сторонних библиотеках или поддерживаемым в отдельных jar файлах.
Использование собственного Spring валидатора
Несмотря на то, что Bean Validation обеспечивает декларативный подход к проверке наших объектов и возможность повторного использования, иногда нам нужно больше для настройки нашей логики проверки.
Для этого случая Spring имеет независимый механизм валидации — интерфейс Validator, который позволятет обеспечить динамическую валидацию ввода.
Давайте расширим нашу проверку, чтобы проверить, что report.email-address содержит определенный домен, например такой как @analysisapp.com:
class ReportEmailAddressValidator implements Validator {
private static final String EMAIL_DOMAIN = "@analysisapp.com";
public boolean supports(Class clazz) {
return ReportProperties.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"emailAddress", "field.required");
ReportProperties reportProperties = (ReportProperties) target;
if (!reportProperties.getEmailAddress().endsWith(EMAIL_DOMAIN)) {
errors.rejectValue("emailAddress", "field.domain.required",
new Object[]{EMAIL_DOMAIN},
"The email address must contain [" + EMAIL_DOMAIN + "] domain.");
}
}
}
Затем нам нужно зарегистрировать наш Spring валидатор с помощью специального метода с именем configurationPropertiesValidator():
@Configuration
class AppConfiguration {
// ...
@Bean
public static ReportEmailAddressValidator configurationPropertiesValidator() {
return new ReportEmailAddressValidator();
}
// ...
}
Только если имя получившегося bean-компонента Spring будет ConfigurationPropertiesValidator, Spring будет запускать этот валидатор для всех bean-компонентов с аннотацией @ConfigurationProperties.
Обратите внимание, что мы должны определить наш метод configurationPropertiesValidator() как статический. Это позволяет Spring создавать bean-компонент на самом раннем этапе, перед классами с аннотацией @Configuration, чтобы избежать каких-либо проблем при создании других bean-компонентов в зависимости от свойств конфигурации.
Валидатор Spring не является частью Bean Validation
Валидатор Spring не связан с Bean Validation и работает независимо после Bean Validation. Его основная цель — инкапсулировать логику проверки от любой инфраструктуры или контекста.
В случае, если нам нужно определить более одного валидатора для наших свойств конфигурации, мы не можем сделать это путем определения методов фабрики bean-компонентов, потому что мы можем определить только один bean-компонент с именем configurationPropertiesValidator.
Вместо определения метода фабрики бинов мы можем переместить нашу собственную реализацию Validator внутрь классов свойств конфигурации:
@Validated
@ConfigurationProperties(prefix = "app.properties")
class AppProperties implements Validator {
// properties ...
public boolean supports(Class clazz) {
return ReportProperties.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
// validation logic
}
}
Таким образом, мы можем выполнить разные реализации Validator для каждого класса @ConfigurationProperties.
Заключение
Если мы хотим обезопасносить себя от ошибок ввода, проверка нашей конфигурации — это хороший способ. Spring Boot облегчает работу способами, описанными в этой статье.
Все приведенные примеры кода, с которыми вы можете поиграть, и даже больше имеется на Github .