Skip to content

Commit a13fe0b

Browse files
committedJan 29, 2025·
Make @ConditionalOn[Boolean]Property @Repeatable
Update `ConditionalOnProperty`, `ConditionalOnBooleanProperty` and `OnPropertyCondition` to support `@Repeatable`. Closes gh-2541
1 parent f32b29e commit a13fe0b

File tree

7 files changed

+190
-7
lines changed

7 files changed

+190
-7
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.condition;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.context.annotation.Conditional;
26+
27+
/**
28+
* Container annotation that aggregates several
29+
* {@link ConditionalOnProperty @ConditionalOnProperty} annotations.
30+
*
31+
* @author Phillip Webb
32+
* @since 3.5.0
33+
* @see ConditionalOnBooleanProperty
34+
*/
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Target({ ElementType.TYPE, ElementType.METHOD })
37+
@Documented
38+
@Conditional(OnPropertyCondition.class)
39+
public @interface ConditionalOnBooleanProperties {
40+
41+
/**
42+
* Return the contained
43+
* {@link ConditionalOnBooleanProperty @ConditionalOnBooleanProperty} annotations.
44+
* @return the contained annotations
45+
*/
46+
ConditionalOnBooleanProperty[] value();
47+
48+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Documented;
2020
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Repeatable;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
@@ -43,6 +44,7 @@
4344
@Target({ ElementType.TYPE, ElementType.METHOD })
4445
@Documented
4546
@Conditional(OnPropertyCondition.class)
47+
@Repeatable(ConditionalOnBooleanProperties.class)
4648
public @interface ConditionalOnBooleanProperty {
4749

4850
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.condition;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.context.annotation.Conditional;
26+
27+
/**
28+
* Container annotation that aggregates several
29+
* {@link ConditionalOnProperty @ConditionalOnProperty} annotations.
30+
*
31+
* @author Phillip Webb
32+
* @since 3.5.0
33+
* @see ConditionalOnProperty
34+
*/
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Target({ ElementType.TYPE, ElementType.METHOD })
37+
@Documented
38+
@Conditional(OnPropertyCondition.class)
39+
public @interface ConditionalOnProperties {
40+
41+
/**
42+
* Return the contained {@link ConditionalOnProperty @ConditionalOnProperty}
43+
* annotations.
44+
* @return the contained annotations
45+
*/
46+
ConditionalOnProperty[] value();
47+
48+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Documented;
2020
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Repeatable;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
@@ -94,6 +95,7 @@
9495
@Target({ ElementType.TYPE, ElementType.METHOD })
9596
@Documented
9697
@Conditional(OnPropertyCondition.class)
98+
@Repeatable(ConditionalOnProperties.class)
9799
public @interface ConditionalOnProperty {
98100

99101
/**

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

+32-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.util.ArrayList;
21+
import java.util.Arrays;
2122
import java.util.List;
2223
import java.util.stream.Stream;
2324

@@ -28,6 +29,7 @@
2829
import org.springframework.core.annotation.AnnotationAttributes;
2930
import org.springframework.core.annotation.MergedAnnotation;
3031
import org.springframework.core.annotation.MergedAnnotationPredicates;
32+
import org.springframework.core.annotation.MergedAnnotations;
3133
import org.springframework.core.annotation.Order;
3234
import org.springframework.core.env.PropertyResolver;
3335
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -50,11 +52,8 @@ class OnPropertyCondition extends SpringBootCondition {
5052

5153
@Override
5254
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
53-
List<MergedAnnotation<Annotation>> annotations = Stream
54-
.concat(metadata.getAnnotations().stream(ConditionalOnProperty.class.getName()),
55-
metadata.getAnnotations().stream(ConditionalOnBooleanProperty.class.getName()))
56-
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
57-
.toList();
55+
MergedAnnotations mergedAnnotations = metadata.getAnnotations();
56+
List<MergedAnnotation<Annotation>> annotations = stream(mergedAnnotations).toList();
5857
List<ConditionMessage> noMatch = new ArrayList<>();
5958
List<ConditionMessage> match = new ArrayList<>();
6059
for (MergedAnnotation<Annotation> annotation : annotations) {
@@ -67,6 +66,34 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
6766
return ConditionOutcome.match(ConditionMessage.of(match));
6867
}
6968

69+
private Stream<MergedAnnotation<Annotation>> stream(MergedAnnotations mergedAnnotations) {
70+
return Stream.concat(stream(mergedAnnotations, ConditionalOnProperty.class, ConditionalOnProperties.class),
71+
stream(mergedAnnotations, ConditionalOnBooleanProperty.class, ConditionalOnBooleanProperties.class));
72+
}
73+
74+
private Stream<MergedAnnotation<Annotation>> stream(MergedAnnotations mergedAnnotations,
75+
Class<? extends Annotation> type, Class<? extends Annotation> containerType) {
76+
return Stream.concat(stream(mergedAnnotations, type), streamRepeated(mergedAnnotations, type, containerType));
77+
}
78+
79+
private Stream<MergedAnnotation<Annotation>> streamRepeated(MergedAnnotations mergedAnnotations,
80+
Class<? extends Annotation> type, Class<? extends Annotation> containerType) {
81+
return stream(mergedAnnotations, containerType).flatMap((container) -> streamRepeated(container, type));
82+
}
83+
84+
@SuppressWarnings("unchecked")
85+
private Stream<MergedAnnotation<Annotation>> streamRepeated(MergedAnnotation<Annotation> container,
86+
Class<? extends Annotation> type) {
87+
MergedAnnotation<? extends Annotation>[] repeated = container.getAnnotationArray(MergedAnnotation.VALUE, type);
88+
return Arrays.stream((MergedAnnotation<Annotation>[]) repeated);
89+
}
90+
91+
private Stream<MergedAnnotation<Annotation>> stream(MergedAnnotations annotations,
92+
Class<? extends Annotation> containerType) {
93+
return annotations.stream(containerType.getName())
94+
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes));
95+
}
96+
7097
private ConditionOutcome determineOutcome(MergedAnnotation<Annotation> annotation, PropertyResolver resolver) {
7198
Class<Annotation> annotationType = annotation.getType();
7299
Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes());

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

+28
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,22 @@ void conditionReportWhenDoesNotMatch() {
178178
.contains("@ConditionalOnBooleanProperty (test=true) found different value in property 'test'");
179179
}
180180

181+
@Test
182+
void repeatablePropertiesConditionReportWhenMatched() {
183+
load(RepeatablePropertiesRequiredConfiguration.class, "property1=true", "property2=true");
184+
assertThat(this.context.containsBean("foo")).isTrue();
185+
String report = getConditionEvaluationReport();
186+
assertThat(report).contains("@ConditionalOnBooleanProperty (property1=true) matched");
187+
assertThat(report).contains("@ConditionalOnBooleanProperty (property2=true) matched");
188+
}
189+
190+
@Test
191+
void repeatablePropertiesConditionReportWhenDoesNotMatch() {
192+
load(RepeatablePropertiesRequiredConfiguration.class, "property1=true");
193+
assertThat(getConditionEvaluationReport())
194+
.contains("@ConditionalOnBooleanProperty (property2=true) did not find property 'property2'");
195+
}
196+
181197
private <T extends Exception> Consumer<T> causeMessageContaining(String message) {
182198
return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message);
183199
}
@@ -266,4 +282,16 @@ String foo() {
266282

267283
}
268284

285+
@Configuration(proxyBeanMethods = false)
286+
@ConditionalOnBooleanProperty("property1")
287+
@ConditionalOnBooleanProperty("property2")
288+
static class RepeatablePropertiesRequiredConfiguration {
289+
290+
@Bean
291+
String foo() {
292+
return "foo";
293+
}
294+
295+
}
296+
269297
}

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

+30-2
Original file line numberDiff line numberDiff line change
@@ -274,19 +274,35 @@ void metaAndDirectAnnotationWithAliasConditionMatchesWhenBothPropertiesAreSet()
274274
}
275275

276276
@Test
277-
void conditionReportWhenMatched() {
277+
void multiplePropertiesConditionReportWhenMatched() {
278278
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2");
279279
assertThat(this.context.containsBean("foo")).isTrue();
280280
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched");
281281
}
282282

283283
@Test
284-
void conditionReportWhenDoesNotMatch() {
284+
void multiplePropertiesConditionReportWhenDoesNotMatch() {
285285
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1");
286286
assertThat(getConditionEvaluationReport())
287287
.contains("@ConditionalOnProperty ([property1,property2]) did not find property 'property2'");
288288
}
289289

290+
@Test
291+
void repeatablePropertiesConditionReportWhenMatched() {
292+
load(RepeatablePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2");
293+
assertThat(this.context.containsBean("foo")).isTrue();
294+
String report = getConditionEvaluationReport();
295+
assertThat(report).contains("@ConditionalOnProperty (property1) matched");
296+
assertThat(report).contains("@ConditionalOnProperty (property2) matched");
297+
}
298+
299+
@Test
300+
void repeatablePropertiesConditionReportWhenDoesNotMatch() {
301+
load(RepeatablePropertiesRequiredConfiguration.class, "property1=value1");
302+
assertThat(getConditionEvaluationReport())
303+
.contains("@ConditionalOnProperty (property2) did not find property 'property2'");
304+
}
305+
290306
private void load(Class<?> config, String... environment) {
291307
TestPropertyValues.of(environment).applyTo(this.environment);
292308
this.context = new SpringApplicationBuilder(config).environment(this.environment)
@@ -315,6 +331,18 @@ String foo() {
315331

316332
}
317333

334+
@Configuration(proxyBeanMethods = false)
335+
@ConditionalOnProperty("property1")
336+
@ConditionalOnProperty("property2")
337+
static class RepeatablePropertiesRequiredConfiguration {
338+
339+
@Bean
340+
String foo() {
341+
return "foo";
342+
}
343+
344+
}
345+
318346
@Configuration(proxyBeanMethods = false)
319347
@ConditionalOnProperty(prefix = "spring.", name = "the-relaxed-property")
320348
static class RelaxedPropertiesRequiredConfiguration {

0 commit comments

Comments
 (0)