jim stafford
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
At the conclusion of this lecture and related exercises, the student will be able to:
map a Java @ConfigurationProperties
class to a group of properties
generate property metadata — used by IDEs for property editors
create read-only @ConfigurationProperties
class using constructor binding
define Jakarta EE Java validation rule for property and have validated at runtime
generate boilerplate JavaBean methods using Lombok library
use relaxed binding to map between JavaBean and property syntax
map nested properties to a @ConfigurationProperties
class
map array properties to a @ConfigurationProperties
class
reuse @ConfigurationProperties
class to map multiple property trees
use @Qualifier
annotation and other techniques to map or disambiguate an injection
Starting off simple …
# application.properties
app.config.car.name=Suburban
define a property (app.config.car.name
) to hold the name of a car
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 + "\'}";
}
}
1 | class is a standard Java bean with one property |
2 | class designed for us to use its default constructor and a setter() to assign value(s) |
3 | class annotated with @ConfigurationProperties to identify that is mapped to properties and
the property prefix that pertains to this class |
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)
...
1 | Our @ConfigurationProperties instance is being injected into a @Component class
using FIELD injection |
2 | Simple print statement of bean’s toString() result |
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)
1 | Error message indicates that Spring is not seeing our @ConfigurationProperties class |
Current problem similar to issue when first implementing @Configuration
and @Component
classes
the bean was not being scanned
Even though we have our @ConfigurationProperties
class is in the same basic classpath as the
@Configuration
and @Component
classes
|-- java
| `-- info
| `-- ejava
| `-- examples
| `-- app
| `-- config
| `-- configproperties
| |-- AppCommand.java
| |-- ConfigurationPropertiesApp.java (1)
| `-- properties
| `-- CarProperties.java (1)
`-- resources
`-- application.properties
1 | …properties.CarProperties Java package is under main class` Java package scope |
We need a little more to have it processed by Spring
There are several ways to do that:
package info.ejava.examples.app.config.configproperties.properties;
...
@Component
@ConfigurationProperties("app.config.car") (1)
public class CarProperties {
1 | causes 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
import info.ejava.examples.app.config.configproperties.properties.CarProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
...
@SpringBootApplication
@EnableConfigurationProperties(CarProperties.class) (1)
public class ConfigurationPropertiesApp {
1 | targets 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
@SpringBootApplication
@ConfigurationPropertiesScan (1)
public class ConfigurationPropertiesApp {
1 | allows a generalized scan to be defined that is separate for configurations |
We can control which root-level Java packages to scan. The default root is where annotation declared. |
benefits: easy to add more configuration classes without changing application
drawbacks: generalized scan may accidentally pick up an unwanted configuration
@Bean
Factory@SpringBootApplication
public class ConfigurationPropertiesApp {
...
@Bean
@ConfigurationProperties("app.config.car") (1)
public CarProperties carProperties() {
return new CarProperties();
}
1 | gives 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
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'}
IDEs have support for linking Java properties to their @ConfigurationProperty
class
information.
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!) |
IDEs rely on a JSON-formatted metadata file for that information
META-INF/spring-configuration-metadata.json
...
"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-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>
1 | dependency will generate additional artifacts during compilation |
2 | dependency 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. |
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;
1 | Javadoc information is extracted from the class and placed in the property metadata |
Rebuild module with Maven to generate JSON metadata file
$ mvn clean compile
target/classes
Treetarget/classes/META-INF/
`-- spring-configuration-metadata.json
{
"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": []
}
If your IDE supports Spring Boot and property metadata, the property editor will offer help filling out properties.
IntelliJ free Community Edition does not support this feature. The following link provides a comparison with the for-cost Ultimate Edition. |
Slight improvement — make the JavaBean read-only to match read-only contract with properties
...
import org.springframework.boot.context.properties.bind.ConstructorBinding;
@ConfigurationProperties("app.config.boat")
public class BoatProperties {
private final String name; (3)
@ConstructorBinding //only required for multiple constructors (2)
public BoatProperties(String name) {
this.name = name;
}
//not used for ConfigurationProperties initialization
public BoatProperties() { this.name = "default"; }
//no setter method(s) in read-only example (1)
public String getName() {
return name;
}
@Override
public String toString() {
return "BoatProperties{name='" + name + "\'}";
}
}
1 | remove setter methods to better advertise the read-only contract of the bean |
2 | add custom constructor and annotate with @ConstructorBinding when multiple ctors |
3 | make 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. |
no longer have setter method name(s) to map properties
constructor argument name(s) used instead
# application.properties
app.config.boat.name=Maxum
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
boatProperties=BoatProperties{name='Maxum'}
# 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;
}
1 | constructor argument name has been changed to not match the property name from application.properties |
$ 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. |
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
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 @NotBlank constraint to report error if property null or lacks non-whitespace character
...
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotBlank;
@ConfigurationProperties("app.config.boat")
@Validated (1)
public class BoatProperties {
@NotBlank (2)
private final String name;
@ConstructorBinding
public BoatProperties(String nameX) {
this.name = nameX;
}
...
1 | The Spring @Validated annotation tells Spring to validate instances of this
class |
2 | The Jakarta EE @NotBlank annotation tells the validator this field is not
allowed to be null or lacking a non-whitespace character |
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. |
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 \
--app.config.boat.name=
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target info.ejava.examples.app.config.configproperties.properties.BoatProperties failed:
Property: app.config.boat.name
Value: ""
Origin: "app.config.boat.name" from property source "commandLineArgs"
Reason: must not be blank
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. |
Notice all the boilerplate constructs in the class
...
@ConfigurationProperties("app.config.boat")
@Validated
public class BoatProperties {
@NotBlank
private final String name;
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 + "\'}";
}
}
1 | Many boilerplate methods in source code — likely generated by IDE |
Will get worse code gets more complex and more attributes added to classes
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")
@Data (1)
@Validated
public class CompanyProperties {
@NotNull
private final String name;
//constructor (1)
//getter (1)
//toString (1)
//hashCode and equals (1)
}
1 | Lombok @Data annotation generated constructor, getter(/setter), toString, hashCode, and equals |
Additional methods can be identified in a class structure view of an IDE
You may need to locate a compiler option within your IDE properties to make the code generation within your IDE. |
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 annotations are defined with
RetentionPolicy.SOURCE
.
discarded by compiler
not available at runtime
@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
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
...
@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)
...
1 | BoatProperties JavaBean methods were provided by hand |
2 | CompanyProperties 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
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
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
Each attribute expressed using camelCase
consistent with common Java coding conventions
@ConfigurationProperties("app.config.business")
@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;
}
Properties supplied use a variety of cases
# 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
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)
Previous examples used a flat property model
This example maps nested properties
(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
1 | name is part of a flat property model below corp |
2 | address is a container of nested properties |
Supply JavaBean to hold their nested properties
Reference from the host/outer-class
...
@Data
public class AddressProperties {
private final String street;
@NotNull
private final String city;
@NotNull
private final String state;
@NotNull
private final String zip;
}
Host class (CorporateProperties
) declares
base property prefix
reference (address
) to the nested class
...
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties("app.config.corp")
@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. |
Properties are populated within the host and nested bean
accessible to components within the application
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
corporationProperties=CorporationProperties(name=Acme,
address=AddressProperties(street=null, city=Newark, state=DE, zip=19711))
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
...
@ConfigurationProperties("app.config.route")
@Data
@Validated
public class RouteProperties {
@NotNull
private String name;
@NestedConfigurationProperty
@NotNull
@Size(min = 1)
private List<AddressProperties> stops; (1)
...
1 | RouteProperties hosts list of stops as AddressProperties |
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
Properties are populated within host and nested JavaBeans
Nested JavaBeans are added to a collection within the host
$ 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)
])
Java properties can come from several sources — this includes Java system properties
Example shows mapping three (3) system properties
@ConfigurationProperties("user")
@Data
public class UserProperties {
@NotNull
private final String name; (1)
@NotNull
private final String home; (2)
@NotNull
private final String timezone; (3)
1 | mapped to SystemProperty user.name |
2 | mapped to SystemProperty user.home |
3 | mapped to SystemProperty user.timezone |
Gives easy access to mapped properties using standard getters
@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)
1 | output UserProperties toString |
2 | get specific value mapped from user.home |
$ java -jar target/appconfig-configproperties-example-*-SNAPSHOT-bootexec.jar
...
UserProperties(name=jim, home=/Users/jim, timezone=America/New_York)
user.home=/Users/jim
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
# 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
1 | owner 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
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("???") multiple prefixes mapped (1)
@Data
@Validated
public class PersonProperties {
@NotNull
private String name;
@NestedConfigurationProperty
@NotNull
private AddressProperties address;
1 | unable to apply root prefix-specific @ConfigurationProperties to class |
Solution: add a @Bean
factory method for each use
separates prefix definition from class definition
@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 |
2 | Spring 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. |
When we inject instance of PersonProperties
into ownerProps
attribute of component
ownerProps
@Bean
factory is called
get the information for our owner
@Component
public class AppCommand implements CommandLineRunner {
@Autowired
private PersonProperties ownerProps;
$ 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.
Spring had two @Bean
factory methods to chose from to produce an instance of PersonProperties
.
@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.
@Component
public class AppCommand implements CommandLineRunner {
@Autowired
private PersonProperties ownerProps; (1)
1 | Attribute name of injected bean matches @Bean factory method name |
add manager
and specifically make the two names not match
there will be ambiguity as to which @Bean
factory to use.
@Component
public class AppCommand implements CommandLineRunner {
@Autowired
private PersonProperties manager; (1)
1 | Java 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
This may be due to missing parameter name information
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
Ensure that your compiler is configured to use the '-parameters' flag.
You may need to update both your build tool settings as well as your IDE.
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
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
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 {
}
@Bean
@ConfigurationProperties("manager")
@Manager (1)
public PersonProperties managerProps() {
return new PersonProperties();
}
1 | @Manager annotation used to add additional qualification beyond just type |
@Autowired
private PersonProperties ownerProps;
@Autowired
@Manager (1)
private PersonProperties manager;
1 | @Manager annotation is used to disambiguate the factory choices |
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 |
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
@Bean
@ConfigurationProperties("owner")
public PersonProperties ownerProps() {
...
@Bean
@ConfigurationProperties("manager")
public PersonProperties managerProps() {
...
@Autowired
private PersonProperties ownerProps;
@Autowired
private PersonProperties managerProps; (1)
1 | Attribute name of injected bean matches @Bean factory method name |
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
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