Polish contribution

This commit applies the same rules to ConfigurationPropertiesSource.

See gh-50367
This commit is contained in:
Stéphane Nicoll
2026-07-15 11:37:31 +02:00
parent 87c4755a86
commit 10fdd20606
11 changed files with 316 additions and 28 deletions
@@ -87,6 +87,8 @@ public abstract class ArchitectureCheck extends DefaultTask {
.classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONDITIONAL_ON_MISSING_BEAN))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation
.classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation
.classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE))));
getRules().addAll(whenMainSources(
() -> ArchitectureRules.configurationPropertiesBinding(ArchitectureCheckAnnotation.classFor(
getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING))));
@@ -42,6 +42,11 @@ public enum ArchitectureCheckAnnotation {
*/
CONFIGURATION_PROPERTIES,
/**
* Configuration properties source.
*/
CONFIGURATION_PROPERTIES_SOURCE,
/**
* Deprecated configuration property.
*/
@@ -56,6 +61,8 @@ public enum ArchitectureCheckAnnotation {
"org.springframework.boot.autoconfigure.condition.ConditionalOnClass", CONDITIONAL_ON_MISSING_BEAN.name(),
"org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean",
CONFIGURATION_PROPERTIES.name(), "org.springframework.boot.context.properties.ConfigurationProperties",
CONFIGURATION_PROPERTIES_SOURCE.name(),
"org.springframework.boot.context.properties.ConfigurationPropertiesSource",
DEPRECATED_CONFIGURATION_PROPERTY.name(),
"org.springframework.boot.context.properties.DeprecatedConfigurationProperty",
CONFIGURATION_PROPERTIES_BINDING.name(),
@@ -131,10 +131,10 @@ final class ArchitectureRules {
static List<ArchRule> configurationProperties(String annotationClass) {
return List.of(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass),
methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass),
initializedConfigurationPropertiesShouldUse("java.util.Set", "java.util.LinkedHashSet",
annotationClass),
initializedConfigurationPropertiesShouldUse("java.util.Map",
List.of("java.util.LinkedHashMap", "java.util.EnumMap"), annotationClass));
configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Set",
List.of("java.util.LinkedHashSet")),
configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Map",
List.of("java.util.LinkedHashMap", "java.util.EnumMap")));
}
static List<ArchRule> configurationPropertiesBinding(String annotationClass) {
@@ -436,42 +436,36 @@ final class ArchitectureRules {
.allowEmptyShould(true);
}
private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType, String implementationType,
String annotationClass) {
return initializedConfigurationPropertiesShouldUse(propertyType, List.of(implementationType), annotationClass);
}
private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType,
List<String> implementationTypes, String annotationClass) {
private static ArchRule configurationPropertyOfTypeShouldUseImplementation(String annotationClass,
String propertyType, List<String> allowedImplementations) {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith(annotationClass)
.or(areNestedInConfigurationPropertiesClasses(annotationClass))
.should(useImplementationForInitializedProperties(propertyType, implementationTypes))
.because("@ConfigurationProperties classes should preserve the property type's expected implementation")
.or(areNestedInClassAnnotatedWith(annotationClass))
.should(useAllowedImplementationForProperties(propertyType, allowedImplementations))
.allowEmptyShould(true);
}
private static ArchCondition<JavaClass> useImplementationForInitializedProperties(String propertyType,
List<String> implementationTypes) {
String description = implementationTypes.stream().collect(Collectors.joining(" or "));
return check("use %s for initialized %s properties".formatted(description, propertyType),
private static ArchCondition<JavaClass> useAllowedImplementationForProperties(String propertyType,
List<String> allowedImplementations) {
return check(
"use %s for properties of type %s".formatted(String.join(" or ", allowedImplementations), propertyType),
(javaClass, events) -> javaClass.getFields()
.stream()
.filter((field) -> propertyType.equals(field.getRawType().getName()))
.forEach((field) -> checkPropertyInitializer(field, implementationTypes, events)));
.forEach((field) -> checkPropertyImplementation(field, allowedImplementations, events)));
}
private static void checkPropertyInitializer(JavaField field, List<String> implementationTypes,
private static void checkPropertyImplementation(JavaField field, List<String> allowedImplementations,
ConditionEvents events) {
String description = implementationTypes.stream().collect(Collectors.joining(" or "));
field.getAccessesToSelf()
.stream()
.filter((access) -> access.getAccessType() == AccessType.SET)
.flatMap((access) -> initializerConstructorCalls(access).stream())
.filter((call) -> !implementationTypes.contains(call.getTargetOwner().getName()))
.forEach((call) -> addViolation(events, field, "%s should be initialized with %s instead of %s"
.formatted(field.getDescription(), description, call.getTargetOwner().getName())));
.filter((call) -> !allowedImplementations.contains(call.getTargetOwner().getName()))
.forEach((call) -> addViolation(events, field,
"%s should be initialized with %s instead of %s".formatted(field.getDescription(),
String.join(" or ", allowedImplementations), call.getTargetOwner().getName())));
}
private static List<JavaConstructorCall> initializerConstructorCalls(JavaFieldAccess fieldAccess) {
@@ -482,11 +476,11 @@ final class ArchitectureRules {
.toList();
}
private static DescribedPredicate<JavaClass> areNestedInConfigurationPropertiesClasses(String annotationClass) {
return DescribedPredicate.describe("are nested in @ConfigurationProperties",
private static DescribedPredicate<JavaClass> areNestedInClassAnnotatedWith(String annotationClass) {
return DescribedPredicate.describe("one of its inner class",
(javaClass) -> javaClass.getEnclosingClass()
.map((enclosing) -> enclosing.isAnnotatedWith(annotationClass)
|| areNestedInConfigurationPropertiesClasses(annotationClass).test(enclosing))
|| areNestedInClassAnnotatedWith(annotationClass).test(enclosing))
.orElse(false));
}
@@ -47,6 +47,7 @@ import org.springframework.boot.build.architecture.annotations.TestConditionalOn
import org.springframework.boot.build.architecture.annotations.TestConditionalOnMissingBean;
import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesBinding;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileSystemUtils;
@@ -339,6 +340,13 @@ class ArchitectureCheckTests {
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap");
}
@Test
void whenConfigurationPropertiesSourceUsesHashMapShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashmap", "annotations");
buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap");
}
@Test
void whenConfigurationPropertiesUsesHashSetShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashset", "annotations");
@@ -346,6 +354,13 @@ class ArchitectureCheckTests {
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet");
}
@Test
void whenConfigurationPropertiesSourceUsesHashSetShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashset", "annotations");
buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet");
}
@Test
void whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashmap", "annotations");
@@ -353,6 +368,13 @@ class ArchitectureCheckTests {
Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenConfigurationPropertiesSourceUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashmap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenConfigurationPropertiesUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/enummap", "annotations");
@@ -360,6 +382,13 @@ class ArchitectureCheckTests {
Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenConfigurationPropertiesSourceUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/enummap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashset", "annotations");
@@ -367,6 +396,13 @@ class ArchitectureCheckTests {
Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenConfigurationPropertiesSourceUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashset", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenConfigurationPropertiesBindingBeanMethodIsNotStaticShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/bindingnonstatic", "annotations");
@@ -604,6 +640,12 @@ class ArchitectureCheckTests {
return this;
}
GradleBuild withConfigurationPropertiesSourceAnnotation() {
configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE.name(),
TestConfigurationPropertiesSource.class.getName());
return this;
}
GradleBuild withConfigurationPropertiesBindingAnnotation() {
configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING.name(),
TestConfigurationPropertiesBinding.class.getName());
@@ -0,0 +1,28 @@
/*
* Copyright 2012-present 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.build.architecture.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TestConfigurationPropertiesSource {
}
@@ -0,0 +1,48 @@
/*
* Copyright 2012-present 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.build.architecture.configurationpropertiessource.enummap;
import java.util.EnumMap;
import java.util.Map;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
/**
* Test {@link TestConfigurationPropertiesSource} using {@link EnumMap}.
*
* @author Stephane Nicoll
*/
@TestConfigurationPropertiesSource
public class ConfigurationPropertiesWithEnumMap {
private Map<Example, String> properties = new EnumMap<>(Example.class);
public Map<Example, String> getProperties() {
return this.properties;
}
public void setProperties(Map<Example, String> properties) {
this.properties = properties;
}
enum Example {
ONE
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2012-present 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.build.architecture.configurationpropertiessource.hashmap;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
/**
* Test {@link TestConfigurationPropertiesSource} using {@link HashMap}.
*
* @author Stephane Nicoll
*/
@TestConfigurationPropertiesSource
public class ConfigurationPropertiesWithHashMap {
private Map<String, String> properties = new HashMap<>();
public Map<String, String> getProperties() {
return this.properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2012-present 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.build.architecture.configurationpropertiessource.hashset;
import java.util.HashSet;
import java.util.Set;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
/**
* Test {@link TestConfigurationPropertiesSource} using {@link HashSet}.
*
* @author Stephane Nicoll
*/
@TestConfigurationPropertiesSource
public class ConfigurationPropertiesWithHashSet {
private Set<String> items = new HashSet<>();
public Set<String> getItems() {
return this.items;
}
public void setItems(Set<String> items) {
this.items = items;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2012-present 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.build.architecture.configurationpropertiessource.linkedhashmap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
/**
* Test {@link TestConfigurationPropertiesSource} using {@link LinkedHashMap}.
*
* @author Stephane Nicoll
*/
@TestConfigurationPropertiesSource
public class ConfigurationPropertiesWithLinkedHashMap {
private Map<String, String> properties = new LinkedHashMap<>();
public Map<String, String> getProperties() {
return this.properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2012-present 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.build.architecture.configurationpropertiessource.linkedhashset;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
/**
* Test {@link TestConfigurationPropertiesSource} using {@link LinkedHashSet}.
*
* @author Stephane Nicoll
*/
@TestConfigurationPropertiesSource
public class ConfigurationPropertiesWithLinkedHashSet {
private Set<String> items = new LinkedHashSet<>();
public Set<String> getItems() {
return this.items;
}
public void setItems(Set<String> items) {
this.items = items;
}
}
@@ -59,7 +59,6 @@ import org.springframework.util.unit.DataSize;
* @author Andy Wilkinson
* @author Scott Frederick
* @author Yanming Zhou
* @author Venkata Naga Sai Srikanth Gollapudi
* @since 4.0.0
*/
@ConfigurationProperties("spring.kafka")