Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot condition based on whether a collection in configuration properties is empty or not

I have the following classes:

@Component
@ConifgurationProperties("redis")
public class RedisProperties {
    private List<String> hosts;
    // getters, setters
}

@Component
public class StaticRedisHostsProvider implements RedisHostsProvider {
    private final RedisProperties redisProperties;

    public StaticRedisHostsProvider(RedisProperties redisProperties) {
        this.redisProperties = redisProperties;
    }

    @Override
    public List<String> getAll() {
        return redisProperties.getHosts();
    }
}

@Component
public DiscoveryBasedRedisHostsProvider { ... }

I want StaticRedisHostsProvider to be used if redis.hosts property is specified, DiscoveryBasedRedisHostsProvider otherwise.

I could annotate StaticRedisHostsProvider with @ConditionalOnProperty(prefix = "redis", name = "hosts"), but there is no similar @ConditionalOnMissingProperty annotation for using with DiscoveryBasedRedisHostsProvider.

I tried to use @ConditionalOnExpression("@redisProperties.hosts.empty"), but it doesn't work for some reason:

Description:
A component required a bean named 'redisProperties' that could not be found.
Action:
Consider defining a bean named 'redisProperties' in your configuration.

Is there some way to fix that (maybe with @Order or similar annotations)?

like image 966
Victor Avatar asked Oct 15 '25 12:10

Victor


1 Answers

Here's my take on this issue with the use of custom conditions in Spring autoconfiguration.

@Conditional annotations are executed very early in during the application startup. Properties sources are already loaded but ConfgurationProperties beans are not yet created. However we can work around that issue by binding properties to Java POJO ourselves.

First I introduce a functional interface which will enable us to define any custom logic checking if properties are in fact present or not. In your case this method will take care of checking if the property List is empty or null.

public interface OptionalProperties {
  boolean isPresent();
}

Now let's create an annotation which will be metannotated with Spring @Conditional and allow us to define custom parameters. prefix represents the property namespace and targetClass represents the configuration properties model class to which properties should be mapped.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnConfigurationPropertiesCondition.class)
public @interface ConditionalOnConfigurationProperties {

  String prefix();

  Class<? extends OptionalProperties> targetClass();

}

And now the main part. The custom condition implementation.

public class OnConfigurationPropertiesCondition extends SpringBootCondition {

  @Override
  public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    MergedAnnotation<ConditionalOnConfigurationProperties> mergedAnnotation = metadata.getAnnotations().get(ConditionalOnConfigurationProperties.class);
    String prefix = mergedAnnotation.getString("prefix");
    Class<?> targetClass = mergedAnnotation.getClass("targetClass");
    // type precondition
    if (!OptionalProperties.class.isAssignableFrom(targetClass)) {
      return ConditionOutcome.noMatch("Target type does not implement the OptionalProperties interface.");
    }
    // the crux of this solution, binding properties to Java POJO
    Object bean = Binder.get(context.getEnvironment()).bind(prefix, targetClass).orElse(null);
    // if properties are not present at all return no match
    if (bean == null) {
      return ConditionOutcome.noMatch("Binding properties to target type resulted in null value.");
    }
    OptionalProperties props = (OptionalProperties) bean;

    // execute method from OptionalProperties interface 
    // to check if condition should be matched or not
    // can include any custom logic using property values in a type safe manner
    if (props.isPresent()) {
      return ConditionOutcome.match();
    } else {
      return ConditionOutcome.noMatch("Properties are not present.");
    }
  }

}

Now you should create your own configuration properties class implementing OptionalProperties interface.

@ConfigurationProperties("redis")
@ConstructorBinding
public class RedisProperties implements OptionalProperties {

  private final List<String> hosts;

  @Override
  public boolean isPresent() {
    return hosts != null && !hosts.isEmpty();
  }

}

And then in Spring @Configuration class.

@Configuration
class YourConfiguration {

  @ConditionalOnConfigurationProperty(prefix = "redis", targetClass = RedisProperties.class)
  StaticRedisHostsProvider staticRedisHostsProvider() {
    ...
  }

  @ConditionalOnMissingBean(StaticRedisHostsProvider.class)
  DiscoveryBasedRedisHostsProvider discoveryRedisHostsProvider() {
    ...
  }

} 

There are two downsides to this solution:

  • Property prefix must be specified in two locations: on @ConfigurationProperties annotation and on @ConditionalOnConfigurationProperties annotation. This can partially be alleviated by defining a public static final String PREFIX = "namespace" in your configuration properties POJO.
  • Property binding process is executed separately for each use of our custom conditional annotation and then once again to create the configuration properties bean itself. It happens only during app startup so it shouldn't be an issue but it still is an inefficiency.
like image 111
Tomasz Krug Avatar answered Oct 17 '25 02:10

Tomasz Krug



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!