Configuration Properties

jim stafford

Introduction

Goals

The student will learn to:

  • map a Java @ConfigurationProperties class to properties

  • define validation rules for property values

  • leverage tooling to generate boilerplate code for JavaBean classes

  • solve more complex property mapping scenarios

  • solve injection mapping or ambiguity

Objectives

At the conclusion of this lecture and related exercises, the student will be able to:

  1. map a Java @ConfigurationProperties class to a group of properties

    • generate property metadata — used by IDEs for property editors

  2. create read-only @ConfigurationProperties class using @ConstructorBinding

  3. define Jakarta EE Java validation rule for property and have validated at runtime

  4. generate boilerplate JavaBean methods using Lombok library

  5. use relaxed binding to map between JavaBean and property syntax

  6. map nested properties to a @ConfigurationProperties class

  7. map array properties to a @ConfigurationProperties class

  8. reuse @ConfigurationProperties class to map multiple property trees

  9. use @Qualifier annotation and other techniques to map or disambiguate an injection

Mapping properties to @ConfigurationProperties class

Starting off simple …​


# application.properties
app.config.car.name=Suburban
  • define a property (app.config.car.name) to hold the name of a car

Mapped Java Class

Create a JavaBean class to hold the assigned propert(ies)

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("app.config.car") (3)
public class CarProperties { (1)
    private String name;

    //default ctor (2)

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name; (2)
    }

    @Override
    public String toString() {
        return "CarProperties{name='" + name + "\'}";
    }
}
1class is a standard Java bean with one property
2class designed for us to use its default constructor and a setter() to assign value(s)
3class annotated with @ConfigurationProperties to identify that is mapped to properties and the property prefix that pertains to this class

Injection Point

Define an injection point and use within a component class


...
@Component
public class AppCommand implements CommandLineRunner {
    @Autowired
    private CarProperties carProperties; (1)

    public void run(String... args) throws Exception {
        System.out.println("carProperties=" + carProperties); (2)
...
1Our @ConfigurationProperties instance is being injected into a @Component class using FIELD injection
2Simple print statement of bean’s toString() result

Initial Error

Spring was not able to locate what it needed to complete the injection


***************************
APPLICATION FAILED TO START
***************************

Description:

Field carProperties in info.ejava.examples.app.config.configproperties.AppCommand required a bean
  of type 'info.ejava.examples.app.config.configproperties.properties.CarProperties' that could
  not be found.

The injection point has the following annotations:
        - @org.springframework.beans.factory.annotation.Autowired(required=true)

Action:

Consider defining a bean of type
  'info.ejava.examples.app.config.configproperties.properties.CarProperties'
  in your configuration. (1)
1Error message indicates that Spring is not seeing our @ConfigurationProperties class

Registering the @ConfigurationProperties class

  • Current problem similar to issue when first implementing @Configuration and @Component classes

    • the bean is not being scanned

  • Even though we have our @ConfigurationProperties class is in the same basic classpath as the @Configuration and @Component classes

    src
    `-- main
        |-- java
        |   `-- info
        |       `-- ejava
        |           `-- examples
        |               `-- app
        |                   `-- config
        |                       `-- configproperties
        |                           |-- AppCommand.java
        |                           |-- ConfigurationPropertiesApp.java
        |                           `-- properties
        |                               `-- CarProperties.java
        `-- resources
            `-- application.properties
  • We need a little more to have it processed by Spring

    • There are several ways to do that:

way 1 - Register Class as a @Component

package info.ejava.examples.app.config.configproperties.properties;
...
@Component
@ConfigurationProperties("app.config.car") (1)
public class CarProperties {
1causes Spring to process the bean and annotation as part of component classpath scanning


  • benefits: simple

  • drawbacks: harder to override when configuration class and component class are in the same Java class package tree

way 2 - Explicitly Register Class

import info.ejava.examples.app.config.configproperties.properties.CarProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
...
@SpringBootApplication
@EnableConfigurationProperties(CarProperties.class) (1)
public class ConfigurationPropertiesApp {
1targets a specific @ConfigurationProperties class to process


  • benefits: @Configuration class has explicit control over which configuration properties classes to activate

  • drawbacks: application could be coupled with the details if where configurations come from

way 3 - Enable Packaging Scanning

@SpringBootApplication
@ConfigurationPropertiesScan (1)
public class ConfigurationPropertiesApp {
1allows a generalized scan to be defined that is separate for configurations


  • benefits: easy to add more configuration classes without changing application

  • drawbacks: generalized scan may accidentally pick up an unwanted configuration

way 4 - Use @Bean Factory

@SpringBootApplication
public class ConfigurationPropertiesApp {
...
    @Bean
    @ConfigurationProperties("app.config.car") (1)
    public CarProperties carProperties() {
        return new CarProperties();
    }
1gives more control over the runtime mapping of the bean to the @Configuration class


  • benefits: decouples the @ConfigurationProperties class from the specific property prefix used to populate it. This allows for reuse of the same @ConfigurationProperties class for multiple prefixes

  • drawbacks: implementation spread out between the @ConfigurationProperties and @Configuration classes. It also prohibits the use of read-only instances since the returned object is not yet populated

Result

  • CarProperties @ConfigurationProperties bean instantiated and initialized with matching properties

# application.properties
app.config.car.name=Suburban
  • Bean injected into @Component

...
@Component
public class AppCommand implements CommandLineRunner {
    @Autowired
    private CarProperties carProperties;

    public void run(String... args) throws Exception {
        System.out.println("carProperties" + carProperties);
...
  • Bean state printed by component

$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
carProperties=CarProperties{name='Suburban'}

Metadata

IDEs have support for linking Java properties to their @ConfigurationProperty class information.

configprops ide help
Figure 1. IDE Configuration Property Support

This allows the property editor to know:

  • there is a property app.config.carname

  • any provided Javadoc


Spring Configuration Metadata and IDE support is very helpful when faced with configuring dozens of components with hundreds of properties (or more!)

Spring Configuration Metadata

  • IDEs rely on a JSON-formatted metadata file for that information

    • META-INF/spring-configuration-metadata.json

META-INF/spring-configuration-metadata.json Snippet
...
"properties": [
    {
      "name": "app.config.car.name",
      "type": "java.lang.String",
      "description": "Name of car with no set maximum size",
      "sourceType": "info.ejava.examples.app.config.configproperties.properties.CarProperties"
    }
...


We can author it manually. However, there are ways to automate this.

Spring Configuration Processor

  • spring-boot-configuration-processor dependency will generate JSON metadata file

  • processed during javac compilation

<!-- pom.xml dependencies -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId> (1)
    <optional>true</optional> (2)
</dependency>
1dependency will generate additional artifacts during compilation
2dependency not required at runtime and can be eliminated from dependents


Dependencies labelled optional=true or scope=provided are not included in the Spring Boot executable JAR or transitive dependencies in downstream deployments without further configuration by downstream dependents.

Javadoc Supported

  • metadata also supports documentation extracted from Javadoc comments

@ConfigurationProperties("app.config.car")
public class CarProperties {
    /**
     * Name of car with no set maximum size (1)
     */
    private String name;
1Javadoc information is extracted from the class and placed in the property metadata

Rebuild Module

  • Rebuild module with Maven to generate JSON metadata file

Metadata File Created During Compilation
$ mvn clean compile
Produced Metadata File in target/classes Tree
target/classes/META-INF/
`-- spring-configuration-metadata.json
Produced Metadata File Contents
{
  "groups": [
    {
      "name": "app.config.car",
      "type": "info.ejava.examples.app.config.configproperties.properties.CarProperties",
      "sourceType": "info.ejava.examples.app.config.configproperties.properties.CarProperties"
    }
  ],
  "properties": [
    {
      "name": "app.config.car.name",
      "type": "java.lang.String",
      "description": "Name of car with no set maximum size",
      "sourceType": "info.ejava.examples.app.config.configproperties.properties.CarProperties"
    }
  ],
  "hints": []
}

IDE Property Help

If your IDE supports Spring Boot and property metadata, the property editor will offer help filling out properties.

configprops ide help
IntelliJ free Community Edition does not support this feature. The following link provides a comparison with the for-cost Ultimate Edition.

Constructor Binding

Slight improvement — make the JavaBean read-only to match read-only contract with properties

...
import org.springframework.boot.context.properties.ConstructorBinding;

@ConfigurationProperties("app.config.boat")
public class BoatProperties {
    private final String name; (3)

    @ConstructorBinding (2)
    public BoatProperties(String name) {
        this.name = name;
    }
    //no setter method(s) (1)
    public String getName() {
        return name;
    }
    @Override
    public String toString() {
        return "BoatProperties{name='" + name + "\'}";
    }
}
1remove setter methods to better advertise the read-only contract of the bean
2add custom constructor and annotate the class or constructor with @ConstructorBinding
3make attributes final to better enforce the read-only nature of the bean
@ConstructorBinding annotation required on the constructor method when more than one constructor is supplied.

Property Names Bound to Constructor Parameter Names

  • no longer have setter method name(s) to map properties

  • constructor argument name(s) used instead

# application.properties
app.config.boat.name=Maxum
Result of Parameter Name Matching Property Name
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
boatProperties=BoatProperties{name='Maxum'}

Constructor Parameter Name Mismatch

# application.properties
app.config.boat.name=Maxum
@ConfigurationProperties("app.config.boat")
public class BoatProperties {
    private final String name;

    @ConstructorBinding
    public BoatProperties(String nameX) { (1)
        this.name = nameX;
    }
1constructor argument name has been changed to not match the property name from application.properties


Result of Parameter Name not Matching Property Name
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
boatProperties=BoatProperties{name='null'}


We will discuss relaxed binding soon and see that some syntactical differences between the property name and JavaBean property name are accounted for during @ConfigurationProperties binding. However, this was a clear case of a name mis-match that will not be mapped.

Validation

  • previous error would have occurred with constructor or setter-based binding

  • can help detect invalid property values using Java validation through the JavaEE/ Jakarta EE standard API

    • allows us to express constraints on JavaBeans

    • helps further modularize objects within our application

  • add compile dependency on spring-boot-starter-validation)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • This will bring in three (3) dependencies

    • jakarta.validation-api - validation API, required to compile the module

    • hibernate-validator - validation implementation, required at runtime to perform validation

    • tomcat-embed-el - required when expressing validations using regular expressions with @Pattern annotation

Validation Annotations

  • trigger Spring to validate our JavaBean when instantiated by the container by adding the Spring @Validated annotation to the class

  • define Java attribute with Jakarta EE @NotNull constraint to report error if property null

...
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;

@ConfigurationProperties("app.config.boat")
@Validated (1)
public class BoatProperties {
    @NotNull (2)
    private final String name;

    @ConstructorBinding
    public BoatProperties(String nameX) {
        this.name = nameX;
    }
...
1The Spring @Validated annotation tells Spring to validate instances of this class
2The Jakarta EE @NotNull annotation tells the validator this field is not allowed to be null
You can locate other validation constraints in the Validation API and also extend the API to provide more customized validations using the Validation Spec, Hibernate Validator Documentation, or various web searches.

Validation Error

  • error produced is caught by Spring Boot

    • turned into a helpful description of the problem

    • description clearly states there is a problem with one of the properties specified

$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
***************************
APPLICATION FAILED TO START
***************************
Description:

Binding to target org.springframework.boot.context.properties.bind.BindException:
   Failed to bind properties under 'app.config.boat' to
   info.ejava.examples.app.config.configproperties.properties.BoatProperties failed:

    Property: app.config.boat.name
    Value: null
    Reason: must not be null

Action:

Update your application's configuration
Notice how the error message output by Spring Boot automatically knew what a validation error was and that the invalid property mapped to a specific property name. That is an example of Spring Boot’s FailureAnalyzer framework in action — which aims to make meaningful messages out of what would otherwise be a clunky stack trace.

Boilerplate JavaBean Methods

  • Notice all the boilerplate constructs in the class

...
@ConfigurationProperties("app.config.boat")
@Validated
public class BoatProperties {
    @NotNull
    private final String name;

    @ConstructorBinding
    public BoatProperties(String name) { //boilerplate (1)
        this.name = name;
    }

    public String getName() {  //boilerplate (1)
        return name;
    }

    @Override
    public String toString() { //boilerplate (1)
        return "BoatProperties{name='" + name + "\'}";
    }
}
1Many boilerplate methods in source code — likely generated by IDE


  • Will get worse code gets more complex and more attributes added to classes

Generating Boilerplate Methods with Lombok

  • Can be automatically provided for us at compilation using Lombok library.

    • Lombok not unique to Spring Boot, but adopted into Spring Boot’s overall opinionated approach to developing software

    • Simple Lombok @Data annotation intelligently inspects JavaBean class and supplies boilerplate constructs commonly supplied by IDE

...
import lombok.Data;

@ConfigurationProperties("app.config.company")
@ConstructorBinding
@Data (1)
@Validated
public class CompanyProperties {
    @NotNull
    private final String name;
    //constructor (1)
    //getter (1)
    //toString (1)
    //hashCode and equals (1)
}
1Lombok @Data annotation generated constructor, getter(/setter), toString, hashCode, and equals

Visible Generated Constructs

  • Additional methods can be identified in a class structure view of an IDE

configprops lombok methods

You may need to locate a compiler option within your IDE properties to make the code generation within your IDE.

Visible Generated Constructs using javap

  • Or view using the Java disassembler (javap) command on the compiled .class files

$ javap -cp target/classes info.ejava.examples.app.config.configproperties.properties.CompanyProperties
Compiled from "CompanyProperties.java"
public class info.ejava.examples.app.config.configproperties.properties.CompanyProperties {
  public info.ejava.examples.app.config.configproperties.properties.CompanyProperties(java.lang.String);
  public java.lang.String getName();
  public boolean equals(java.lang.Object);
  protected boolean canEqual(java.lang.Object);
  public int hashCode();
  public java.lang.String toString();
}

Lombok Build Dependency

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
  • Permits us to declare the dependency as scope=provided

    • eliminates it from transitive dependencies

    • no extra bloat

Maven Dependency
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
</dependency>

Example Output

...
@Autowired
private BoatProperties boatProperties;
@Autowired
private CompanyProperties companyProperties;

public void run(String... args) throws Exception {
    System.out.println("boatProperties=" + boatProperties); (1)
    System.out.println("====");
    System.out.println("companyProperties=" + companyProperties); (2)
...
1BoatProperties JavaBean methods were provided by hand
2CompanyProperties JavaBean methods were provided by Lombok
# application.properties
app.config.boat.name=Maxum
app.config.company.name=Acme
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
boatProperties=BoatProperties{name='Maxum'}
====
companyProperties=CompanyProperties(name=Acme)
  • produces near identical results from caller’s perspective

  • only difference here is specific text used in the returned string

@ConstructorBinding Issue with Lombok

There is a Spring @ConstructorBinding issue that prevents property metadata from being automatically generated. This is due to a Lombok issue where usable argument names are not provided in the generated constructor. The only workaround at this time if you want metadata generated for @ConstructorBinding with Lombok is to provide a custom constructor supplying the valid names. The IDE is very good at generating these for you until that issue is corrected.

@ConfigurationProperties("app.config.company")
@ConstructorBinding
@Data
@Validated
public class CompanyProperties {
    @NotNull
    private final String name;

    //https://github.com/spring-projects/spring-boot/issues/18730
    //https://github.com/rzwitserloot/lombok/issues/2275
    public CompanyProperties(String name) {
        this.name = name;
    }
}
Lombok ConstructorBinding Issue Listed as Closed
Since providing the warning above, the version of Lombok has advanced in class (1.18.20), issue closed, and may have been resolved. Confirmation needed.

Lombok Summary

  • Adding Lombok to our development approach for JavaBeans is almost a 100% win situation

    • 80-90% of the JavaBean class is written for us

    • we can override the defaults at any time with further annotations or custom methods

    • gives us an escape route in the event something needs to be customized

Relaxed Binding

  • Key difference between Spring’s @Value injection and @ConfigurationProperties is support for relaxed binding by the later

  • With relaxed binding, property definitions do not have to be an exact match

    • JavaBean properties are commonly defined with camelCase

    • Property definitions can come in a number of different case formats. Examples:

      • camelCase

      • UpperCamelCase

      • kebab-case

      • snake_case

      • UPPERCASE

Relaxed Binding Example JavaBean

JavaBean Attributes using camelCase
@ConfigurationProperties("app.config.business")
@ConstructorBinding
@Data
@Validated
public class BusinessProperties {
    @NotNull
    private final String name;
    @NotNull
    private final String streetAddress;
    @NotNull
    private final String city;
    @NotNull
    private final String state;
    @NotNull
    private final String zipCode;
    private final String notes;
}

Relaxed Binding Example Properties

  • Properties supplied use a variety of cases

Example Properties to Demonstrate Relaxed Binding
# application.properties
app.config.business.name=Acme
app.config.business.street-address=100 Suburban Dr
app.config.business.CITY=Newark
app.config.business.State=DE
app.config.business.zip_code=19711
app.config.business.notess=This is a property name typo
  • kebab-case street-address matched Java camelCase streetAddress

  • UPPERCASE CITY matched Java camelCase city

  • UpperCamelCase State matched Java camelCase state

  • snake_case zip_code matched Java camelCase zipCode

  • typo notess does not match Java camelCase notes

Relaxed Binding Example Output

  • Extra character typo in notess prevented a mapping to the notes attribute

    • IDE/metadata can help avoid the error

    • Validation can identify when the error exists

$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
businessProperties=BusinessProperties(name=Acme, streetAddress=100 Suburban Dr,
city=Newark, state=DE, zipCode=19711, notes=null)

Nested Properties

  • Previous examples used a flat property model

  • This example maps nested properties


Nested Properties Example
                (1)
app.config.corp.name=Acme
                     (2)
app.config.corp.address.street=100 Suburban Dr
app.config.corp.address.city=Newark
app.config.corp.address.state=DE
app.config.corp.address.zip=19711
1name is part of a flat property model below corp
2address is a container of nested properties

Nested Properties JavaBean Mapping

  • Supply JavaBean to hold their nested properties

    • Reference from the host/outer-class

Nested Property Mapping
...
@Data
@ConstructorBinding
public class AddressProperties {
    private final String street;
    @NotNull
    private final String city;
    @NotNull
    private final String state;
    @NotNull
    private final String zip;
}
In this specific case we are using a read-only JavaBean and need to supply the @ConstructorBinding annotation.

Nested Properties Host JavaBean Mapping

  • Host class (CorporateProperties) declares

    • base property prefix

    • reference (address) to the nested class

Host Property Mapping
...
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties("app.config.corp")
@ConstructorBinding
@Data
@Validated
public class CorporationProperties {
    @NotNull
    private final String name;
    @NestedConfigurationProperty //needed for metadata
    @NotNull
    //@Valid
    private final AddressProperties address;
The @NestedConfigurationProperty is only supplied to generate correct metadata — otherwise only a single address property will be identified to exist within the generated metadata.
The validation initiated by the @Validated annotation seems to automatically propagate into the nested AddressProperties class without the need to add @Valid annotation.

Nested Properties Output

  • Properties are populated within the host and nested bean

    • accessible to components within the application

Nested Property Example Output
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
corporationProperties=CorporationProperties(name=Acme,
   address=AddressProperties(street=null, city=Newark, state=DE, zip=19711))

Property Arrays

  • Property mapping can get complex

    • Not demonstrating them all

    • Please consult documentation available on the Internet for a complete view

  • Will demonstrate an initial collection mapping to arrays to go a level deeper

Property Array JavaBean Mapping
...
@ConfigurationProperties("app.config.route")
@ConstructorBinding
@Data
@Validated
public class RouteProperties {
    @NotNull
    private String name;
    @NestedConfigurationProperty
    @NotNull
    @Size(min = 1)
    private List<AddressProperties> stops; (1)
 ...
1RouteProperties hosts list of stops as AddressProperties

Property Arrays Definition

The above can be mapped using a properties format.

# application.properties
app.config.route.name: Superbowl
app.config.route.stops[0].street: 1101 Russell St
app.config.route.stops[0].city: Baltimore
app.config.route.stops[0].state: MD
app.config.route.stops[0].zip: 21230
app.config.route.stops[1].street: 347 Don Shula Drive
app.config.route.stops[1].city: Miami
app.config.route.stops[1].state: FLA
app.config.route.stops[1].zip: 33056

However, it may be easier to map using YAML.

# application.yml
app:
  config:
    route:
      name: Superbowl
      stops:
        - street: 1101 Russell St
          city: Baltimore
          state: MD
          zip: 21230
        - street: 347 Don Shula Drive
          city: Miami
          state: FLA
          zip: 33056

Property Arrays Output

  • Properties are populated within host and nested JavaBeans

  • Nested JavaBeans are added to a collection within the host

Property Arrays Example Output
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
routeProperties=RouteProperties(name=Superbowl, stops=[
  AddressProperties(street=1101 Russell St, city=Baltimore, state=MD, zip=21230),
  AddressProperties(street=347 Don Shula Drive, city=Miami, state=FLA, zip=33056)
])

System Properties

  • Java properties can come from several sources — this includes Java system properties

  • Example shows mapping three (3) system properties

Example System Properties JavaBean
@ConfigurationProperties("user")
@ConstructorBinding
@Data
public class UserProperties {
    @NotNull
    private final String name; (1)
    @NotNull
    private final String home; (2)
    @NotNull
    private final String timezone; (3)
1mapped to SystemProperty user.name
2mapped to SystemProperty user.home
3mapped to SystemProperty user.timezone

System Properties Usage

  • Gives easy access to mapped properties using standard getters

Example System Properties Usage
@Component
public class AppCommand implements CommandLineRunner {
...
    @Autowired
    private UserProperties userProps;

    public void run(String... args) throws Exception {
...
        System.out.println(userProps); (1)
        System.out.println("user.home=" + userProps.getHome()); (2)
1output UserProperties toString
2get specific value mapped from user.home


System Properties Example Output
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
UserProperties(name=jim, home=/Users/jim, timezone=America/New_York)
user.home=/Users/jim

@ConfigurationProperties Class Reuse

  • Examples to date have been singleton values mapped to one root prefix

  • However, could have groups of properties with same structure and different root prefixes

Example Properties with Common Structure
# application.yml
owner: (1)
  name: Steve Bushati
  address:
    city: Millersville
    state: MD
    zip: 21108

manager: (1)
  name: Eric Decosta
  address:
    city: Owings Mills
    state: MD
    zip: 21117
1owner and manager root prefixes both follow the same structural schema

  • Want two (2) bean instances that represent their respective person implemented as one JavaBean class

@ConfigurationProperties Class Reuse Mapping

  • Can structurally map both to the same class and create two instances

  • However, can no longer apply the @ConfigurationProperties annotation and prefix to the bean class

    • prefix is instance-specific

@ConfigurationProperties Class Reuse JavaBean Mapping
//@ConfigurationProperties("???") multiple prefixes mapped  (1)
@Data
@Validated
public class PersonProperties {
    @NotNull
    private String name;
    @NestedConfigurationProperty
    @NotNull
    private AddressProperties address;
1unable to apply root prefix-specific @ConfigurationProperties to class

@ConfigurationProperties @Bean Factory

  • Solution: add a @Bean factory method for each use

    • separates prefix definition from class definition

@Bean Factory Methods for Separate Property Root Prefixes
@SpringBootApplication
@ConfigurationPropertiesScan
public class ConfigurationPropertiesApp {
...
    @Bean
    @ConfigurationProperties("owner") (2)
    public PersonProperties ownerProps() {
        return new PersonProperties(); (1)
    }

    @Bean
    @ConfigurationProperties("manager") (2)
    public PersonProperties managerProps() {
        return new PersonProperties(); (1)
    }
1@Bean factory method returns JavaBean instance to use
2Spring populates the JavaBean according to the ConfigurationProperties annotation
We are no longer able to use read-only JavaBeans when using the @Bean factory method in this way. We are returning a default instance for Spring to populate based on the specified @ConfigurationProperties prefix of the factory method.

Injecting ownerProps

  • When we inject instance of PersonProperties into ownerProps attribute of component

    • ownerProps @Bean factory is called

    • get the information for our owner

Owner Person Injection
@Component
public class AppCommand implements CommandLineRunner {
    @Autowired
    private PersonProperties ownerProps;
Owner Person Injection Result
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
PersonProperties(name=Steve Bushati, address=AddressProperties(street=null, city=Millersville, state=MD, zip=21108))

Great! However, there was something subtle there that allowed things to work.

Injection Matching

Spring had two @Bean factory methods to chose from to produce an instance of PersonProperties.

Two PersonProperties Sources
    @Bean
    @ConfigurationProperties("owner")
    public PersonProperties ownerProps() {
...
    @Bean
    @ConfigurationProperties("manager")
    public PersonProperties managerProps() {
...

The ownerProps @Bean factory method name happened to match the ownerProps Java attribute name and that resolved the ambiguity.

Target Attribute Name for Injection provides Qualifier
@Component
public class AppCommand implements CommandLineRunner {
    @Autowired
    private PersonProperties ownerProps; (1)
1Attribute name of injected bean matches @Bean factory method name

Ambiguous Injection

  • add manager and specifically make the two names not match

    • there will be ambiguity as to which @Bean factory to use.

Manager Person Injection
@Component
public class AppCommand implements CommandLineRunner {
    @Autowired
    private PersonProperties manager; (1)
1Java attribute name does not match @Bean factory method name
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
***************************
APPLICATION FAILED TO START
***************************
Description:

Field manager in info.ejava.examples.app.config.configproperties.AppCommand
   required a single bean, but 2 were found:
        - ownerProps: defined by method 'ownerProps' in
      info.ejava.examples.app.config.configproperties.ConfigurationPropertiesApp
        - managerProps: defined by method 'managerProps' in
      info.ejava.examples.app.config.configproperties.ConfigurationPropertiesApp

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans,
or using @Qualifier to identify the bean that should be consumed

Injection @Qualifier

  • As the error message states, we can solve this one of several ways

    • @Qualifier route is mostly what we want

    • can do that one of at least three ways

way1: Create Custom @Qualifier Annotation

Create custom @Qualifier annotation and apply to @Bean factory and injection point

  • benefits: eliminates string name matching between factory mechanism and attribute

  • drawbacks: new annotation must be created and applied to both factory and injection point

way1: Custom @Manager Qualifier Annotation

Custom @Manager Qualifier Annotation
package info.ejava.examples.app.config.configproperties.properties;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Qualifier
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Manager {
}

way1: @Manager Annotation Usage

@Manager Annotation Applied to @Bean Factory Method
@Bean
@ConfigurationProperties("manager")
@Manager (1)
public PersonProperties managerProps() {
    return new PersonProperties();
}
1@Manager annotation used to add additional qualification beyond just type


@Manager Annotation Applied to Injection Point
@Autowired
private PersonProperties ownerProps;
@Autowired
@Manager (1)
private PersonProperties manager;
1@Manager annotation is used to disambiguate the factory choices

way2: @Bean Factory Method Name as Qualifier

Use name of @Bean factory method as qualifier

  • benefits: no custom qualifier class required and factory signature does not need to be modified

  • drawbacks: text string must match factory method name

    @Autowired
    private PersonProperties ownerProps;
    @Autowired
    @Qualifier("managerProps") (1)
    private PersonProperties manager;
    1@Bean factory name is being applied as a qualifier versus defining a type

way3: Match @Bean Factory Method Name

Change name of injected attribute to match @Bean factory method name

  • benefits: simple and properly represents the semantics of the singleton property

  • drawbacks: injected attribute name must match factory method name

PersonProperties Sources
    @Bean
    @ConfigurationProperties("owner")
    public PersonProperties ownerProps() {
...
    @Bean
    @ConfigurationProperties("manager")
    public PersonProperties managerProps() {
...
Injection Points
    @Autowired
    private PersonProperties ownerProps;
    @Autowired
    private PersonProperties managerProps; (1)
1Attribute name of injected bean matches @Bean factory method name

Ambiguous Injection Summary

  • factory choices and qualifiers is a whole topic within itself

  • simple way3 solution good enough

    • good to know there is easy way to use a @Qualifier

Summary

In this module we

  • mapped properties from property sources to JavaBean classes annotated with @ConfigurationProperties and injected them into component classes

  • generated property metadata that can be used by IDEs to provide an aid to configuring properties

  • implemented a read-only JavaBean

  • defined property validation using Jakarta EE Java Validation framework

  • generated boilerplate JavaBean constructs with the Lombok library

  • demonstrated how relaxed binding can lead to more flexible property names

  • mapped flat/simple properties, nested properties, and collections of properties

  • leveraged custom @Bean factories to reuse common property structure for different root instances

  • leveraged @Qualifier s in order to map or disambiguate injections