diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java index d3e1d663b6a9..3f86cc35ef89 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java @@ -16,9 +16,9 @@ package org.springframework.boot.autoconfigure.condition; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.stream.Stream; import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style; @@ -33,6 +33,7 @@ import org.springframework.core.env.PropertyResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -43,6 +44,7 @@ * @author Stephane Nicoll * @author Andy Wilkinson * @see ConditionalOnProperty + * @see ConditionalOnBooleanProperty */ @Order(Ordered.HIGHEST_PRECEDENCE + 40) class OnPropertyCondition extends SpringBootCondition { @@ -50,16 +52,15 @@ class OnPropertyCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { MergedAnnotations annotations = metadata.getAnnotations(); - List allAnnotationAttributes = Stream + List> allAnnotations = Stream .concat(annotations.stream(ConditionalOnProperty.class.getName()), annotations.stream(ConditionalOnBooleanProperty.class.getName())) .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) - .map(MergedAnnotation::asAnnotationAttributes) .toList(); List noMatch = new ArrayList<>(); List match = new ArrayList<>(); - for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) { - ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment()); + for (MergedAnnotation annotation : allAnnotations) { + ConditionOutcome outcome = determineOutcome(annotation, context.getEnvironment()); (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage()); } if (!noMatch.isEmpty()) { @@ -68,27 +69,29 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM return ConditionOutcome.match(ConditionMessage.of(match)); } - private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) { - Spec spec = new Spec(annotationAttributes); + private ConditionOutcome determineOutcome(MergedAnnotation annotation, PropertyResolver resolver) { + Class annotationType = annotation.getType(); + Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes()); List missingProperties = new ArrayList<>(); List nonMatchingProperties = new ArrayList<>(); spec.collectProperties(resolver, missingProperties, nonMatchingProperties); if (!missingProperties.isEmpty()) { - return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec) + return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec) .didNotFind("property", "properties") .items(Style.QUOTE, missingProperties)); } if (!nonMatchingProperties.isEmpty()) { - return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec) + return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec) .found("different value in property", "different value in properties") .items(Style.QUOTE, nonMatchingProperties)); } - return ConditionOutcome - .match(ConditionMessage.forCondition(ConditionalOnProperty.class, spec).because("matched")); + return ConditionOutcome.match(ConditionMessage.forCondition(annotationType, spec).because("matched")); } private static class Spec { + private final Class annotationType; + private final String prefix; private final String[] names; @@ -97,7 +100,8 @@ private static class Spec { private final boolean matchIfMissing; - Spec(AnnotationAttributes annotationAttributes) { + Spec(Class annotationType, AnnotationAttributes annotationAttributes) { + this.annotationType = annotationType; this.prefix = (!annotationAttributes.containsKey("prefix")) ? "" : getPrefix(annotationAttributes); this.names = getNames(annotationAttributes); this.havingValue = annotationAttributes.get("havingValue").toString(); @@ -112,13 +116,13 @@ private String getPrefix(AnnotationAttributes annotationAttributes) { return prefix; } - private String[] getNames(Map annotationAttributes) { + private String[] getNames(AnnotationAttributes annotationAttributes) { String[] value = (String[]) annotationAttributes.get("value"); String[] name = (String[]) annotationAttributes.get("name"); - Assert.state(value.length > 0 || name.length > 0, - "The name or value attribute of @ConditionalOnProperty must be specified"); - Assert.state(value.length == 0 || name.length == 0, - "The name and value attributes of @ConditionalOnProperty are exclusive"); + Assert.state(value.length > 0 || name.length > 0, "The name or value attribute of @%s must be specified" + .formatted(ClassUtils.getShortName(this.annotationType))); + Assert.state(value.length == 0 || name.length == 0, "The name and value attributes of @%s are exclusive" + .formatted(ClassUtils.getShortName(this.annotationType))); return (value.length > 0) ? value : name; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanPropertyTests.java index 4b083b90cf90..aff83eac9e90 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanPropertyTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanPropertyTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.condition; +import java.util.function.Consumer; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -29,6 +31,7 @@ import org.springframework.core.env.StandardEnvironment; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ConditionalOnBooleanProperty @ConditionalOnBooleanProperty}. @@ -144,6 +147,48 @@ void withPrefix() { assertThat(this.context.containsBean("foo")).isTrue(); } + @Test + void nameOrValueMustBeSpecified() { + assertThatIllegalStateException().isThrownBy(() -> load(NoNameOrValueAttribute.class, "some.property")) + .satisfies(causeMessageContaining( + "The name or value attribute of @ConditionalOnBooleanProperty must be specified")); + } + + @Test + void nameAndValueMustNotBeSpecified() { + assertThatIllegalStateException().isThrownBy(() -> load(NameAndValueAttribute.class, "some.property")) + .satisfies(causeMessageContaining( + "The name and value attributes of @ConditionalOnBooleanProperty are exclusive")); + } + + @Test + void conditionReportWhenMatched() { + load(Defaults.class, "test=true"); + assertThat(this.context.containsBean("foo")).isTrue(); + assertThat(getConditionEvaluationReport()).contains("@ConditionalOnBooleanProperty (test=true) matched"); + } + + @Test + void conditionReportWhenDoesNotMatch() { + load(Defaults.class, "test=false"); + assertThat(this.context.containsBean("foo")).isFalse(); + assertThat(getConditionEvaluationReport()) + .contains("@ConditionalOnBooleanProperty (test=true) found different value in property 'test'"); + } + + private Consumer causeMessageContaining(String message) { + return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message); + } + + private String getConditionEvaluationReport() { + ConditionEvaluationReport report = ConditionEvaluationReport.get(this.context.getBeanFactory()); + StringBuilder builder = new StringBuilder(); + report.getConditionAndOutcomesBySource() + .values() + .forEach((outcomes) -> outcomes.forEach((outcome) -> builder.append(outcome.toString()).append('\n'))); + return builder.toString(); + } + private void load(Class config, String... environment) { TestPropertyValues.of(environment).applyTo(this.environment); this.context = new SpringApplicationBuilder(config).environment(this.environment) @@ -196,4 +241,26 @@ static class WithPrefix extends BeanConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnBooleanProperty + static class NoNameOrValueAttribute { + + @Bean + String foo() { + return "foo"; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBooleanProperty(value = "x", name = "y") + static class NameAndValueAttribute { + + @Bean + String foo() { + return "foo"; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java index f663915ce99e..73d8dafff194 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -271,6 +271,20 @@ void metaAndDirectAnnotationWithAliasConditionMatchesWhenBothPropertiesAreSet() assertThat(this.context.containsBean("foo")).isTrue(); } + @Test + void conditionReportWhenMatched() { + load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2"); + assertThat(this.context.containsBean("foo")).isTrue(); + assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched"); + } + + @Test + void conditionReportWhenDoesNotMatch() { + load(MultiplePropertiesRequiredConfiguration.class, "property1=value1"); + assertThat(getConditionEvaluationReport()) + .contains("@ConditionalOnProperty ([property1,property2]) did not find property 'property2'"); + } + private void load(Class config, String... environment) { TestPropertyValues.of(environment).applyTo(this.environment); this.context = new SpringApplicationBuilder(config).environment(this.environment) @@ -278,6 +292,15 @@ private void load(Class config, String... environment) { .run(); } + private String getConditionEvaluationReport() { + ConditionEvaluationReport report = ConditionEvaluationReport.get(this.context.getBeanFactory()); + StringBuilder builder = new StringBuilder(); + report.getConditionAndOutcomesBySource() + .values() + .forEach((outcomes) -> outcomes.forEach((outcome) -> builder.append(outcome.toString()).append('\n'))); + return builder.toString(); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(name = { "property1", "property2" }) static class MultiplePropertiesRequiredConfiguration {