Skip to content

Commit 11f1c6c

Browse files
committed
Add architecture rule to prevent public modifiers in imported configurations
See gh-47715
1 parent 64136a9 commit 11f1c6c

File tree

3 files changed

+136
-2
lines changed

3 files changed

+136
-2
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,11 @@ private List<String> asDescriptions(List<ArchRule> rules) {
110110
void checkArchitecture() throws Exception {
111111
withCompileClasspath(() -> {
112112
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
113-
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
113+
List<EvaluationResult> results = new ArrayList<>();
114+
evaluate(javaClasses).forEach(results::add);
115+
results.add(new AutoConfigurationChecker().check(javaClasses));
114116
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
117+
List<EvaluationResult> violations = results.stream().filter(EvaluationResult::hasViolation).toList();
115118
writeViolationReport(violations, outputFile);
116119
if (!violations.isEmpty()) {
117120
throw new VerificationException("Architecture check failed. See '" + outputFile + "' for details.");

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,16 @@ private static ArchRule autoConfigurationClassesShouldHaveNoPublicMembers() {
389389
.allowEmptyShould(true);
390390
}
391391

392+
static ArchRule shouldHaveNoPublicMembers() {
393+
return ArchRuleDefinition.members()
394+
.that(areNotDefaultConstructors())
395+
.and(areNotConstants())
396+
.and(dontOverridePublicMethods())
397+
.should()
398+
.notBePublic()
399+
.allowEmptyShould(true);
400+
}
401+
392402
private static ArchRule testAutoConfigurationClassesShouldBePackagePrivateAndFinal() {
393403
return ArchRuleDefinition.classes()
394404
.that()
@@ -400,7 +410,7 @@ private static ArchRule testAutoConfigurationClassesShouldBePackagePrivateAndFin
400410
.allowEmptyShould(true);
401411
}
402412

403-
private static DescribedPredicate<JavaClass> areRegularAutoConfiguration() {
413+
static DescribedPredicate<JavaClass> areRegularAutoConfiguration() {
404414
return DescribedPredicate.describe("Regular @AutoConfiguration",
405415
(javaClass) -> javaClass.isMetaAnnotatedWith(AUTOCONFIGURATION_ANNOTATION)
406416
&& !javaClass.isMetaAnnotatedWith(TEST_AUTOCONFIGURATION_ANNOTATION)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import com.tngtech.archunit.base.DescribedPredicate;
24+
import com.tngtech.archunit.core.domain.JavaAnnotation;
25+
import com.tngtech.archunit.core.domain.JavaClass;
26+
import com.tngtech.archunit.core.domain.JavaClasses;
27+
import com.tngtech.archunit.lang.EvaluationResult;
28+
29+
import org.springframework.util.ReflectionUtils;
30+
31+
/**
32+
* Finds all configurations from auto-configurations (either nested configurations or
33+
* imported ones) and checks that these classes don't contain public members.
34+
*
35+
* @author Moritz Halbritter
36+
*/
37+
class AutoConfigurationChecker {
38+
39+
private final DescribedPredicate<JavaClass> isAutoConfiguration = ArchitectureRules.areRegularAutoConfiguration();
40+
41+
EvaluationResult check(JavaClasses javaClasses) {
42+
AutoConfigurations autoConfigurations = new AutoConfigurations();
43+
for (JavaClass javaClass : javaClasses) {
44+
if (isAutoConfigurationOrEnclosedInAutoConfiguration(javaClass)) {
45+
autoConfigurations.add(javaClass);
46+
}
47+
}
48+
return ArchitectureRules.shouldHaveNoPublicMembers().evaluate(autoConfigurations.getConfigurations());
49+
}
50+
51+
private boolean isAutoConfigurationOrEnclosedInAutoConfiguration(JavaClass javaClass) {
52+
if (this.isAutoConfiguration.test(javaClass)) {
53+
return true;
54+
}
55+
JavaClass enclosingClass = javaClass.getEnclosingClass().orElse(null);
56+
while (enclosingClass != null) {
57+
if (this.isAutoConfiguration.test(enclosingClass)) {
58+
return true;
59+
}
60+
enclosingClass = enclosingClass.getEnclosingClass().orElse(null);
61+
}
62+
return false;
63+
}
64+
65+
private static final class AutoConfigurations {
66+
67+
private static final String SPRING_BOOT_ROOT_PACKAGE = "org.springframework.boot";
68+
69+
private static final String IMPORT = "org.springframework.context.annotation.Import";
70+
71+
private static final String CONFIGURATION = "org.springframework.context.annotation.Configuration";
72+
73+
private final Map<String, JavaClass> classes = new HashMap<>();
74+
75+
void add(JavaClass autoConfiguration) {
76+
if (!autoConfiguration.isMetaAnnotatedWith(CONFIGURATION)) {
77+
return;
78+
}
79+
if (this.classes.putIfAbsent(autoConfiguration.getName(), autoConfiguration) != null) {
80+
return;
81+
}
82+
processImports(autoConfiguration, this.classes);
83+
}
84+
85+
JavaClasses getConfigurations() {
86+
// TODO: Find a way without reflection
87+
Method method = ReflectionUtils.findMethod(JavaClasses.class, "of", Iterable.class);
88+
ReflectionUtils.makeAccessible(method);
89+
return (JavaClasses) ReflectionUtils.invokeMethod(method, null, this.classes.values());
90+
}
91+
92+
private void processImports(JavaClass javaClass, Map<String, JavaClass> result) {
93+
JavaClass[] importedClasses = getImportedClasses(javaClass);
94+
for (JavaClass importedClass : importedClasses) {
95+
if (!isBootClass(importedClass)) {
96+
continue;
97+
}
98+
if (result.putIfAbsent(importedClass.getName(), importedClass) != null) {
99+
continue;
100+
}
101+
// Recursively find other imported classes
102+
processImports(importedClass, result);
103+
}
104+
}
105+
106+
private JavaClass[] getImportedClasses(JavaClass javaClass) {
107+
if (!javaClass.isAnnotatedWith(IMPORT)) {
108+
return new JavaClass[0];
109+
}
110+
JavaAnnotation<JavaClass> imports = javaClass.getAnnotationOfType(IMPORT);
111+
return (JavaClass[]) imports.get("value").orElse(new JavaClass[0]);
112+
}
113+
114+
private boolean isBootClass(JavaClass javaClass) {
115+
String pkg = javaClass.getPackage().getName();
116+
return pkg.equals(SPRING_BOOT_ROOT_PACKAGE) || pkg.startsWith(SPRING_BOOT_ROOT_PACKAGE + ".");
117+
}
118+
119+
}
120+
121+
}

0 commit comments

Comments
 (0)