Is it possible to use @ConstructorBinding bound immutable @ConfigurationProperties beans in an implementation of EnvironmentPostProcessor?

Viewed 261

Is it possible to use @ConstructorBinding bound immutable @ConfigurationProperties beans in an implementation of EnvironmentPostProcessor?

I have an EnvironmentPostProcessor implementation:

@Order(LOWEST_PRECEDENCE)
@EnableConfigurationProperties({KeyVaultCertificatesEnvironmentProperties.class, KeyVaultCertificatesEnvironmentServerSslProperties.class})
public class KeyVaultCertificatesEnvironmentPostProcessor implements EnvironmentPostProcessor {
    private KeyVaultCertificatesEnvironmentProperties keyVaultProperites;
    private KeyVaultCertificatesEnvironmentServerSslProperties sslProperties;

This implementation must use some @ConfigurationProperties beans. I would like these bean to be immutable. Here is a sketch of the beans

@ConstructorBinding
@ConfigurationProperties(prefix=KeyVaultCertificatesEnvironmentProperties.PREFIX)
public class KeyVaultCertificatesEnvironmentProperties {

//...
}

@ConstructorBinding
@ConfigurationProperties(prefix=KeyVaultCertificatesEnvironmentServerSslProperties.PREFIX)
public class KeyVaultCertificatesEnvironmentServerSslProperties {
//...
}

I don't know how to make it so the two @ConfigurationProperties beans are available to my EnvironmentPostProcessor.postProcessEnvironment implementation. I suspect there may be some timing issues.

Any ideas how I can do this?

Thanks,

Ed

1 Answers

It's not possible to directly use any @ConfigurationProperties beans with an EnvironmentPostProcessor because the ApplicationContext that holds the beans is always created after the Environment. It is, however, possible to use the org.springframework.boot.context.properties.bind.Binder class directly if you want to create a an object from the properties that have been loaded into the Environment so far.

For example, you can have the following class:

public class Certs {

    // ... fields

    Certs(String first, String second) {
      // ...
    }   

}

and bind it with the following code:

Binder binder = Binder.get(environment);
Certs certs = binder.bind("my.certs", Certs.class).get();

Note that the Certs properties class doesn't use any @ConfigurationProperties or @ConstructorBinding annotations and it has a single constructor. For more complex setups you might need to use the Binder constructor that accepts a BindConstructorProvider.

If you're using Spring Boot 2.4 or above you might want to look into the ConfigDataLocationResolver and ConfigDataLoaders classes. These allow you to support custom imports from an application.properties or application.yaml file. The resolver provides a ConfigDataLocationResolverContext object with a getBinder() method that you can use for reading additional properties. You can read a bit about it in this blog post.

You can also look at the new org.springframework.boot.Bootstrapper interface that was added in Spring Boot 2.4 to help Spring Cloud solve a similar chicken-and-egg problem. There's also a test application in the Spring Boot codebase that might help.

Related