Specialized conditional annotations for controlling actuator endpoint and component registration based on endpoint availability, management port configuration, and info contributor enablement.
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.info.ConditionalOnEnabledInfoContributor;
import org.springframework.boot.actuate.autoconfigure.logging.ConditionalOnEnabledLoggingExport;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;| Annotation | Checks | Primary Use |
|---|---|---|
@ConditionalOnAvailableEndpoint | Endpoint enabled + exposed | Endpoint auto-configurations |
@ConditionalOnEnabledInfoContributor | Info contributor enabled | Info contributors |
@ConditionalOnEnabledLoggingExport | Logging export enabled | Logging exporters (since 3.4.0) |
@ConditionalOnManagementPort | Management port type | Port-specific beans |
@ManagementContextConfiguration | Management context type | Management context beans |
// Endpoint auto-configuration
@AutoConfiguration
@ConditionalOnAvailableEndpoint(endpoint = CustomEndpoint.class)
public class CustomEndpointAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CustomEndpoint customEndpoint() {
return new CustomEndpoint();
}
}
// Info contributor
@Component
@ConditionalOnEnabledInfoContributor("custom")
public class CustomInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("custom", Map.of("key", "value"));
}
}
// Management port specific
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
public ManagementSecurityFilter managementSecurityFilter() {
return new ManagementSecurityFilter();
}/**
* Condition that checks if endpoint is available (enabled AND exposed)
* Used extensively in endpoint auto-configurations
*
* Evaluation:
* 1. Endpoint enabled: management.endpoint.<id>.access != NONE
* 2. Endpoint exposed: management.endpoints.{web|jmx}.exposure.include contains <id>
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnAvailableEndpointCondition.class)
public @interface ConditionalOnAvailableEndpoint {
Class<?> endpoint() default Void.class; // Endpoint class to check
Class<?> value() default Void.class; // Alias for endpoint
EndpointExposure[] exposure() default {}; // Technologies to check (WEB, JMX, or both)
}Properties Checked:
# Endpoint enabled
management.endpoint.<id>.access=UNRESTRICTED|READ_ONLY|NONE
# Endpoint exposed
management.endpoints.web.exposure.include=<id>
management.endpoints.jmx.exposure.include=<id>Usage:
@AutoConfiguration
@ConditionalOnAvailableEndpoint(endpoint = BeansEndpoint.class)
public class BeansEndpointAutoConfiguration {
// Only registered if beans endpoint enabled + exposed
}
@Bean
@ConditionalOnAvailableEndpoint(endpoint = InfoEndpoint.class, exposure = EndpointExposure.WEB)
public InfoWebFilter infoWebFilter() {
// Only if info exposed via web
}/**
* Condition for info contributors
* Checks: management.info.<name>.enabled
* Fallback: management.info.defaults.enabled (if USE_DEFAULTS_PROPERTY)
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnEnabledInfoContributorCondition.class)
public @interface ConditionalOnEnabledInfoContributor {
String value(); // Contributor name
InfoContributorFallback fallback() default USE_DEFAULTS_PROPERTY; // Fallback behavior
}
public enum InfoContributorFallback {
USE_DEFAULTS_PROPERTY, // Check management.info.defaults.enabled
DISABLE // Disable if not explicitly enabled
}Property Resolution:
1. Check management.info.<name>.enabled
↓ (if not set)
2. If fallback == USE_DEFAULTS_PROPERTY:
Check management.info.defaults.enabled (default: true)
If fallback == DISABLE:
Disable contributorProperties:
# Per-contributor
management.info.git.enabled=true
management.info.env.enabled=true
# Default fallback
management.info.defaults.enabled=trueUsage:
@Bean
@ConditionalOnEnabledInfoContributor("git")
public GitInfoContributor gitInfoContributor(GitProperties properties) {
return new GitInfoContributor(properties);
}
@Bean
@ConditionalOnEnabledInfoContributor(value = "custom", fallback = DISABLE)
public CustomInfoContributor customInfoContributor() {
// Only if management.info.custom.enabled=true (no fallback)
}/**
* Condition for logging exporters
* Checks if logging export is enabled globally or for a specific exporter
* Matches if management.logging.export.enabled is true or not configured
*
* @since 3.4.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnEnabledLoggingExportCondition.class)
public @interface ConditionalOnEnabledLoggingExport {
String value() default ""; // Name of the logging exporter (optional)
}Property Resolution:
1. If value() is set (specific exporter name):
- Check management.logging.export.<name>.enabled
- If not set, fall back to management.logging.export.enabled
2. If value() is empty (global):
- Check management.logging.export.enabled (default: true)Properties:
# Global logging export (default: true)
management.logging.export.enabled=true
# Per-exporter configuration
management.logging.export.otlp.enabled=true
management.logging.export.zipkin.enabled=falseUsage:
// Global logging export check
@Bean
@ConditionalOnEnabledLoggingExport
public LoggingExportConfiguration loggingExportConfiguration() {
return new LoggingExportConfiguration();
}
// Specific exporter check
@Bean
@ConditionalOnEnabledLoggingExport("otlp")
public OtlpLoggingExporter otlpLoggingExporter() {
// Only if management.logging.export.otlp.enabled=true
// Falls back to management.logging.export.enabled if not set
return new OtlpLoggingExporter();
}/**
* Condition based on management port type
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnManagementPortCondition.class)
public @interface ConditionalOnManagementPort {
ManagementPortType value(); // Port type to match
}
public enum ManagementPortType {
DISABLED, // management.server.port=-1
SAME, // port not set or same as server.port
DIFFERENT; // separate port specified
/**
* Determines type from environment
* Logic:
* - DISABLED: if management.server.port < 0
* - SAME: if port null OR (server.port null AND mgmt.port == 8080) OR (mgmt.port == server.port)
* - DIFFERENT: otherwise
*/
public static ManagementPortType get(Environment environment);
}Properties:
# Not set or same
management.server.port=${server.port} # SAME
# Different
management.server.port=8081 # DIFFERENT
# Disabled
management.server.port=-1 # DISABLEDUsage:
@Bean
@ConditionalOnManagementPort(ManagementPortType.SAME)
public SamePortFilter samePortFilter() {
return new SamePortFilter();
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
public ChildContextInitializer childContextInitializer() {
return new ChildContextInitializer();
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DISABLED)
public NoOpManagementConfig noOpConfig() {
return new NoOpManagementConfig();
}/**
* Configuration specific to management context
* Registered via META-INF/spring/...ManagementContextConfiguration.imports
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
public @interface ManagementContextConfiguration {
ManagementContextType value() default ManagementContextType.ANY;
boolean proxyBeanMethods() default true;
}
public enum ManagementContextType {
SAME, // Same context as application
CHILD, // Separate child context (separate port)
ANY // Applies to both
}Registration: META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
com.example.CommonManagementConfiguration
com.example.ChildManagementConfigurationUsage:
// Applies to both same and child contexts
@ManagementContextConfiguration
public class CommonConfiguration {
@Bean
public CommonBean commonBean() {
return new CommonBean();
}
}
// Only for child context (separate port)
@ManagementContextConfiguration(ManagementContextType.CHILD)
public class ChildConfiguration {
@Bean
public ChildOnlyBean childOnlyBean() {
return new ChildOnlyBean();
}
}
// Only for same context
@ManagementContextConfiguration(ManagementContextType.SAME)
public class SameConfiguration {
@Bean
public SameOnlyBean sameOnlyBean() {
return new SameOnlyBean();
}
}/**
* SPI interface for contributing to endpoint exposure determination
* Loaded from spring.factories and used by @ConditionalOnAvailableEndpoint
*
* If any contributor returns a matching ConditionOutcome, the endpoint is considered exposed
* Implementations may declare a constructor that accepts an Environment argument
*
* @since 3.4.0
*/
public interface EndpointExposureOutcomeContributor {
/**
* Determines if the given endpoint is exposed for the given exposure technologies
* @param endpointId the endpoint ID
* @param exposures the exposure technologies to check (WEB, JMX, or both)
* @param message the condition message builder for logging
* @return a matching ConditionOutcome if endpoint is exposed, or null if contributor does not apply
*/
@Nullable ConditionOutcome getExposureOutcome(
EndpointId endpointId,
Set<EndpointExposure> exposures,
ConditionMessage.Builder message
);
}Registration: META-INF/spring/org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor.imports
Example Implementation:
public class CustomEndpointExposureContributor implements EndpointExposureOutcomeContributor {
private final Environment environment;
// Constructor with Environment is optional but supported
public CustomEndpointExposureContributor(Environment environment) {
this.environment = environment;
}
@Override
public ConditionOutcome getExposureOutcome(
EndpointId endpointId,
Set<EndpointExposure> exposures,
ConditionMessage.Builder message) {
// Custom logic to determine if endpoint should be exposed
if (endpointId.toString().equals("custom")) {
boolean exposed = environment.getProperty("custom.endpoint.exposed", Boolean.class, false);
if (exposed) {
return ConditionOutcome.match(message.found("custom endpoint").items("exposed"));
}
return ConditionOutcome.noMatch(message.didNotFind("custom endpoint").items("not exposed"));
}
// Return null if this contributor doesn't apply to this endpoint
return null;
}
}Use Case: Custom exposure logic beyond standard include/exclude patterns, such as:
/**
* Base class for custom endpoint element conditions
* Supports per-element and default enablement
*
* Property resolution order:
* 1. <prefix><element-name>.enabled
* 2. <prefix>defaults.enabled (default: true)
*
* @since 2.0.0
*/
public abstract class OnEndpointElementCondition extends SpringBootCondition {
protected OnEndpointElementCondition(String prefix, Class<? extends Annotation> annotationType);
protected @Nullable ConditionOutcome getEndpointOutcome(ConditionContext context, String endpointName);
protected ConditionOutcome getDefaultOutcome(ConditionContext context, AnnotationAttributes annotationAttributes);
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);
}Example: Creating custom condition
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnEnabledMyElementCondition.class)
public @interface ConditionalOnEnabledMyElement {
String value();
}
class OnEnabledMyElementCondition extends OnEndpointElementCondition {
OnEnabledMyElementCondition() {
super("management.myelement.", ConditionalOnEnabledMyElement.class);
}
}Properties:
# Specific element
management.myelement.foo.enabled=true
# Default for all
management.myelement.defaults.enabled=true// Endpoint class
@Endpoint(id = "custom")
public class CustomEndpoint {
@ReadOperation
public Map<String, Object> info() {
return Map.of("status", "ok");
}
}
// Auto-configuration
@AutoConfiguration
@ConditionalOnAvailableEndpoint(endpoint = CustomEndpoint.class)
public class CustomEndpointAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CustomEndpoint customEndpoint() {
return new CustomEndpoint();
}
}Properties:
management.endpoint.custom.access=UNRESTRICTED
management.endpoints.web.exposure.include=custom@Component
@ConditionalOnEnabledInfoContributor("app")
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("app", Map.of(
"name", "My App",
"version", "1.0.0"
));
}
}Properties:
management.info.app.enabled=true@Configuration
public class SecurityConfiguration {
@Bean
@ConditionalOnManagementPort(ManagementPortType.SAME)
public SecurityFilterChain combinedSecurityFilterChain(HttpSecurity http) {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**").hasRole("ACTUATOR")
.anyRequest().authenticated());
return http.build();
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@Order(1)
public SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) {
http.securityMatcher(request -> request.getServerPort() == 8081)
.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ACTUATOR"));
return http.build();
}
}@Configuration
@ConditionalOnClass(SomeLibrary.class)
public class ConditionalConfiguration {
// Only when metrics endpoint available AND using same port
@Bean
@ConditionalOnAvailableEndpoint(endpoint = MetricsEndpoint.class)
@ConditionalOnManagementPort(ManagementPortType.SAME)
public MetricsIntegration metricsIntegration() {
return new MetricsIntegration();
}
// Only when using different port AND web exposure
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@ConditionalOnAvailableEndpoint(endpoint = MetricsEndpoint.class, exposure = EndpointExposure.WEB)
public ManagementMetricsFilter managementMetricsFilter() {
return new ManagementMetricsFilter();
}
}Debug:
logging.level.org.springframework.boot.autoconfigure=DEBUG
logging.level.org.springframework.boot.actuate.autoconfigure=DEBUGCheck:
management.endpoint.<id>.accessmanagement.endpoints.web.exposure.includemanagement.server.portmanagement.info.<name>.enabledConditions evaluated in this order:
@ConditionalOnClass, @ConditionalOnBean@ConditionalOnAvailableEndpoint@ConditionalOnManagementPort@ConditionalOnPropertyCause: Not registered in imports file
Solution: Add to META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
@SpringBootTest(properties = {
"management.endpoint.custom.access=UNRESTRICTED",
"management.endpoints.web.exposure.include=custom"
})
class ConditionalTest {
@Autowired
private ApplicationContext context;
@Test
void customEndpointIsRegistered() {
assertThat(context.containsBean("customEndpoint")).isTrue();
}
}
@SpringBootTest(properties = {
"management.server.port=8081"
})
class ManagementPortTest {
@Autowired(required = false)
@Qualifier("managementSecurityFilter")
private SecurityFilter managementFilter;
@Test
void managementFilterIsRegistered() {
assertThat(managementFilter).isNotNull();
}
}