2020-05-27 20:00:00 +0000

One of the important steps to keep software applications customizable is effective configuration management. Modern frameworks provide out-of-the-box features to externalize configuration parameters.

For some configuration parameters it makes sense to fail application startup if they’re invalid.

Spring Boot offers us a neat way of validating configuration parameters. We’re going to bind input values to @ConfigurationProperties and use Bean Validation to validate them.

Code Example

This article is accompanied by a working code example on GitHub.

Why Do We Need to Validate Configuration Parameters?

Doing proper validation of our configuration parameters can be critical sometimes.

Let’s think about a scenario:

We wake up early to a frustrated call. Our client complains about not having received their very important report emails from the fancy analysis application we developed. We jump out of bed to debug the issue.

Finally, we realize the cause. A typo in the e-mail address we defined in the configuration:

app.properties.report-email-address = manager.analysisapp.com

“Didn’t I validate it? Oh, I see. I had to implement a helper class to read and validate the configuration data and I was so lazy at that moment. Ahh, nevermind, it’s fixed right now.”

I lived that scenario, not just once.

So, that’s the motivation behind this article. Let’s keep going to see a practical solution to this problem.

Validating Properties at Startup

Binding our configuration parameters to an object is a clean way to maintain them. This way we can benefit from type-safety and find errors earlier.

Spring Boot has the @ConfigurationProperties annotation to do this binding for the properties defined in application.properties or application.yml files.

However, to validate them we need to follow a couple of more steps.

First, let’s take a look at our application.properties file:

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

Next, we add the @Validated annotation to our @ConfigurationProperties class along with some Bean Validation anotations on the fields:

@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
}

To have Spring Boot pick up our AppProperties class, we annotate our @Configuration class with @EnableConfigurationProperties:

@Configuration
@EnableConfigurationProperties(AppProperties.class)
class AppConfiguration {
  // ...
}

When we start the Spring Boot application now with the (invalid) email address from the example above, the application won’t start up:

***************************
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

Bean Validation API Dependency

In order to use the bean validation annotations, we must have the javax.validation.validation-api dependency in our classpath

Additionally, we can also define some default values by initializing the fields of AppProperties:

@Validated
@ConfigurationProperties(prefix="app.properties")
class AppProperties {
  // ...
  private Boolean sendReportEmails = Boolean.FALSE;

  private ReportType reportType = ReportType.HTML;
  // ...
}

Even if we don’t define any values for the properties send-report-emails and report-type in application.properties, we will now get the default values Boolean.FALSE and ReportType.HTML respectively.

Validate Nested Configuration Objects

For some properties, it makes sense to bundle them into a nested object.

So, let’s create ReportProperties to group the properties related to our very important report:

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
}

Next, we refactor our AppProperties to include our nested object ReportProperties instead of the single properties:

@Validated
@ConfigurationProperties(prefix="app.properties")
class AppProperties {

  @NotEmpty
  private String name;

  @Valid
  private ReportProperties report;

  // getters / setters
}

We should pay attention to put @Valid annotation on our nested report field.

This tells Spring to validate the properties of the nested objects.

Finally, we should change the prefix of the report-related properties to report.* in our application.properties file as well:

...
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

This way, properties with the prefix app.properties will still be bound to the AppProperties class, but properties with the prefix app.properties.report will be bound to the ReportProperties object in the report field.

Validate Using @Bean Factory Methods

We can also trigger validation by binding a properties file to a @Bean factory method with the @ConfigurationProperties annotation:

@Configuration
class AppConfiguration {
  // ...
  @Bean
  @Validated
  @ConfigurationProperties(prefix = "app.third-party.properties")
  public ThirdPartyComponentProperties thirdPartyComponentProperties() {
    return new ThirdPartyComponentProperties();
  }
  // ...
}

This is particularly useful when we want to bind properties to components defined in third-party libraries or maintained in separate jar files.

Using a Custom Spring Validator

Even though Bean Validation provides a declarative approach to validate our objects in a reusable way, sometimes we need more to customize our validation logic.

For this case, Spring has an independent Validator mechanism to allow dynamic input validation.

Let’s extend our validation to check that the report.email-address has a specific domain like @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.");
    }

  }
}

Then, we need to register our custom Spring validator with the special method name configurationPropertiesValidator():

@Configuration
class AppConfiguration {
  // ...
  @Bean
  public static ReportEmailAddressValidator configurationPropertiesValidator() {
    return new ReportEmailAddressValidator();
  }
  // ...
}

Only if the resulting Spring bean’s name is configurationPropertiesValidator will Spring run this validator against all @ConfigurationProperties beans.

Note that we must define our configurationPropertiesValidator() method as static. This allows Spring to create the bean in a very early stage, before @Configuration classes, to avoid any problems when creating other beans depending on the configuration properties.

Validator Is Not a Part of Bean Validation

Spring's Validator is not related to Bean Validation and works independently after the Bean Validation happens. Its main purpose is to encapsulate the validation logic from any infrastructure or context.

In case we need to define more than one Validator for our configuration properties, we cannot do it by defining bean factory methods, because we can only define one bean named configurationPropertiesValidator.

Instead of defining a bean factory method, we can move our custom Validator implementation to inside the configuration property classes:

@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
  }
}

By doing so, we can implement a different Validator implementation for each @ConfigurationProperties class.

Conclusion

If we want to be safe from input errors, validating our configuration is a good way to go. Spring Boot makes it easy with the ways described in this article.

All the code examples and even more you can play with is over on Github.

Get My Book for just $5!

Get Your Hands Dirty on Clean Architecture

Liked this article? Subscribe to my mailing list to get notified about new content and get my eBook "Get Your Hands Dirty on Clean Architecture" for just $5!

Grab My Book for Just $5!

Get Your Hands Dirty on Clean Architecture

Rated 4.8 stars on Amazon and 4.6 stars on Goodreads!

Subscribe to my Mailing List and get my book Get Your Hands Dirty on Clean Architecture for just $5!

Grab My Book for Just $5!

Get Your Hands Dirty on Clean Architecture

Rated 4.8 stars on Amazon and 4.6 stars on Goodreads!

Subscribe to my Mailing List and get my book Get Your Hands Dirty on Clean Architecture for just $5!

Follow Reflectoring