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)?
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:
@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.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With