Skip to content

Commit 17acb85

Browse files
committed
Merge pull request #43754 from nosan
* pr/43754: Polish OnPropertyCondition Polish OnPropertyCondition Closes gh-43754
2 parents 59bcaaf + 4812328 commit 17acb85

File tree

4 files changed

+131
-22
lines changed

4 files changed

+131
-22
lines changed

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReport.java

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import java.util.Set;
2828
import java.util.SortedMap;
2929
import java.util.TreeMap;
30+
import java.util.stream.Stream;
31+
import java.util.stream.StreamSupport;
3032

3133
import org.springframework.beans.factory.BeanFactory;
3234
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@@ -237,6 +239,15 @@ public boolean isFullMatch() {
237239
return true;
238240
}
239241

242+
/**
243+
* Return a {@link Stream} of the {@link ConditionAndOutcome} items.
244+
* @return a stream of the {@link ConditionAndOutcome} items.
245+
* @since 3.5.0
246+
*/
247+
public Stream<ConditionAndOutcome> stream() {
248+
return StreamSupport.stream(spliterator(), false);
249+
}
250+
240251
@Override
241252
public Iterator<ConditionAndOutcome> iterator() {
242253
return Collections.unmodifiableSet(this.outcomes).iterator();

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

+23-21
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;
@@ -28,11 +28,11 @@
2828
import org.springframework.core.annotation.AnnotationAttributes;
2929
import org.springframework.core.annotation.MergedAnnotation;
3030
import org.springframework.core.annotation.MergedAnnotationPredicates;
31-
import org.springframework.core.annotation.MergedAnnotations;
3231
import org.springframework.core.annotation.Order;
3332
import org.springframework.core.env.PropertyResolver;
3433
import org.springframework.core.type.AnnotatedTypeMetadata;
3534
import org.springframework.util.Assert;
35+
import org.springframework.util.ClassUtils;
3636
import org.springframework.util.StringUtils;
3737

3838
/**
@@ -43,23 +43,22 @@
4343
* @author Stephane Nicoll
4444
* @author Andy Wilkinson
4545
* @see ConditionalOnProperty
46+
* @see ConditionalOnBooleanProperty
4647
*/
4748
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
4849
class OnPropertyCondition extends SpringBootCondition {
4950

5051
@Override
5152
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
52-
MergedAnnotations annotations = metadata.getAnnotations();
53-
List<AnnotationAttributes> allAnnotationAttributes = Stream
54-
.concat(annotations.stream(ConditionalOnProperty.class.getName()),
55-
annotations.stream(ConditionalOnBooleanProperty.class.getName()))
53+
List<MergedAnnotation<Annotation>> annotations = Stream
54+
.concat(metadata.getAnnotations().stream(ConditionalOnProperty.class.getName()),
55+
metadata.getAnnotations().stream(ConditionalOnBooleanProperty.class.getName()))
5656
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
57-
.map(MergedAnnotation::asAnnotationAttributes)
5857
.toList();
5958
List<ConditionMessage> noMatch = new ArrayList<>();
6059
List<ConditionMessage> match = new ArrayList<>();
61-
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
62-
ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
60+
for (MergedAnnotation<Annotation> annotation : annotations) {
61+
ConditionOutcome outcome = determineOutcome(annotation, context.getEnvironment());
6362
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
6463
}
6564
if (!noMatch.isEmpty()) {
@@ -68,27 +67,29 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
6867
return ConditionOutcome.match(ConditionMessage.of(match));
6968
}
7069

71-
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) {
72-
Spec spec = new Spec(annotationAttributes);
70+
private ConditionOutcome determineOutcome(MergedAnnotation<Annotation> annotation, PropertyResolver resolver) {
71+
Class<Annotation> annotationType = annotation.getType();
72+
Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes());
7373
List<String> missingProperties = new ArrayList<>();
7474
List<String> nonMatchingProperties = new ArrayList<>();
7575
spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
7676
if (!missingProperties.isEmpty()) {
77-
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
77+
return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec)
7878
.didNotFind("property", "properties")
7979
.items(Style.QUOTE, missingProperties));
8080
}
8181
if (!nonMatchingProperties.isEmpty()) {
82-
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
82+
return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec)
8383
.found("different value in property", "different value in properties")
8484
.items(Style.QUOTE, nonMatchingProperties));
8585
}
86-
return ConditionOutcome
87-
.match(ConditionMessage.forCondition(ConditionalOnProperty.class, spec).because("matched"));
86+
return ConditionOutcome.match(ConditionMessage.forCondition(annotationType, spec).because("matched"));
8887
}
8988

9089
private static class Spec {
9190

91+
private final Class<? extends Annotation> annotationType;
92+
9293
private final String prefix;
9394

9495
private final String[] names;
@@ -97,7 +98,8 @@ private static class Spec {
9798

9899
private final boolean matchIfMissing;
99100

100-
Spec(AnnotationAttributes annotationAttributes) {
101+
Spec(Class<? extends Annotation> annotationType, AnnotationAttributes annotationAttributes) {
102+
this.annotationType = annotationType;
101103
this.prefix = (!annotationAttributes.containsKey("prefix")) ? "" : getPrefix(annotationAttributes);
102104
this.names = getNames(annotationAttributes);
103105
this.havingValue = annotationAttributes.get("havingValue").toString();
@@ -112,13 +114,13 @@ private String getPrefix(AnnotationAttributes annotationAttributes) {
112114
return prefix;
113115
}
114116

115-
private String[] getNames(Map<String, Object> annotationAttributes) {
117+
private String[] getNames(AnnotationAttributes annotationAttributes) {
116118
String[] value = (String[]) annotationAttributes.get("value");
117119
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");
120+
Assert.state(value.length > 0 || name.length > 0, "The name or value attribute of @%s must be specified"
121+
.formatted(ClassUtils.getShortName(this.annotationType)));
122+
Assert.state(value.length == 0 || name.length == 0, "The name and value attributes of @%s are exclusive"
123+
.formatted(ClassUtils.getShortName(this.annotationType)));
122124
return (value.length > 0) ? value : name;
123125
}
124126

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

+70
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

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

19+
import java.util.function.Consumer;
20+
import java.util.stream.Collectors;
21+
1922
import org.junit.jupiter.api.AfterEach;
2023
import org.junit.jupiter.api.Test;
2124

2225
import org.springframework.boot.WebApplicationType;
26+
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
2327
import org.springframework.boot.builder.SpringApplicationBuilder;
2428
import org.springframework.boot.test.util.TestPropertyValues;
2529
import org.springframework.context.ConfigurableApplicationContext;
@@ -29,6 +33,7 @@
2933
import org.springframework.core.env.StandardEnvironment;
3034

3135
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3237

3338
/**
3439
* Tests for {@link ConditionalOnBooleanProperty @ConditionalOnBooleanProperty}.
@@ -144,6 +149,49 @@ void withPrefix() {
144149
assertThat(this.context.containsBean("foo")).isTrue();
145150
}
146151

152+
@Test
153+
void nameOrValueMustBeSpecified() {
154+
assertThatIllegalStateException().isThrownBy(() -> load(NoNameOrValueAttribute.class, "some.property"))
155+
.satisfies(causeMessageContaining(
156+
"The name or value attribute of @ConditionalOnBooleanProperty must be specified"));
157+
}
158+
159+
@Test
160+
void nameAndValueMustNotBeSpecified() {
161+
assertThatIllegalStateException().isThrownBy(() -> load(NameAndValueAttribute.class, "some.property"))
162+
.satisfies(causeMessageContaining(
163+
"The name and value attributes of @ConditionalOnBooleanProperty are exclusive"));
164+
}
165+
166+
@Test
167+
void conditionReportWhenMatched() {
168+
load(Defaults.class, "test=true");
169+
assertThat(this.context.containsBean("foo")).isTrue();
170+
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnBooleanProperty (test=true) matched");
171+
}
172+
173+
@Test
174+
void conditionReportWhenDoesNotMatch() {
175+
load(Defaults.class, "test=false");
176+
assertThat(this.context.containsBean("foo")).isFalse();
177+
assertThat(getConditionEvaluationReport())
178+
.contains("@ConditionalOnBooleanProperty (test=true) found different value in property 'test'");
179+
}
180+
181+
private <T extends Exception> Consumer<T> causeMessageContaining(String message) {
182+
return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message);
183+
}
184+
185+
private String getConditionEvaluationReport() {
186+
return ConditionEvaluationReport.get(this.context.getBeanFactory())
187+
.getConditionAndOutcomesBySource()
188+
.values()
189+
.stream()
190+
.flatMap(ConditionAndOutcomes::stream)
191+
.map(Object::toString)
192+
.collect(Collectors.joining("\n"));
193+
}
194+
147195
private void load(Class<?> config, String... environment) {
148196
TestPropertyValues.of(environment).applyTo(this.environment);
149197
this.context = new SpringApplicationBuilder(config).environment(this.environment)
@@ -196,4 +244,26 @@ static class WithPrefix extends BeanConfiguration {
196244

197245
}
198246

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

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

+27-1
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.
@@ -21,11 +21,13 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323
import java.util.function.Consumer;
24+
import java.util.stream.Collectors;
2425

2526
import org.junit.jupiter.api.AfterEach;
2627
import org.junit.jupiter.api.Test;
2728

2829
import org.springframework.boot.WebApplicationType;
30+
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
2931
import org.springframework.boot.builder.SpringApplicationBuilder;
3032
import org.springframework.boot.test.util.TestPropertyValues;
3133
import org.springframework.context.ConfigurableApplicationContext;
@@ -271,13 +273,37 @@ void metaAndDirectAnnotationWithAliasConditionMatchesWhenBothPropertiesAreSet()
271273
assertThat(this.context.containsBean("foo")).isTrue();
272274
}
273275

276+
@Test
277+
void conditionReportWhenMatched() {
278+
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2");
279+
assertThat(this.context.containsBean("foo")).isTrue();
280+
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched");
281+
}
282+
283+
@Test
284+
void conditionReportWhenDoesNotMatch() {
285+
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1");
286+
assertThat(getConditionEvaluationReport())
287+
.contains("@ConditionalOnProperty ([property1,property2]) did not find property 'property2'");
288+
}
289+
274290
private void load(Class<?> config, String... environment) {
275291
TestPropertyValues.of(environment).applyTo(this.environment);
276292
this.context = new SpringApplicationBuilder(config).environment(this.environment)
277293
.web(WebApplicationType.NONE)
278294
.run();
279295
}
280296

297+
private String getConditionEvaluationReport() {
298+
return ConditionEvaluationReport.get(this.context.getBeanFactory())
299+
.getConditionAndOutcomesBySource()
300+
.values()
301+
.stream()
302+
.flatMap(ConditionAndOutcomes::stream)
303+
.map(Object::toString)
304+
.collect(Collectors.joining("\n"));
305+
}
306+
281307
@Configuration(proxyBeanMethods = false)
282308
@ConditionalOnProperty(name = { "property1", "property2" })
283309
static class MultiplePropertiesRequiredConfiguration {

0 commit comments

Comments
 (0)