Skip to content

Commit 3d23261

Browse files
committed
Merge branch '3.5.x'
Closes gh-48122
2 parents 864252d + b50a2d0 commit 3d23261

File tree

56 files changed

+1297
-580
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1297
-580
lines changed

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.ArrayList;
2727
import java.util.Collections;
2828
import java.util.List;
29+
import java.util.Map;
2930
import java.util.concurrent.Callable;
3031
import java.util.function.Supplier;
3132
import java.util.stream.Stream;
@@ -42,6 +43,7 @@
4243
import org.gradle.api.file.FileCollection;
4344
import org.gradle.api.file.FileTree;
4445
import org.gradle.api.provider.ListProperty;
46+
import org.gradle.api.provider.MapProperty;
4547
import org.gradle.api.provider.Property;
4648
import org.gradle.api.provider.Provider;
4749
import org.gradle.api.provider.SetProperty;
@@ -72,19 +74,27 @@
7274
*/
7375
public abstract class ArchitectureCheck extends DefaultTask {
7476

77+
static final String CONDITIONAL_ON_CLASS = "ConditionalOnClass";
78+
79+
static final String DEPRECATED_CONFIGURATION_PROPERTY = "DeprecatedConfigurationProperty";
80+
7581
private static final String CONDITIONAL_ON_CLASS_ANNOTATION = "org.springframework.boot.autoconfigure.condition.ConditionalOnClass";
7682

83+
private static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty";
84+
7785
private FileCollection classes;
7886

7987
public ArchitectureCheck() {
8088
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
81-
getConditionalOnClassAnnotation().convention(CONDITIONAL_ON_CLASS_ANNOTATION);
89+
getAnnotationClasses().convention(Map.of(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION,
90+
DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION));
8291
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
8392
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
8493
getRules().addAll(ArchitectureRules.standard());
85-
getRules().addAll(whenMainSources(() -> List
86-
.of(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType(), ArchitectureRules
87-
.allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(getConditionalOnClassAnnotation().get()))));
94+
getRules().addAll(whenMainSources(() -> ArchitectureRules
95+
.beanMethods(annotationClassFor(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION))));
96+
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(
97+
annotationClassFor(DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION))));
8898
getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
8999
ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
90100
getRuleDescriptions().set(getRules().map(this::asDescriptions));
@@ -110,6 +120,10 @@ private List<String> asDescriptions(List<ArchRule> rules) {
110120
return rules.stream().map(ArchRule::getDescription).toList();
111121
}
112122

123+
private String annotationClassFor(String name, String defaultValue) {
124+
return getAnnotationClasses().get().getOrDefault(name, defaultValue);
125+
}
126+
113127
@TaskAction
114128
void checkArchitecture() throws Exception {
115129
withCompileClasspath(() -> {
@@ -209,7 +223,7 @@ final FileTree getInputClasses() {
209223
@Internal
210224
abstract SetProperty<String> getNullMarkedIgnoredPackages();
211225

212-
@Internal
213-
abstract Property<String> getConditionalOnClassAnnotation();
226+
@Input
227+
abstract MapProperty<String, String> getAnnotationClasses();
214228

215229
}

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,16 @@ static List<ArchRule> standard() {
116116
return List.copyOf(rules);
117117
}
118118

119-
static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
119+
static List<ArchRule> beanMethods(String annotationName) {
120+
return List.of(allBeanMethodsShouldReturnNonPrivateType(),
121+
allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(annotationName));
122+
}
123+
124+
static List<ArchRule> configurationProperties(String annotationName) {
125+
return List.of(allDeprecatedConfigurationPropertiesShouldIncludeSince(annotationName));
126+
}
127+
128+
private static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
120129
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should(check(
121130
"not return types declared with the %s modifier, as such types are incompatible with Spring AOT processing"
122131
.formatted(JavaModifier.PRIVATE),
@@ -130,7 +139,7 @@ static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
130139
.allowEmptyShould(true);
131140
}
132141

133-
static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
142+
private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
134143
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should()
135144
.notBeAnnotatedWith(annotationName)
136145
.because("@ConditionalOnClass on @Bean methods is ineffective - it doesn't prevent "
@@ -374,6 +383,20 @@ private static ArchRule allConfigurationPropertiesBindingBeanMethodsShouldBeStat
374383
.allowEmptyShould(true);
375384
}
376385

386+
private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(String annotationName) {
387+
return methodsThatAreAnnotatedWith(annotationName)
388+
.should(check("include a non-empty 'since' attribute", (method, events) -> {
389+
JavaAnnotation<JavaMethod> annotation = method.getAnnotationOfType(annotationName);
390+
Map<String, Object> properties = annotation.getProperties();
391+
Object since = properties.get("since");
392+
if (!(since instanceof String) || ((String) since).isEmpty()) {
393+
addViolation(events, method, annotation.getDescription()
394+
+ " should include a non-empty 'since' attribute of @DeprecatedConfigurationProperty");
395+
}
396+
}))
397+
.allowEmptyShould(true);
398+
}
399+
377400
private static ArchRule autoConfigurationClassesShouldBePublicAndFinal() {
378401
return ArchRuleDefinition.classes()
379402
.that(areRegularAutoConfiguration())

buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ void check() throws IOException {
5959
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(getSource().getFiles());
6060
Report report = new Report(this.projectDir);
6161
analyzer.analyzeSort(report);
62+
analyzer.analyzeDeprecationSince(report);
6263
File reportFile = getReportLocation().get().getAsFile();
6364
report.write(reportFile);
6465
if (report.hasProblems()) {

buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void check() throws IOException {
6666
Report report = new Report(this.projectDir);
6767
analyzer.analyzeSort(report);
6868
analyzer.analyzePropertyDescription(report, getExclusions().get());
69+
analyzer.analyzeDeprecationSince(report);
6970
File reportFile = getReportLocation().get().getAsFile();
7071
report.write(reportFile);
7172
if (report.hasProblems()) {

buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ private boolean isDescribed(Map<String, Object> property) {
134134
return property.get("description") != null;
135135
}
136136

137+
void analyzeDeprecationSince(Report report) throws IOException {
138+
for (File source : this.sources) {
139+
report.registerAnalysis(source, analyzeDeprecationSince(source));
140+
}
141+
}
142+
143+
@SuppressWarnings("unchecked")
144+
private Analysis analyzeDeprecationSince(File source) throws IOException {
145+
Analysis analysis = new Analysis("The following properties are deprecated without a 'since' version:");
146+
Map<String, Object> json = readJsonContent(source);
147+
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
148+
properties.stream().filter((property) -> property.containsKey("deprecation")).forEach((property) -> {
149+
Map<String, Object> deprecation = (Map<String, Object>) property.get("deprecation");
150+
if (!deprecation.containsKey("since")) {
151+
analysis.addItem(property.get("name").toString());
152+
}
153+
});
154+
return analysis;
155+
}
156+
137157
@SuppressWarnings("unchecked")
138158
private Map<String, Object> readJsonContent(File source) {
139159
return this.jsonMapperSupplier.obtain().readValue(source, Map.class);

buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import java.io.IOException;
2020
import java.nio.charset.StandardCharsets;
2121
import java.nio.file.Files;
22+
import java.nio.file.NoSuchFileException;
2223
import java.nio.file.Path;
2324
import java.nio.file.Paths;
2425
import java.util.Arrays;
26+
import java.util.HashMap;
2527
import java.util.LinkedHashMap;
2628
import java.util.LinkedHashSet;
2729
import java.util.Map;
@@ -42,6 +44,7 @@
4244
import org.junit.jupiter.params.provider.EnumSource;
4345

4446
import org.springframework.boot.build.architecture.annotations.TestConditionalOnClass;
47+
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
4548
import org.springframework.util.ClassUtils;
4649
import org.springframework.util.CollectionUtils;
4750
import org.springframework.util.FileSystemUtils;
@@ -340,6 +343,16 @@ void whenConditionalOnClassUsedOnBeanMethodsWithTestSourcesShouldSucceedAndWrite
340343
build(gradleBuild, Task.CHECK_ARCHITECTURE_TEST);
341344
}
342345

346+
@Test
347+
void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport() throws IOException {
348+
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties", "annotations");
349+
GradleBuild gradleBuild = this.gradleBuild.withDependencies(SPRING_CONTEXT)
350+
.withDeprecatedConfigurationPropertyAnnotation(TestDeprecatedConfigurationProperty.class.getName());
351+
buildAndFail(gradleBuild, Task.CHECK_ARCHITECTURE_MAIN,
352+
"should include a non-empty 'since' attribute of @DeprecatedConfigurationProperty",
353+
"DeprecatedConfigurationPropertySince.getProperty");
354+
}
355+
343356
private void prepareTask(Task task, String... sourceDirectories) throws IOException {
344357
for (String sourceDirectory : sourceDirectories) {
345358
FileSystemUtils.copyRecursively(
@@ -372,7 +385,12 @@ private void buildAndFail(GradleBuild gradleBuild, Task task, String... messages
372385
try {
373386
BuildResult buildResult = gradleBuild.buildAndFail(task.toString());
374387
assertThat(buildResult.taskPaths(TaskOutcome.FAILED)).as(buildResult.getOutput()).contains(":" + task);
375-
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages);
388+
try {
389+
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages);
390+
}
391+
catch (NoSuchFileException ex) {
392+
throw new AssertionError("Expected failure report not found\n" + buildResult.getOutput());
393+
}
376394
}
377395
catch (UnexpectedBuildSuccess ex) {
378396
throw new AssertionError("Expected build to fail but it succeeded\n" + ex.getBuildResult().getOutput(), ex);
@@ -436,9 +454,18 @@ GradleBuild withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonN
436454
return this;
437455
}
438456

439-
GradleBuild withConditionalOnClassAnnotation(String annotationName) {
457+
GradleBuild withConditionalOnClassAnnotation(String annotationClass) {
440458
for (Task task : Task.values()) {
441-
configureTask(task, (configuration) -> configuration.withConditionalOnClassAnnotation(annotationName));
459+
configureTask(task, (configuration) -> configuration
460+
.withAnnotation(ArchitectureCheck.CONDITIONAL_ON_CLASS, annotationClass));
461+
}
462+
return this;
463+
}
464+
465+
GradleBuild withDeprecatedConfigurationPropertyAnnotation(String annotationClass) {
466+
for (Task task : Task.values()) {
467+
configureTask(task, (configuration) -> configuration
468+
.withAnnotation(ArchitectureCheck.DEPRECATED_CONFIGURATION_PROPERTY, annotationClass));
442469
}
443470
return this;
444471
}
@@ -498,18 +525,18 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
498525
for (String dependency : this.dependencies) {
499526
buildFile.append("\n implementation ").append(StringUtils.quote(dependency));
500527
}
501-
buildFile.append("}\n");
528+
buildFile.append("\n}\n\n");
502529
}
503530
this.taskConfigurations.forEach((task, configuration) -> {
504531
buildFile.append(task).append(" {");
505-
if (configuration.conditionalOnClassAnnotation() != null) {
506-
buildFile.append("\n conditionalOnClassAnnotation = ")
507-
.append(StringUtils.quote(configuration.conditionalOnClassAnnotation()));
508-
}
509532
if (configuration.prohibitObjectsRequireNonNull() != null) {
510533
buildFile.append("\n prohibitObjectsRequireNonNull = ")
511534
.append(configuration.prohibitObjectsRequireNonNull());
512535
}
536+
if (configuration.annotations() != null && !configuration.annotations().isEmpty()) {
537+
buildFile.append("\n annotationClasses = ")
538+
.append(toGroovyMapString(configuration.annotations()));
539+
}
513540
buildFile.append("\n}\n");
514541
});
515542
NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension;
@@ -536,6 +563,13 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
536563
.withPluginClasspath();
537564
}
538565

566+
static String toGroovyMapString(Map<String, String> map) {
567+
return map.entrySet()
568+
.stream()
569+
.map((entry) -> "'" + entry.getKey() + "' : '" + entry.getValue() + "'")
570+
.collect(Collectors.joining(", ", "[", "]"));
571+
}
572+
539573
private record NullMarkedExtension(Boolean enabled, Set<String> ignoredPackages) {
540574

541575
private NullMarkedExtension withEnabled(Boolean enabled) {
@@ -548,15 +582,24 @@ private NullMarkedExtension withIgnoredPackages(String... ignoredPackages) {
548582

549583
}
550584

551-
private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, String conditionalOnClassAnnotation) {
585+
private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, Map<String, String> annotations) {
552586

553-
private TaskConfiguration withConditionalOnClassAnnotation(String annotationName) {
554-
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, annotationName);
587+
public TaskConfiguration {
588+
if (annotations == null) {
589+
annotations = new HashMap<>();
590+
}
555591
}
556592

557593
private TaskConfiguration withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonNull) {
558-
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.conditionalOnClassAnnotation);
594+
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.annotations);
559595
}
596+
597+
private TaskConfiguration withAnnotation(String name, String annotationClass) {
598+
Map<String, String> map = new HashMap<>(this.annotations);
599+
map.put(name, annotationClass);
600+
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, map);
601+
}
602+
560603
}
561604

562605
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-present 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.build.architecture.annotations;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* {@code @DeprecatedConfigurationProperty} analogue for architecture checks.
26+
*/
27+
@Target(ElementType.METHOD)
28+
@Retention(RetentionPolicy.RUNTIME)
29+
public @interface TestDeprecatedConfigurationProperty {
30+
31+
/**
32+
* The reason for the deprecation.
33+
* @return the deprecation reason
34+
*/
35+
String reason() default "";
36+
37+
/**
38+
* The field that should be used instead (if any).
39+
* @return the replacement field
40+
*/
41+
String replacement() default "";
42+
43+
/**
44+
* The version in which the property became deprecated.
45+
* @return the version
46+
*/
47+
String since() default "";
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-present 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.build.architecture.configurationproperties;
18+
19+
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
20+
21+
public class DeprecatedConfigurationPropertySince {
22+
23+
private String property;
24+
25+
@TestDeprecatedConfigurationProperty(reason = "no longer used")
26+
@Deprecated
27+
public String getProperty() {
28+
return this.property;
29+
}
30+
31+
public void setProperty(String property) {
32+
this.property = property;
33+
}
34+
35+
}

0 commit comments

Comments
 (0)