Rohde Fischer
Rohde Fischer

Reputation: 1482

Nested Spring configuration (ConfigurationProperties) in records

How can one map an application.yaml configuration with nested properties to a similar record structure in Java?

E.g., if we have the following yaml:

foo:
    bar:
        something: 42

    baz:
        otherThing: true

    color: blue

The desired record structure would be something along the lines of:

@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
public record Foo(
    Bar bar,
    Baz baz,
    String color
) {}

// ---

@ConfigurationProperties(prefix = "foo.bar")
@ConstructorBinding
public record Bar(
    int something
) {}

// ---

@ConfigurationProperties(prefix = "foo.baz")
@ConstructorBinding
public record Baz(
    boolean otherThing
) {}

Upvotes: 10

Views: 14584

Answers (5)

user22395619
user22395619

Reputation: 1

One point I noted is that if we use the lombok annotation @AllArgsConstructor on the bean it won't work. Just use only @Data which gives setters and getters and a few others. Seems this piece of configuration works based off of setter injections. I spent lot of time figuring this out.

Upvotes: 0

Todd
Todd

Reputation: 642

I've had an oddly difficult time finding succinct, practical, working examples of this, so I thought I'd share a bit of code from after I pieced it together and got it working.

For some application.yml like this:

spring:
    security:
        oauth2:
            authorizationserver:
                client:
                    oidc-client:
                        registration:
                            client-id: "client ID value"
                            client-authentication-methods:
                                - "value 1"
                                - "value 2"
                                - "atc."

I created a class like this:

@Configuration
@ConfigurationPropertiesScan 
@ConfigurationProperties(prefix = "spring.security.oauth2.authorizationserver.client.oidc-client.registration")
public class RewindOidcConfiguration {

    private String clientId;
    public void setClientId(String value) { this.clientId = value; }

    private List<String> clientAuthenticationMethods;
    public void setClientAuthenticationMethods(List<String> value) { this.clientAuthenticationMethods = value; }

    ...

}

This sets the private fields which are then available for you to use as you wish. This demonstrates hierarchy, flexible binding for the names, list values, the need for setters (which Spring calls), etc.

Upvotes: 0

р&#252;ффп
р&#252;ффп

Reputation: 5448

I think for simplification you can just create a single file with:

@ConfigurationProperties(prefix = "foo")
public record Foo(Bar bar) {
    public record Bar(Baz baz) {
        public record Baz(String bum) {}    
    }
}

and this is working fine in spring-boot and you don't need to repeat annotations and when using it you will just use:

String bumVal = foo.bar().baz().bum();

where foo is just injected in your Bean(s) where you need it.

I removed even the @ConsructorBinding as since spring-boot 2.6 it is no more needed as long the record defines only one constructor, see release notes.

The above configuration relates to your own answer structure, but here is also the compact way for the original questions structure:

@ConfigurationProperties(prefix = "foo")
public record Foo(Bar bar, Baz baz, String color) {
    public record Bar(String something) {
    }
    public record Baz(String otherThing) {
    }   
}

I found the recordtype very useful for this use case as very compact and not too much code to write.

Upvotes: 10

Rohde Fischer
Rohde Fischer

Reputation: 1482

Turns out I didn't ask the correct question for the issue I had :/ So for the case people find this topic from a similar issue, the answer to my actual issue follows here.

The problem arises with a nested yaml trying to "short cut" on the model hierarchy, so given the following yaml:

foo:
    bar:
        baz:
            bum: "hello"

I was trying to model the hierarchy as follows:

@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
public record Foo(BarBaz barBaz) {}

// --- 

@ConfigurationProperties(prefix = "foo.bar.baz")
@ConstructorBinding
public record BarBaz(String bum) {}

Here the problem arises that Foo cannot do constructor binding for BarBaz (not sure why). So there are two possible solutions that I found:

1. Do the full modelling (decided that this is what I prefer)

That is, don't try to skip the middle model for bar.

@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
public record Foo(Bar bar) {}

// ---

@ConfigurationProperties(prefix = "foo.bar")
@ConstructorBinding
public record Bar(Baz baz) {}

// --- 

@ConfigurationProperties(prefix = "foo.bar.baz")
@ConstructorBinding
public record Baz(String bum) {}

2. Don't use @ConstructorBinding when embedding more nestings

Simply skip the constructor binding in Foo.

@ConfigurationProperties(prefix = "foo")
public record Foo(BarBaz barBaz) {}

// --- 

@ConfigurationProperties(prefix = "foo.bar.baz")
@ConstructorBinding
public record BarBaz(String bum) {}

Although simpler, it's less consistent.

Upvotes: 1

Ferry
Ferry

Reputation: 414

You don't need @ConfigurationProperties for each nested class. It only for the root class (Foo.class). Then make the Foo as Spring Bean by inserting @Component above the class or put @ConfigurationPropertiesScan on the Application class.

Upvotes: 5

Related Questions