javaspringspring-bootconfigurationconfigurationproperties

How to inject generic @ConfigurationProperties in Spring


I have generic ClientProperties class used by multiple rest clients to bind the client specific properties with implementation.

@Data
@Validated
public class ClientProperties {

    @NotBlank
    private String url;

    @NotNull
    private LoggingProperties logging;
}

Here is an example of application.properties

# Kasittely service
kasittelyservice.url= ...
kasittelyservice.logging.enabled=true

# Some other service
otherservice.url= ...
otherservice.logging.enabled=false

Currently I'm doing this with remote clients

@Service
public class KasittelyServiceClient {

    private final RestTemplate restTemplate;

    @SuppressWarnings("unused")
    public KasittelyServiceClient(RestTemplateBuilder builder, @Qualifier("kasittelyService") ClientProperties properties) {
        
        builder.rootUri(properties.getUrl());

        if (properties.getLogging().isEnabled()) {
            // TODO: debug logging interceptor
        }

        restTemplate = builder.build();
    }

    public VastausDto kasittelePyynto(PyyntoDto pyyntoDto) {
        return restTemplate.postForObject("/kasittelePyynto", pyyntoDto, VastausDto.class);
    }

    @Configuration
    public static class KasittelyServiceClientConfiguration {

        @Bean
        @Qualifier("kasittelyService")
        @ConfigurationProperties(prefix = "kasittelyservice")
        public ClientProperties properties() {
            return new ClientProperties();
        }
    }
}

However, that is a lot of boilerplate to bind properties for each client.

Ideally, I would like to do something like this, which isn't allowed

public KasittelyServiceClient(RestTemplateBuilder builder, @ConfigurationProperties("kasittelyService") ClientProperties properties) {

        builder.rootUri(properties.getUrl());

        if (properties.getLogging().isEnabled()) {
            // TODO: debug logging interceptor
        }

        restTemplate = builder.build();
}

How do I reduce boilerplate in this case?


Solution

  • You can create an additional @ConfigurationProperties class that will contain the properties of all clients, which requires an additional prefix to be defined. For example:

    @Data
    @Component
    @ConfigurationProperties(prefix = "properties")
    public class AllClientsProperties {
      Map<String, ClientProperties> clients;
    
      public ClientProperties getClientPropertiesByKey(String key) {
        return clients.get(key);
      }
    }
    

    Then, change your application.properties accordingly.

    # Kasittely service
    properties.clients.kasittelyservice.url= ...
    properties.clients.kasittelyservice.logging.enabled=true
    
    # Some other service
    properties.clients.otherservice.url= ...
    properties.clients.otherservice.logging.enabled=false
    

    Lastly, add the AllClientsProperties bean as a dependency to the constructor of the client service, and get the properties of the client by the key which are used to perform the initialization logic.

    @Service
    public class KasittelyServiceClient {
      public static final String PROPERTIES_KEY = "kasittelyservice";
      private final RestTemplate restTemplate;
    
      @SuppressWarnings("unused")
      public KasittelyServiceClient(RestTemplateBuilder builder, AllClientsProperties allClientsProperties) {
        ClientProperties properties = allClientsProperties.getClientPropertiesByKey(PROPERTIES_KEY);
        builder.rootUri(properties.getUrl());
        if (properties.getLogging().isEnabled()) {
          // TODO: debug logging interceptor
        }
        restTemplate = builder.build();
      }
      //...
    }