Merge pull request #43754 from nosan

* pr/43754:
  Polish OnPropertyCondition
  Polish OnPropertyCondition

Closes gh-43754
This commit is contained in:
Phillip Webb
2025-01-13 14:35:53 -08:00
4 changed files with 131 additions and 22 deletions

View File

@@ -27,6 +27,8 @@ import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@@ -237,6 +239,15 @@ public final class ConditionEvaluationReport {
return true;
}
/**
* Return a {@link Stream} of the {@link ConditionAndOutcome} items.
* @return a stream of the {@link ConditionAndOutcome} items.
* @since 3.5.0
*/
public Stream<ConditionAndOutcome> stream() {
return StreamSupport.stream(spliterator(), false);
}
@Override
public Iterator<ConditionAndOutcome> iterator() {
return Collections.unmodifiableSet(this.outcomes).iterator();

View File

@@ -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;
@@ -28,11 +28,11 @@ 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;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -43,23 +43,22 @@ import org.springframework.util.StringUtils;
* @author Stephane Nicoll
* @author Andy Wilkinson
* @see ConditionalOnProperty
* @see ConditionalOnBooleanProperty
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
class OnPropertyCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
MergedAnnotations annotations = metadata.getAnnotations();
List<AnnotationAttributes> allAnnotationAttributes = Stream
.concat(annotations.stream(ConditionalOnProperty.class.getName()),
annotations.stream(ConditionalOnBooleanProperty.class.getName()))
List<MergedAnnotation<Annotation>> annotations = Stream
.concat(metadata.getAnnotations().stream(ConditionalOnProperty.class.getName()),
metadata.getAnnotations().stream(ConditionalOnBooleanProperty.class.getName()))
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
.map(MergedAnnotation::asAnnotationAttributes)
.toList();
List<ConditionMessage> noMatch = new ArrayList<>();
List<ConditionMessage> match = new ArrayList<>();
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
for (MergedAnnotation<Annotation> annotation : annotations) {
ConditionOutcome outcome = determineOutcome(annotation, context.getEnvironment());
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
}
if (!noMatch.isEmpty()) {
@@ -68,27 +67,29 @@ class OnPropertyCondition extends SpringBootCondition {
return ConditionOutcome.match(ConditionMessage.of(match));
}
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) {
Spec spec = new Spec(annotationAttributes);
private ConditionOutcome determineOutcome(MergedAnnotation<Annotation> annotation, PropertyResolver resolver) {
Class<Annotation> annotationType = annotation.getType();
Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes());
List<String> missingProperties = new ArrayList<>();
List<String> 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<? extends Annotation> annotationType;
private final String prefix;
private final String[] names;
@@ -97,7 +98,8 @@ class OnPropertyCondition extends SpringBootCondition {
private final boolean matchIfMissing;
Spec(AnnotationAttributes annotationAttributes) {
Spec(Class<? extends Annotation> 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 +114,13 @@ class OnPropertyCondition extends SpringBootCondition {
return prefix;
}
private String[] getNames(Map<String, Object> 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;
}

View File

@@ -16,10 +16,14 @@
package org.springframework.boot.autoconfigure.condition;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
@@ -29,6 +33,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
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 +149,49 @@ class ConditionalOnBooleanPropertyTests {
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 <T extends Exception> Consumer<T> causeMessageContaining(String message) {
return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message);
}
private String getConditionEvaluationReport() {
return ConditionEvaluationReport.get(this.context.getBeanFactory())
.getConditionAndOutcomesBySource()
.values()
.stream()
.flatMap(ConditionAndOutcomes::stream)
.map(Object::toString)
.collect(Collectors.joining("\n"));
}
private void load(Class<?> config, String... environment) {
TestPropertyValues.of(environment).applyTo(this.environment);
this.context = new SpringApplicationBuilder(config).environment(this.environment)
@@ -196,4 +244,26 @@ class ConditionalOnBooleanPropertyTests {
}
@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";
}
}
}

View File

@@ -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.
@@ -21,11 +21,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
@@ -271,6 +273,20 @@ class ConditionalOnPropertyTests {
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 +294,16 @@ class ConditionalOnPropertyTests {
.run();
}
private String getConditionEvaluationReport() {
return ConditionEvaluationReport.get(this.context.getBeanFactory())
.getConditionAndOutcomesBySource()
.values()
.stream()
.flatMap(ConditionAndOutcomes::stream)
.map(Object::toString)
.collect(Collectors.joining("\n"));
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = { "property1", "property2" })
static class MultiplePropertiesRequiredConfiguration {