@ConditionalOnProperty
Registers a bean or configuration only when a configuration property is present, or has a specific value, so features can be switched per environment.
@ConditionalOnProperty(prefix = "app.audit", name = "enabled", havingValue = "true", matchIfMissing = false)- Added in Spring Boot Spring Boot 1.1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$value / nameoptional | String[]= {} | Property names to test (with prefix prepended). All must match. Use value or name, not both. |
$prefixoptional | String= "" | Prefix applied to every name, for example app.audit. |
$havingValueoptional | String= "" | Expected value, compared case-insensitively. When empty, the condition matches if the property exists and is not "false". |
$matchIfMissingoptional | boolean= false | Whether the condition matches when the property is not set at all. |
Return value
n/aRegisters the annotated configuration class, component or @Bean method only when the named property matches.
Errors
Nothing is thrown when the condition does not match: the bean is skipped. Any injection point that requires it then fails startup with NoSuchBeanDefinitionException.
Changelog
- PHP Spring Boot 3.5.0The annotation became repeatable, and @ConditionalOnBooleanProperty was added for boolean flags.
@ConditionalOnProperty decides at startup whether a bean exists, based on a property. It is how Spring Boot's own auto-configuration offers enabled switches, and it works the same on your @Configuration classes, @Bean methods and components.
@Bean
@ConditionalOnProperty(prefix = "app.audit", name = "enabled", havingValue = "true")
AuditPublisher auditPublisher() { ... }How matching works
- With
havingValueset, the property must equal it (ignoring case). - Without
havingValue, the property must exist and must not befalse. Any other value, evennoor0, matches. matchIfMissingdecides what happens when the property is absent. The defaultfalsemeans a feature is off until switched on;truemeans it is on unless switched off.- Several names must all match.
Spring Boot 3.5 added @ConditionalOnBooleanProperty, whose havingValue is a boolean. For on/off flags it reads better and avoids the "no counts as on" surprise. The same release made @ConditionalOnProperty repeatable, so several properties with different expected values can be required on one bean.
Properties versus profiles
A profile describes an environment; a property describes a feature. app.audit.enabled=true can be set in exactly the environments that need it, overridden with APP_AUDIT_ENABLED, and read by anyone without knowing what each profile means. Profile-specific files are then a good place to set those properties.
Mistakes to avoid
- Choosing the wrong default. For something dangerous (deleting old data, sending real SMS) leave
matchIfMissingfalse so a forgotten setting means "off". - Making a bean conditional while other beans require it. When the property is off, startup fails with
NoSuchBeanDefinitionException. InjectObjectProvider<T>, or provide a no-op bean for the "off" case with the opposite condition. - Expecting it to react at runtime. The condition is evaluated once, when the context starts; changing the property later has no effect until a restart.
The conditions report printed with debug=true lists each conditional bean and why it matched or not, which is the quickest way to see why a bean is missing.
Example
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
interface AuditPublisher {
void publish(String event);
}
class KafkaAuditPublisher implements AuditPublisher {
public void publish(String event) {
// send to the audit topic
}
}
class NoOpAuditPublisher implements AuditPublisher {
public void publish(String event) {
}
}
(proxyBeanMethods = false)
class AuditConfig {
(prefix = "app.audit", name = "enabled", havingValue = "true")
AuditPublisher kafkaAuditPublisher() {
return new KafkaAuditPublisher();
}
// The "off" case still provides a bean, so callers never break.
(prefix = "app.audit", name = "enabled", havingValue = "false", matchIfMissing = true)
AuditPublisher noOpAuditPublisher() {
return new NoOpAuditPublisher();
}
}
public class AccountAuditService {
private final AuditPublisher audit;
public AccountAuditService(AuditPublisher audit) {
this.audit = audit;
}
public void passwordChanged(long userId) {
audit.publish("password-changed:" + userId);
}
}When to use it
- Switching an audit publisher on in production while development uses a no-op
- Switching a scheduled cleanup job off in the multi-replica API deployment so only a single worker deployment runs it
- Shipping a feature dark and enabling it per environment without a new build
Frequently asked questions
What does matchIfMissing do?
It decides the result when the property is not set at all. false (the default) means the bean is skipped; true means it is registered unless the property says otherwise.
Why is my conditional bean created even though I set the property to no?
Without havingValue, any value other than false matches. Set havingValue = "true" or use @ConditionalOnBooleanProperty.
Should I use @ConditionalOnProperty or @Profile?
Use a property for a feature that can be on or off anywhere, and a profile for differences between environments. Profiles can set the properties.
More examples
A boolean flag that is on unless switched off
The job runs unless app.cleanup.enabled=false is set. Replicas of one deployment share the same environment, so a flag cannot single out one replica: switch the job off in the API deployment and run it in a separate single-instance worker, or keep it on everywhere behind a distributed lock. @ConditionalOnBooleanProperty only matches the values true and false; anything else counts as not matching.
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Configuration(proxyBeanMethods = false)
@EnableScheduling
@ConditionalOnBooleanProperty(name = "app.cleanup.enabled", matchIfMissing = true)
public class CleanupConfig {
}
@Component
@ConditionalOnBooleanProperty(name = "app.cleanup.enabled", matchIfMissing = true)
class ExpiredSessionCleaner {
@Scheduled(cron = "0 15 3 * * *")
void purge() {
// delete sessions that expired more than a day ago
}
}Setting the switches per environment
The defaults live in the main file and production switches the audit feature on. Environment variables override both, so a single deployment can be adjusted without a rebuild.
# application.properties: safe defaults
app.audit.enabled=false
app.cleanup.enabled=true
# application-prod.properties would contain:
# app.audit.enabled=true
# and the API deployment, which runs several replicas, switches the job off
# so only the single-instance worker deployment runs it:
# APP_CLEANUP_ENABLED=falseTesting both sides of the condition
ApplicationContextRunner starts only the configuration under test, so each case runs in milliseconds. The third test pins down that havingValue = "true" rejects other spellings.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
class SmsSender {
}
@Configuration(proxyBeanMethods = false)
class SmsConfig {
@Bean
@ConditionalOnProperty(name = "app.sms.enabled", havingValue = "true")
SmsSender smsSender() {
return new SmsSender();
}
}
class SmsConfigTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner().withUserConfiguration(SmsConfig.class);
@Test
void offByDefault() {
runner.run(context -> assertThat(context).doesNotHaveBean(SmsSender.class));
}
@Test
void onWhenEnabled() {
runner.withPropertyValues("app.sms.enabled=true")
.run(context -> assertThat(context).hasSingleBean(SmsSender.class));
}
@Test
void anyOtherValueIsOff() {
runner.withPropertyValues("app.sms.enabled=yes")
.run(context -> assertThat(context).doesNotHaveBean(SmsSender.class));
}
}
Discussion