Skip to content
Chapter 44 · Spring Boot

@ConfigurationProperties: Typed Configuration ​

Chapter 9 injected single properties with @Value, and chapters 10 and 11 switched beans and whole environments with properties. That works for one or two values, but real configuration has many related settings. @ConfigurationProperties binds a whole group of them, sharing a common prefix, to one Java object.

The problem with @Value ​

@Value is fine for one property. As configuration grows, it gets messy:

java
@Component
public class DBConnection {

    @Value("${app.username}")
    private String username;

    @Value("${app.password}")
    private String password;

    @Value("${app.db.url}")
    private String dbUrl;
}
  • The same @Value("${app...}") is repeated on every field.
  • There is no way to validate the values: for example, that password must be present and between 10 and 25 characters.

@ConfigurationProperties ​

@ConfigurationProperties maps configuration into a Java object, so it is structured, reusable and can be validated.

properties
app.user.name=test_username
app.user.age=27
app.user.active=true
java
@Component
@ConfigurationProperties(prefix = "app.user")
public class UserConfigurations {

    private String name;
    private int age;
    private boolean active;

    // getters and setters
}

Every property starting with the prefix app.user is bound to the field with the same name. Spring creates the object (it is a @Component), then the configuration binder calls the setters with the values. Inject it anywhere and use it like a normal Java object:

java
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserConfigurations userConfigurations;

    @GetMapping("/")
    public void getUserConfig() {
        System.out.println("name: " + userConfigurations.getName());
        System.out.println("age: " + userConfigurations.getAge());
        System.out.println("is active: " + userConfigurations.isActive());
    }
}

Pick a prefix of your own, such as app.user, not just user. Java already defines a system property called user.name (your operating-system login), and system properties override application.properties. With prefix = "user", the name field would get your login name instead of test_username.

Nested objects ​

properties
app.user.address.city=myCityName
app.user.address.country=myCountryName
java
@Component
@ConfigurationProperties(prefix = "app.user")
public class UserConfigurations {

    private String name;
    private int age;
    private boolean active;
    private AddressConfig address;

    public static class AddressConfig {
        private String city;
        private String country;
        // getters and setters
    }

    // getters and setters
}

The nested class must be static. The binder creates AddressConfig itself through its no-argument constructor. A non-static inner class has no such constructor: the compiler gives it a hidden parameter for the outer object, AddressConfig(UserConfigurations outer). The binder cannot create it, and the application fails to start ("Configuration property name 'this$0' is not valid").

Lists ​

properties
# List<String>
app.user.roles[0]=ADMIN
app.user.roles[1]=EDITOR

# List<Course>
app.user.course[0].name=Java
app.user.course[0].enrolled=true
app.user.course[1].name=Springboot
app.user.course[1].enrolled=false
java
private List<String> roles;
private List<Course> course;

public static class Course {
    private String name;
    private boolean enrolled;
    // getters and setters
}

getRoles().get(0) is "ADMIN", and getCourse().get(1).getName() is "Springboot".

Maps ​

properties
# Map<String, String>
app.user.preferences.theme=dark
app.user.preferences.language=en
app.user.preferences.timezone=IST

# Map<String, AddressConfig>
app.user.locations.home.city=Jaipur
app.user.locations.home.country=India
app.user.locations.office.city=Delhi
app.user.locations.office.country=India
java
private Map<String, String> preferences;
private Map<String, AddressConfig> locations;

getPreferences().get("theme") is "dark", and getLocations().get("home").getCity() is "Jaipur". The part after the map's name (theme, home) becomes the key.

Validation ​

Spring Boot can validate the values right after binding, before the application finishes starting. Add the validation starter:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Then put @Validated on the class and constraints on the fields:

java
@Component
@ConfigurationProperties(prefix = "app.user")
@Validated
public class UserConfigurations {

    @NotBlank(message = "name must not be empty")
    private String name;

    @Min(value = 1, message = "age can not be 0")
    private int age;

    // getters and setters
}

With app.user.age=0, the application does not start ("Binding validation errors on app.user"), and the log shows which field failed and why.

AnnotationMeaningExample
@NotNullnot null@NotNull String name;
@NotBlanknot null and not empty (strings)@NotBlank String username;
@NotEmptynot null and not empty (lists, maps, ...)@NotEmpty List<String> courses;
@Min(value) / @Max(value)minimum / maximum number@Min(1) int age;
@Positive / @PositiveOrZero> 0 / ≥ 0@Positive int retryCount;
@Negative / @NegativeOrZero< 0 / ≤ 0@Negative int loss;
@Emailvalid email format@Email String email;
@Pattern(regexp)matches a regular expression@Pattern(regexp = "[A-Za-z]*") String userName;
@AssertTrue / @AssertFalsemust be true / false@AssertTrue boolean active;

Immutable configuration: constructor binding ​

The binding above uses setters, so the object can be changed later; it is not immutable. To make it immutable, remove the setters, make the fields final, and set them in a constructor:

java
@ConfigurationProperties(prefix = "app.user")
public class ImmutableUserConfiguration {

    private final String name;
    private final int age;
    private final boolean active;

    ImmutableUserConfiguration(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public boolean isActive() { return active; }
}
java
@SpringBootApplication
@ConfigurationPropertiesScan
public class ConfigurationPropertiesApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigurationPropertiesApplication.class, args);
    }
}

Notice there is no @Component on the class. With @Component, the IoC container would try to create the object, and it does not know what to pass to the constructor. Instead, @ConfigurationPropertiesScan finds the class and lets the configuration binder create it, calling the constructor with the property values. (Before, this was not an issue: the class had a default constructor, so the container could create an empty object and the binder then filled it through the setters.)

Validation works exactly the same way: add @Validated to the class and constraints such as @Min(1) to the fields.

Next: That completes the Spring Boot series. You can now build an application with REST controllers, beans and per-environment configuration, AOP, transactions and async work, interceptors and error handling, JPA persistence, several kinds of security, and production monitoring. To learn how to test that code, continue with the JUnit 5 and Mockito series.

Check yourself ​

  1. Give two problems with using many @Value annotations.
  2. What does prefix in @ConfigurationProperties(prefix = "app.user") do?
  3. Why is prefix = "user" a bad choice for a property called user.name?
  4. Why must a nested configuration class be static?
  5. How do you write a List<Course> in application.properties? A Map<String, String>?
  6. What do you need for validation, and what happens when a value is invalid?
  7. How do you make a configuration class immutable, and why does it then have no @Component?
Answers
  1. The prefix is repeated on every field, and the values cannot be validated.
  2. Binds every property that starts with app.user. to the field with the matching name.
  3. Java already has a user.name system property (the OS login), which overrides application.properties, so the field gets the login name.
  4. The binder creates it with a no-argument constructor; a non-static inner class has none (it needs the outer object), so the app fails to start.
  5. app.user.course[0].name=Java, app.user.course[0].enrolled=true, and so on. app.user.preferences.theme=dark.
  6. The spring-boot-starter-validation dependency, @Validated on the class and constraint annotations on the fields. The application fails to start and logs which field is invalid.
  7. final fields set in a constructor, no setters. The container could not call that constructor, so @ConfigurationPropertiesScan hands creation to the configuration binder, which passes the property values in.