Spring - Externalized Configuration

Externalized configuration and easy access to properties defined in properties files.

https://docs.spring.io/spring-boot/docs/1.5.22.RELEASE/reference/html/boot-features-external-config.html

Externalise and mature your configuration management

Two main approaches:

  1. Use a configuration server, something like Spring Cloud Config
  2. Store all your configuration in environment variables (that could be provisioned based on git repository)

Either of these options (the second one more) requires us to dab a bit in the DevOps area, but this is to be expected in the world of microservices.

With Springboot

With Springboot, there is more support and it involves less configuration compared to standard Spring.

Springboot applies its typical convention over configuration approach to property files. Simply put an application.properties file in the src/main/resources directory, and it will be auto-detected. All the properties from this file will be injected into the application.

We can also configure a different file at runtime if we need to, using an environment property: spring.config.location=classpath:/another-location.properties

We can also specify wildcard locations for configuration files. spring.config.location=config/*/ Springboot will look for configuration files matching the config/*/ directory pattern outside of our jar file. This comes in handy when we have multiple sources of configuration properties.

With Spring

The new @PropertySource annotation

e.g.

@Configuration
@PropertySource("classpath:foo.properties")
public class PropertiesWithJavaConfig {
    //...
}

It supports placeholders

@PropertySource({
  "classpath:persistence-${envTarget:mysql}.properties"
})
...

Multiple files:

@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {
    //...
}

@PropertySources annotation

@PropertySources({
    @PropertySource("classpath:foo.properties"),
    @PropertySource("classpath:bar.properties")
})
public class PropertiesWithJavaConfig {
    //...
}

Environment-Specific Properties File

If we need to target different environments, there’s a built-in mechanism for that in Boot.

We can simply define an application-environment.properties file in the src/main/resources directory, and then set a Spring profile with the same environment name.

For example, if we define a “staging” environment, that means we’ll have to define a staging profile and then application-staging.properties.

This env file will be loaded and will take precedence over the default property file. Note that the default file will still be loaded, it’s just that when there is a property collision, the environment-specific property file takes precedence.

Test-Specific Properties File

Spring Boot handles this for us by looking in our src/test/resources directory during a test run. Again, default properties will still be injectable as normal but will be overridden by these if there is a collision.

@TestPropertySource Annotation

This allows us to set test properties for a specific test context, taking precedence over the default property sources:

@RunWith(SpringRunner.class)
@TestPropertySource("/foo.properties")
public class FilePropertyInjectionUnitTest {

    @Value("${foo}")
    private String foo;

    @Test
    public void whenFilePropertyProvided_thenProperlyInjected() {
        assertThat(foo).isEqualTo("bar");
    }
}

If we don’t want to use a file, we can specify names and values directly:

@RunWith(SpringRunner.class)
@TestPropertySource(properties = {"foo=bar"})
public class PropertyInjectionUnitTest {

    @Value("${foo}")
    private String foo;

    @Test
    public void whenPropertyProvided_thenProperlyInjected() {
        assertThat(foo).isEqualTo("bar");
    }
}

We can also achieve a similar effect using the properties argument of the @SpringBootTest annotation:

@RunWith(SpringRunner.class)
@SpringBootTest(
  properties = {"foo=bar"}, classes = SpringBootPropertiesTestApplication.class)
public class SpringBootPropertyInjectionIntegrationTest {

    @Value("${foo}")
    private String foo;

    @Test
    public void whenSpringBootPropertyProvided_thenProperlyInjected() {
        assertThat(foo).isEqualTo("bar");
    }
}

Hierarchical Properties

If we have properties that are grouped together, we can make use of the @ConfigurationProperties annotation, which will map these property hierarchies into Java objects graphs.

Sample entries in a property file

database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar

Reading them from a class

@ConfigurationProperties(prefix = "database")
public class Database {
    String url;
    String username;
    String password;

    // standard getters and setters
}

Spring Boot applies it’s convention over configuration approach again, automatically mapping between property names and their corresponding fields. All that we need to supply is the property prefix.

Similarities and differences between @ConfigurationProperties, @PropertySource and @Value

@PropertySource is to reference a properties file and load it into the Spring environment (where it may be used by @ConfigurationProperties or @Value). If the file doesn’t exist, it doesn’t complain. It may just lead to compilation or runtime errors when the key-value pairs from the property files are needed by the application (depending upon whether they are eagerly loaded or lazily loaded).

@ConfigurationProperties is used on a POJO bean to map properties to its fields or setters. Then you can use the bean to access the property values in your application logic. @ConfigurationProperties(prefix = "myserver.allvalues") injects POJO properties. It is not strict. It ignores the property if there is no key in properties file.

@ConfigurationProperties(prefix = "myserver.allvalues")
public class TestConfigurationProperties {
    private String value;
    private String valuenotexists; // This field doesn't exists in properties file
                                   // it doesn't throw error. It gets default value as null
}

application.properties file
----------------------
myserver:
  allvalues:
    value:  sampleValue

@Value is to inject a particular property value by its key into a variable (member field or constructor argument). @Value will throw exception if there no matching key in application.properties file. It strictly injects property value.

@ConfigurationProperties annotation in more detail

@ConfigurationProperties works best with hierarchical properties that all have the same prefix; therefore, we add a prefix of mail. The official documentation advises that we isolate configuration properties into separate POJOs. e.g.

@Configuration
@ConfigurationProperties(prefix = "mail")
public class ConfigProperties {

    private String hostName;
    private int port;
    private String from;

    // standard getters and setters
}

We use @Configuration so that Spring creates a Spring bean in the application context.

@ConfigurationProperties works best with hierarchical properties that all have the same prefix; therefore, we add a prefix of mail.

The Spring framework uses standard Java bean setters, so we must declare setters for each of the properties.

Note: If we don’t use @Configuration in the POJO, then we need to add @EnableConfigurationProperties(ConfigProperties.class) in the main Spring application class to bind the properties into the POJO:

@SpringBootApplication
@EnableConfigurationProperties(ConfigProperties.class)
public class EnableConfigurationDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(EnableConfigurationDemoApplication.class, args);
    }
}

Spring will automatically bind any property defined in our property file that has the prefix mail and the same name as one of the fields in the ConfigProperties class.

Spring uses some relaxed rules for binding properties. As a result, the following variations are all bound to the property hostName:

mail.hostName
mail.hostname
mail.host_name
mail.host-name
mail.HOST_NAME

Nested properties

We can have nested properties in Lists, Maps, and Classes.

Class for some nested properties

public class Credentials {
    private String authMethod;
    private String username;
    private String password;

    // standard getters and setters
}

Using this in another class

public class ConfigProperties {

    private String hostname;
    private int port;
    private String from;
    private List<String> defaultRecipients;
    private Map<String, String> additionalHeaders;
    private Credentials credentials;

    // standard getters and setters
}

Property file:

#Simple properties
mail.hostname=mailer@mail.com
mail.port=9000
mail.from=mailer@mail.com

#List properties
mail.defaultRecipients[0]=admin@mail.com
mail.defaultRecipients[1]=owner@mail.com

#Map Properties
mail.additionalHeaders.redelivery=true
mail.additionalHeaders.secure=true

#Object properties
mail.credentials.username=john
mail.credentials.password=password
mail.credentials.authMethod=SHA1

@ConfigurationProperties Validation

Spring Boot will attempt to validate @ConfigurationProperties classes whenever they are annotated with Spring’s @Validated annotation. You can use JSR-303 javax.validation constraint annotations directly on your configuration class. Simply ensure that a compliant JSR-303 implementation is on your classpath, then add constraint annotations to your fields:

@ConfigurationProperties(prefix="foo")
@Validated
public class FooProperties {

    @NotNull
    private InetAddress remoteAddress;

    // ... getters and setters

    @NotBlank
    private String hostName;

    @Length(max = 4, min = 1)
    private String authMethod;

    @Min(1025)
    @Max(65536)
    private int port;

    @Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
    private String from;
}

If any of these validations fail, then the main application would fail to start with an IllegalStateException.

The Hibernate Validation framework uses standard Java bean getters and setters, so it’s important that we declare getters and setters for each of the properties.

Immutable @ConfigurationProperties Binding

@ConfigurationProperties(prefix = "mail.credentials")
public class ImmutableCredentials {

    private final String authMethod;
    private final String username;
    private final String password;

    public ImmutableCredentials(String authMethod, String username, String password) {
        this.authMethod = authMethod;
        this.username = username;
        this.password = password;
    }

    public String getAuthMethod() {
        return authMethod;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }
}

When using @ConstructorBinding, we need to provide the constructor with all the parameters we’d like to bind.

Note that all the fields of ImmutableCredentials are final. Also, there are no setter methods.

It is important to emphasize that to use the constructor binding, we need to explicitly enable our configuration class either with @EnableConfigurationProperties or with @ConfigurationPropertiesScan.

Java 16 records

Java 16 introduced the record types as part of JEP 395. Records are classes that act as transparent carriers for immutable data. This makes them perfect candidates for configuration holders and DTOs. As a matter of fact, we can define Java records as configuration properties in Spring Boot. For instance, the previous example can be rewritten as:

@ConstructorBinding
@ConfigurationProperties(prefix = "mail.credentials")
public record ImmutableCredentials(String authMethod, String username, String password) {

}

It is more concise compared to all those noisy getters and setters.

Moreover, as of Spring Boot 2.6, for single-constructor records, we can drop the @ConstructorBinding annotation. If our record has multiple constructors, however, @ConstructorBinding should still be used to identify the constructor to use for property binding.

Tags

Spring Data