Merge pull request #43754 from nosan
* pr/43754: Polish OnPropertyCondition Polish OnPropertyCondition Closes gh-43754
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user