Skip to content

Commit bda2c93

Browse files
committed
Polish OnPropertyCondition
Signed-off-by: Dmytro Nosan <[email protected]>
1 parent b48ce96 commit bda2c93

File tree

3 files changed

+112
-18
lines changed

3 files changed

+112
-18
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package org.springframework.boot.autoconfigure.condition;
1818

19+
import java.lang.annotation.Annotation;
1920
import java.util.ArrayList;
2021
import java.util.List;
21-
import java.util.Map;
2222
import java.util.stream.Stream;
2323

2424
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
@@ -33,6 +33,7 @@
3333
import org.springframework.core.env.PropertyResolver;
3434
import org.springframework.core.type.AnnotatedTypeMetadata;
3535
import org.springframework.util.Assert;
36+
import org.springframework.util.ClassUtils;
3637
import org.springframework.util.StringUtils;
3738

3839
/**
@@ -43,23 +44,23 @@
4344
* @author Stephane Nicoll
4445
* @author Andy Wilkinson
4546
* @see ConditionalOnProperty
47+
* @see ConditionalOnBooleanProperty
4648
*/
4749
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
4850
class OnPropertyCondition extends SpringBootCondition {
4951

5052
@Override
5153
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
5254
MergedAnnotations annotations = metadata.getAnnotations();
53-
List<AnnotationAttributes> allAnnotationAttributes = Stream
55+
List<MergedAnnotation<Annotation>> allAnnotations = Stream
5456
.concat(annotations.stream(ConditionalOnProperty.class.getName()),
5557
annotations.stream(ConditionalOnBooleanProperty.class.getName()))
5658
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
57-
.map(MergedAnnotation::asAnnotationAttributes)
5859
.toList();
5960
List<ConditionMessage> noMatch = new ArrayList<>();
6061
List<ConditionMessage> match = new ArrayList<>();
61-
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
62-
ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
62+
for (MergedAnnotation<Annotation> annotation : allAnnotations) {
63+
ConditionOutcome outcome = determineOutcome(annotation, context.getEnvironment());
6364
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
6465
}
6566
if (!noMatch.isEmpty()) {
@@ -68,27 +69,29 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
6869
return ConditionOutcome.match(ConditionMessage.of(match));
6970
}
7071

71-
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) {
72-
Spec spec = new Spec(annotationAttributes);
72+
private ConditionOutcome determineOutcome(MergedAnnotation<Annotation> annotation, PropertyResolver resolver) {
73+
Class<Annotation> annotationType = annotation.getType();
74+
Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes());
7375
List<String> missingProperties = new ArrayList<>();
7476
List<String> nonMatchingProperties = new ArrayList<>();
7577
spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
7678
if (!missingProperties.isEmpty()) {
77-
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
79+
return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec)
7880
.didNotFind("property", "properties")
7981
.items(Style.QUOTE, missingProperties));
8082
}
8183
if (!nonMatchingProperties.isEmpty()) {
82-
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
84+
return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec)
8385
.found("different value in property", "different value in properties")
8486
.items(Style.QUOTE, nonMatchingProperties));
8587
}
86-
return ConditionOutcome
87-
.match(ConditionMessage.forCondition(ConditionalOnProperty.class, spec).because("matched"));
88+
return ConditionOutcome.match(ConditionMessage.forCondition(annotationType, spec).because("matched"));
8889
}
8990

9091
private static class Spec {
9192

93+
private final Class<?> annotationType;
94+
9295
private final String prefix;
9396

9497
private final String[] names;
@@ -97,7 +100,8 @@ private static class Spec {
97100

98101
private final boolean matchIfMissing;
99102

100-
Spec(AnnotationAttributes annotationAttributes) {
103+
Spec(Class<? extends Annotation> annotationType, AnnotationAttributes annotationAttributes) {
104+
this.annotationType = annotationType;
101105
this.prefix = (!annotationAttributes.containsKey("prefix")) ? "" : getPrefix(annotationAttributes);
102106
this.names = getNames(annotationAttributes);
103107
this.havingValue = annotationAttributes.get("havingValue").toString();
@@ -112,13 +116,13 @@ private String getPrefix(AnnotationAttributes annotationAttributes) {
112116
return prefix;
113117
}
114118

115-
private String[] getNames(Map<String, Object> annotationAttributes) {
119+
private String[] getNames(AnnotationAttributes annotationAttributes) {
116120
String[] value = (String[]) annotationAttributes.get("value");
117121
String[] name = (String[]) annotationAttributes.get("name");
118-
Assert.state(value.length > 0 || name.length > 0,
119-
"The name or value attribute of @ConditionalOnProperty must be specified");
120-
Assert.state(value.length == 0 || name.length == 0,
121-
"The name and value attributes of @ConditionalOnProperty are exclusive");
122+
Assert.state(value.length > 0 || name.length > 0, "The name or value attribute of @%s must be specified"
123+
.formatted(ClassUtils.getShortName(this.annotationType)));
124+
Assert.state(value.length == 0 || name.length == 0, "The name and value attributes of @%s are exclusive"
125+
.formatted(ClassUtils.getShortName(this.annotationType)));
122126
return (value.length > 0) ? value : name;
123127
}
124128

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanPropertyTests.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.autoconfigure.condition;
1818

19+
import java.util.function.Consumer;
20+
1921
import org.junit.jupiter.api.AfterEach;
2022
import org.junit.jupiter.api.Test;
2123

@@ -29,6 +31,7 @@
2931
import org.springframework.core.env.StandardEnvironment;
3032

3133
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3235

3336
/**
3437
* Tests for {@link ConditionalOnBooleanProperty @ConditionalOnBooleanProperty}.
@@ -144,6 +147,48 @@ void withPrefix() {
144147
assertThat(this.context.containsBean("foo")).isTrue();
145148
}
146149

150+
@Test
151+
void nameOrValueMustBeSpecified() {
152+
assertThatIllegalStateException().isThrownBy(() -> load(NoNameOrValueAttribute.class, "some.property"))
153+
.satisfies(causeMessageContaining(
154+
"The name or value attribute of @ConditionalOnBooleanProperty must be specified"));
155+
}
156+
157+
@Test
158+
void nameAndValueMustNotBeSpecified() {
159+
assertThatIllegalStateException().isThrownBy(() -> load(NameAndValueAttribute.class, "some.property"))
160+
.satisfies(causeMessageContaining(
161+
"The name and value attributes of @ConditionalOnBooleanProperty are exclusive"));
162+
}
163+
164+
@Test
165+
void conditionalReportWhenMatched() {
166+
load(Defaults.class, "test=true");
167+
assertThat(this.context.containsBean("foo")).isTrue();
168+
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnBooleanProperty (test=true) matched");
169+
}
170+
171+
@Test
172+
void conditionalReportWhenDoesNotMatch() {
173+
load(Defaults.class, "test=false");
174+
assertThat(this.context.containsBean("foo")).isFalse();
175+
assertThat(getConditionEvaluationReport())
176+
.contains("@ConditionalOnBooleanProperty (test=true) found different value in property 'test'");
177+
}
178+
179+
private <T extends Exception> Consumer<T> causeMessageContaining(String message) {
180+
return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message);
181+
}
182+
183+
private String getConditionEvaluationReport() {
184+
ConditionEvaluationReport report = ConditionEvaluationReport.get(this.context.getBeanFactory());
185+
StringBuilder builder = new StringBuilder();
186+
report.getConditionAndOutcomesBySource()
187+
.values()
188+
.forEach((outcomes) -> outcomes.forEach((outcome) -> builder.append(outcome.toString()).append('\n')));
189+
return builder.toString();
190+
}
191+
147192
private void load(Class<?> config, String... environment) {
148193
TestPropertyValues.of(environment).applyTo(this.environment);
149194
this.context = new SpringApplicationBuilder(config).environment(this.environment)
@@ -196,4 +241,26 @@ static class WithPrefix extends BeanConfiguration {
196241

197242
}
198243

244+
@Configuration(proxyBeanMethods = false)
245+
@ConditionalOnBooleanProperty
246+
static class NoNameOrValueAttribute {
247+
248+
@Bean
249+
String foo() {
250+
return "foo";
251+
}
252+
253+
}
254+
255+
@Configuration(proxyBeanMethods = false)
256+
@ConditionalOnBooleanProperty(value = "x", name = "y")
257+
static class NameAndValueAttribute {
258+
259+
@Bean
260+
String foo() {
261+
return "foo";
262+
}
263+
264+
}
265+
199266
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -271,13 +271,36 @@ void metaAndDirectAnnotationWithAliasConditionMatchesWhenBothPropertiesAreSet()
271271
assertThat(this.context.containsBean("foo")).isTrue();
272272
}
273273

274+
@Test
275+
void conditionalReportWhenMatched() {
276+
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2");
277+
assertThat(this.context.containsBean("foo")).isTrue();
278+
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched");
279+
}
280+
281+
@Test
282+
void conditionalReportWhenDoesNotMatch() {
283+
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1");
284+
assertThat(getConditionEvaluationReport())
285+
.contains("@ConditionalOnProperty ([property1,property2]) did not find property 'property2'");
286+
}
287+
274288
private void load(Class<?> config, String... environment) {
275289
TestPropertyValues.of(environment).applyTo(this.environment);
276290
this.context = new SpringApplicationBuilder(config).environment(this.environment)
277291
.web(WebApplicationType.NONE)
278292
.run();
279293
}
280294

295+
private String getConditionEvaluationReport() {
296+
ConditionEvaluationReport report = ConditionEvaluationReport.get(this.context.getBeanFactory());
297+
StringBuilder builder = new StringBuilder();
298+
report.getConditionAndOutcomesBySource()
299+
.values()
300+
.forEach((outcomes) -> outcomes.forEach((outcome) -> builder.append(outcome.toString()).append('\n')));
301+
return builder.toString();
302+
}
303+
281304
@Configuration(proxyBeanMethods = false)
282305
@ConditionalOnProperty(name = { "property1", "property2" })
283306
static class MultiplePropertiesRequiredConfiguration {

0 commit comments

Comments
 (0)