-
Notifications
You must be signed in to change notification settings - Fork 40.8k
Spring Boot Configuration Binding
The purpose of this page is to describe in detail how configuration keys are bound to actual objects. It is primarily targeted to IDE developers though anyone interested in understanding how binding works will find valuable resources in this document.
Note
|
Expected IDE features are described in a separate document. |
Regardless of the external format, the whole Environment
boils down to a set of keys that is conceptually hierarchically structured. Let’s take an example:
server:
port: 7070
tomcat:
max-threads: 20
This file contributes two keys: server.port
and server.tomcat.max-threads
. The latter belongs to the server.tomcat
"group". Configuration items sharing a same concept are regrouped within the same prefix. Larger concepts have sub-concepts (for instance, the Apache Tomcat specific configuration is defined within the server.tomcat
prefix).
The most basic way to bind to a portion of the Environment
is to define a POJO and inject a set of keys to it. As we will see later, Spring Boot offers several useful features in that area. But let’s first focus on the various binding use cases that are supported.
In the remainder of this section we will cover the following:
-
Simple property binding
-
Collection-based binding
-
Array-based binding
-
Map-based binding
-
Nested property
Note
|
while field-based binding is theoretically possible, Spring Boot only uses regular getter/setter access. |
Let’s take an example:
public class Foo {
private String id = "";
private int port;
public String getId() { ... }
void setId(String id) { ... }
public int getPort() { ... }
public void setPort(int port) { ... }
}
This object exposes one singular value property (port
). id
is not exposed as the setter for it is not public. A singular value is defined as the last portion of the key.
Note
|
the rule of thumb is that a valid public getter/setter pair should exist for the property to be exposed. In theory, the binder has enough information with only the setter but a singular property is exposed in the meta-data if both the getter and the setter are present. |
When a property exposes a collection type, it is possible to contribute many values to it either by specifying the values in a comma-separated fashion or by using the bracket notation to indicate the index
public class Foo {
private final List<String> items = new ArrayList<>();
private Set<Integer> counters;
public List<String> getItems() { .... }
public Set<Integer> getCounters() { ... }
public void setCounters(Set<Integer> counters) { ... }
}
The following keys will do what is expected (assuming foo is the context to which this object is bound to)
foo.items[1]=twoBis
foo.items[2]=four
foo.counters=1,2,3
Note
|
The rule of thumb is that a getter should be present to access the collection. If the collection needs to be initialized, a setter has to be provided. |
Note
|
If Collection properties are specified in multiple property sources, only the elements from the property source with the highest precedence will be used.
Properties from different property sources will NOT be combined when binding to a Collection . For example, if a .properties file has
|
foo[0] = 1
foo[1] = 2
and the Environment has
foo=3,4
the bound collection will only contain the elements 3
and 4
.
As for collections, it is possible to contribute arbitrary values to a map by specifying the key using the bracket notation or using a navigation dot. A setter is only necessary if the map needs to be initialized
public class Foo {
private final Map<String,Integer> items = new HashMap<>();
public Map<String,Integer> getItems() { .... }
private final Map<String,Map<String, Integer>> nested = new HashMap<>();
public Map<String,Map<String, Integer>> getNested() { .... }
}
Here are some valid examples
foo.items.one=1
foo.items[two]=2
Note
|
If the key contains a dot, you must use the bracket notation if it is a nested Map. For example, for the above scenario, a valid example that will use the key bar.baz is
|
foo.items.bar.baz=1
foo.nested[bar.baz].bling=2
For YAML files, the brackets need to be surrounded by quotes for the key to be parsed properly.
foo:
nested:
"[bar.baz]":
bling: 2
There are several levels of nesting. So far we’ve used simple values but more complex objects can be processed. Let’s imagine the following object
public class Bar {
private String id;
private Integer counter;
private boolean active;
// getter and setter
}
A simple nesting would work as follows:
public class Foo {
private final Bar bar = new Bar();
public Bar getBar() { ... }
}
which would allow you to write the following
foo.bar.id=myId
foo.bar.counter=0
foo.bar.active=true
Tip
|
If you want Bar to be created on demand you can leave it null and add a setter instead.
|
Nesting works also for Collections and Arrays
public class Foo {
private List<Bar> bars = new ArrayList<>();
public List<Bar> getBars() { .... }
public void setBars(List<Bar> bars) { ... }
}
which permits the nesting on an index element
foo.bars[0].id=one
foo.bars[2].counter=3
Tip
|
it is not possible to give a comma separated-view of such object unless a Converter<String,Bar> is registered in the ConversionService of the binder.
|
As you may imagine, this works for maps as well:
public class Foo {
private Map<String, Bar> bars = new HashMap<>();
public Map<String, Bar> getBars() { .... }
}
foo.bars.one.counter=1
foo.bars.one.active=false
foo.bars[two].id=IdOfBarWithKeyTwo
Finally, nesting can use as many level as you want. The .
is used as the character to navigate between those relationships. Let’s consider this rather complex key:
foo.items.myKey.customer.address.street=Acme street
This sets the street of the customer that is referenced with the key myKey
in the items
map, something like (assuming foo
is the reference of our root object):
foo.get("myKey").getCustomer().getAddress().setStreet("Acme street")
Tip
|
If an intermediate relationship is null , a new instance will be created using the default constructor and the related setter will be called with it.
|
The table below describes the rules regarding getter/setter presence
Binding type |
Getter |
Setter |
Example |
Simple |
Not technically necessary |
Required |
|
Collection |
Required |
Not required if collection is initialized |
|
List |
Required |
Not required if the collection is initialized |
|
Array |
Required |
Required |
|
Map |
Required |
Not required if the map is initialized |
|
Navigation |
Required |
Not required if the instance is initialized |
|
Spring Boot provides a @ConfigurationProperties
annotation that can be placed on any object to declare its root prefix. It then uses that with an augmented binder to automatically bind properties from the Environment
for matching prefixes. It also exposes the object as a Spring Bean automatically.
@ConfigurationProperties("foo")
public class FooProperties {
private String id = "";
private final Bar bar = new Bar;
public String getId() { ... }
pulic void setId(String id) { ... }
public Bar getBar() { ... }
static class Bar {
private String name;
private boolean active;
public String getName() { ... }
public void setName(String name) { ... }
public boolean isActive() { ... }
public void setActive(boolean active) { ... }
}
}
If the processing of that object is registered via, for instance, @EnableConfigurationProperties(FooProperties.class)
, a bean of type FooProperties
will be automatically registered in the context and the following keys will be bound as you expect:
foo.id=myId
foo.bar.name=barName
foo.bar.active=true
Tip
|
All the other binding types that we have seen above would obviously work here as well. |
You could also apply the same mechanism on a @Bean
declaration. In that case, binding will be applied once the object has been built. This is typically useful to provide a friendly configuration for a third party object.
Let’s consider that FooProperties
is some 3rd party Foo
class that we use within our Spring Boot application:
@Configuration
public class MyConfig {
@Bean
@ConfigurationProperties("foo")
public Foo foo() {
Foo foo = new Foo();
foo.setId("some id");
return foo;
}
}
This will expose the exact same set of properties (since we used the same prefix as the example above). Note also that if the configuration define a foo.id=anotherId
entry, the id of that bean will be ultimately anotherId
as property binding is applied once the object has been built.
Because keys can be defined in various formats and certain sources have some limitations, Spring Boot uses a relaxed binder. Consider the following
@ConfigurationProperties("foo")
public class FooProperties {
private String id;
private String firstName;
private String lastName;
// getters and setters
}
Spring Boot uses a canonical format that is lower case and use hyphen to separate words. But other formats are supported as defined in the table below:
Name |
Example |
uniform |
|
camel case |
|
underscore |
|
upper case |
|
Note
|
There are actually many more variants that are supported |
The main reason behind relaxed binding is to offer flexibility, particularly when the property source does not support certain characters: OS environment variables must be upper case or could not contain a dot on certain OSes. Details about relaxed binding as per property source can be found here.
This section describes how meta-data is concretely discovered and some of the limitations around it.
Note
|
If you haven’t done so, you should first read the configuration meta-data section of the developer guide as it defines the base concepts used in this section. |
The purpose of the meta-data is to provide a static model of the configuration keys so that tools can benefit from it and offer content assistance to the users. Meta-data is generated automatically during the compilation when the spring-boot-configuration-processor
is available.
The properties are a finite set of what is exposed by the current module. While for instance foo.items[myKey].address.street
is a valid configuration key, the meta-data will only expose the foo.items
key with a type that offers the necessary to discover the rest, that is:
-
The type is a
Map
-
The key is a
java.lang.String
-
The value is
com.acme.Person
(referenced in the rest of this document as a Simple POJO) that could be further investigated for additional accessors (aPerson
has anAddress
and anAddress
has astreet
)
Cases such as Collection or Map-based bindings are easy to discover because the type of the property says so. It is harder to figure out whether any other type of property is a single value or represent a concept we should navigate to.
To take back the previous example, we could have wrote foo.bar=???
which would have failed obviously since there is not setter and the intention of that object is to offer an object exposing additional properties.
Spring Boot uses the following rule:
-
If the type of the property is an inner class of the current object, it is considered to be a nested property (we use this pattern a lot and found it nice that it could be auto-discovered. Check
ServerProperties
for instance) -
If the property is flagged with
@NestedConfigurationProperty
, Spring Boot consider it to be a nested property (SeeSsl
for an example) -
In all other cases, the property is supposed to be a singular value.
Let’s consider that Bar
is now defined in a different area (in the same package or in a different package but not as an inner class), we could rewrite our class to ensure that meta-data are discovered in the same way:
import com.acme.Bar;
@ConfigurationProperties("foo")
public class FooProperties {
private String id = "";
@NestedConfigurationProperty
private final Bar bar = new Bar;
public String getId() { ... }
public void setId(String id) { ... }
public Bar getBar() { ... }
}
Default values can only be discovered by using low-level utility of the compiler API. Spring Boot has support for the Oracle JDK but does not support yet the Eclipse compiler (APT).
The default value should be set preferably in the field declaration itself. If a public static
constant is set in the class itself, the processor will discover it as well.
For instance
@ConfigurationProperties("my")
public class MyProperties {
public static final int DEFAULT_INDEX = 0;
private String name = "myName";
private int index = DEFAULT_INDEX;
// getter and setter
}
Documentation is only extracted from field Javadoc. The main reason behind this decision is that the description of a configuration key may greatly differ from the one you usually write for a setter. Besides, the processor does not clean any of the Javadoc tag that is present so having a separate location for the documentation is necessary.
Note
|
If the property does not have a related field or if the field does not match the convention, the documentation is not available. |
The Javadoc is only obviously accessible if the source code is available. No description is therefore available for:
-
Keys defined in the parent of a
@ConfigurationProperties
class if said base class is not in the current compilation unit (i.e. module). -
@Bean
exposing a third party class
One way to mitigate that problem is to upgrade the IDE support so that it can fetch that documentation live if it exists rather than only relying on the meta-data.