From a13fe0b565c164f44c55ca326b2100fa29b614b5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 28 Jan 2025 20:35:45 -0800 Subject: [PATCH] Make `@ConditionalOn[Boolean]Property` `@Repeatable` Update `ConditionalOnProperty`, `ConditionalOnBooleanProperty` and `OnPropertyCondition` to support `@Repeatable`. Closes gh-2541 --- .../ConditionalOnBooleanProperties.java | 48 +++++++++++++++++++ .../ConditionalOnBooleanProperty.java | 2 + .../condition/ConditionalOnProperties.java | 48 +++++++++++++++++++ .../condition/ConditionalOnProperty.java | 2 + .../condition/OnPropertyCondition.java | 37 ++++++++++++-- .../ConditionalOnBooleanPropertyTests.java | 28 +++++++++++ .../condition/ConditionalOnPropertyTests.java | 32 ++++++++++++- 7 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperties.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperties.java new file mode 100644 index 0000000000..b44eeea7d3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperties.java @@ -0,0 +1,48 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * Container annotation that aggregates several + * {@link ConditionalOnProperty @ConditionalOnProperty} annotations. + * + * @author Phillip Webb + * @since 3.5.0 + * @see ConditionalOnBooleanProperty + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@Conditional(OnPropertyCondition.class) +public @interface ConditionalOnBooleanProperties { + + /** + * Return the contained + * {@link ConditionalOnBooleanProperty @ConditionalOnBooleanProperty} annotations. + * @return the contained annotations + */ + ConditionalOnBooleanProperty[] value(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperty.java index 84213f5cc6..a53e95aee6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperty.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperty.java @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.condition; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -43,6 +44,7 @@ import org.springframework.core.env.Environment; @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(OnPropertyCondition.class) +@Repeatable(ConditionalOnBooleanProperties.class) public @interface ConditionalOnBooleanProperty { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperties.java new file mode 100644 index 0000000000..c93a7b4ea9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperties.java @@ -0,0 +1,48 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * Container annotation that aggregates several + * {@link ConditionalOnProperty @ConditionalOnProperty} annotations. + * + * @author Phillip Webb + * @since 3.5.0 + * @see ConditionalOnProperty + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@Conditional(OnPropertyCondition.class) +public @interface ConditionalOnProperties { + + /** + * Return the contained {@link ConditionalOnProperty @ConditionalOnProperty} + * annotations. + * @return the contained annotations + */ + ConditionalOnProperty[] value(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java index 6f60c4f5b2..3b72400a4b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.condition; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -94,6 +95,7 @@ import org.springframework.core.env.Environment; @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(OnPropertyCondition.class) +@Repeatable(ConditionalOnProperties.class) public @interface ConditionalOnProperty { /** 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 99df172acd..694e6e9e6b 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 @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.condition; import java.lang.annotation.Annotation; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -28,6 +29,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotationPredicates; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.Order; import org.springframework.core.env.PropertyResolver; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -50,11 +52,8 @@ class OnPropertyCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - List> annotations = Stream - .concat(metadata.getAnnotations().stream(ConditionalOnProperty.class.getName()), - metadata.getAnnotations().stream(ConditionalOnBooleanProperty.class.getName())) - .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) - .toList(); + MergedAnnotations mergedAnnotations = metadata.getAnnotations(); + List> annotations = stream(mergedAnnotations).toList(); List noMatch = new ArrayList<>(); List match = new ArrayList<>(); for (MergedAnnotation annotation : annotations) { @@ -67,6 +66,34 @@ class OnPropertyCondition extends SpringBootCondition { return ConditionOutcome.match(ConditionMessage.of(match)); } + private Stream> stream(MergedAnnotations mergedAnnotations) { + return Stream.concat(stream(mergedAnnotations, ConditionalOnProperty.class, ConditionalOnProperties.class), + stream(mergedAnnotations, ConditionalOnBooleanProperty.class, ConditionalOnBooleanProperties.class)); + } + + private Stream> stream(MergedAnnotations mergedAnnotations, + Class type, Class containerType) { + return Stream.concat(stream(mergedAnnotations, type), streamRepeated(mergedAnnotations, type, containerType)); + } + + private Stream> streamRepeated(MergedAnnotations mergedAnnotations, + Class type, Class containerType) { + return stream(mergedAnnotations, containerType).flatMap((container) -> streamRepeated(container, type)); + } + + @SuppressWarnings("unchecked") + private Stream> streamRepeated(MergedAnnotation container, + Class type) { + MergedAnnotation[] repeated = container.getAnnotationArray(MergedAnnotation.VALUE, type); + return Arrays.stream((MergedAnnotation[]) repeated); + } + + private Stream> stream(MergedAnnotations annotations, + Class containerType) { + return annotations.stream(containerType.getName()) + .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)); + } + private ConditionOutcome determineOutcome(MergedAnnotation annotation, PropertyResolver resolver) { Class annotationType = annotation.getType(); Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes()); 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 32bb1f2744..68b43ac685 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 @@ -178,6 +178,22 @@ class ConditionalOnBooleanPropertyTests { .contains("@ConditionalOnBooleanProperty (test=true) found different value in property 'test'"); } + @Test + void repeatablePropertiesConditionReportWhenMatched() { + load(RepeatablePropertiesRequiredConfiguration.class, "property1=true", "property2=true"); + assertThat(this.context.containsBean("foo")).isTrue(); + String report = getConditionEvaluationReport(); + assertThat(report).contains("@ConditionalOnBooleanProperty (property1=true) matched"); + assertThat(report).contains("@ConditionalOnBooleanProperty (property2=true) matched"); + } + + @Test + void repeatablePropertiesConditionReportWhenDoesNotMatch() { + load(RepeatablePropertiesRequiredConfiguration.class, "property1=true"); + assertThat(getConditionEvaluationReport()) + .contains("@ConditionalOnBooleanProperty (property2=true) did not find property 'property2'"); + } + private Consumer causeMessageContaining(String message) { return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message); } @@ -266,4 +282,16 @@ class ConditionalOnBooleanPropertyTests { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnBooleanProperty("property1") + @ConditionalOnBooleanProperty("property2") + static class RepeatablePropertiesRequiredConfiguration { + + @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 9df542bae1..e371d52d1f 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 @@ -274,19 +274,35 @@ class ConditionalOnPropertyTests { } @Test - void conditionReportWhenMatched() { + void multiplePropertiesConditionReportWhenMatched() { load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2"); assertThat(this.context.containsBean("foo")).isTrue(); assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched"); } @Test - void conditionReportWhenDoesNotMatch() { + void multiplePropertiesConditionReportWhenDoesNotMatch() { load(MultiplePropertiesRequiredConfiguration.class, "property1=value1"); assertThat(getConditionEvaluationReport()) .contains("@ConditionalOnProperty ([property1,property2]) did not find property 'property2'"); } + @Test + void repeatablePropertiesConditionReportWhenMatched() { + load(RepeatablePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2"); + assertThat(this.context.containsBean("foo")).isTrue(); + String report = getConditionEvaluationReport(); + assertThat(report).contains("@ConditionalOnProperty (property1) matched"); + assertThat(report).contains("@ConditionalOnProperty (property2) matched"); + } + + @Test + void repeatablePropertiesConditionReportWhenDoesNotMatch() { + load(RepeatablePropertiesRequiredConfiguration.class, "property1=value1"); + assertThat(getConditionEvaluationReport()) + .contains("@ConditionalOnProperty (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) @@ -315,6 +331,18 @@ class ConditionalOnPropertyTests { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty("property1") + @ConditionalOnProperty("property2") + static class RepeatablePropertiesRequiredConfiguration { + + @Bean + String foo() { + return "foo"; + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.", name = "the-relaxed-property") static class RelaxedPropertiesRequiredConfiguration {