Skip to content

Commit 56f1722

Browse files
committed
Merge branch 'improve-auto-config-checks' into 4.0.x-restructure
2 parents 60487f3 + b49c183 commit 56f1722

File tree

67 files changed

+733
-238
lines changed

Some content is hidden

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

67 files changed

+733
-238
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2025 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.autoconfigure;
18+
19+
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.IOException;
22+
import java.io.UncheckedIOException;
23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Objects;
29+
import java.util.Set;
30+
31+
import org.springframework.asm.AnnotationVisitor;
32+
import org.springframework.asm.ClassReader;
33+
import org.springframework.asm.ClassVisitor;
34+
import org.springframework.asm.SpringAsmInfo;
35+
import org.springframework.asm.Type;
36+
37+
/**
38+
* An {@code @AutoConfiguration} class.
39+
*
40+
* @param name name of the auto-configuration class
41+
* @param before values of the {@code before} attribute
42+
* @param beforeName values of the {@code beforeName} attribute
43+
* @param after values of the {@code after} attribute
44+
* @param afterName values of the {@code afterName} attribute
45+
* @author Andy Wilkinson
46+
*/
47+
public record AutoConfigurationClass(String name, List<String> before, List<String> beforeName, List<String> after,
48+
List<String> afterName) {
49+
50+
private AutoConfigurationClass(String name, Map<String, List<String>> attributes) {
51+
this(name, attributes.getOrDefault("before", Collections.emptyList()),
52+
attributes.getOrDefault("beforeName", Collections.emptyList()),
53+
attributes.getOrDefault("after", Collections.emptyList()),
54+
attributes.getOrDefault("afterName", Collections.emptyList()));
55+
}
56+
57+
static AutoConfigurationClass of(File classFile) {
58+
try (FileInputStream input = new FileInputStream(classFile)) {
59+
ClassReader classReader = new ClassReader(input);
60+
AutoConfigurationClassVisitor visitor = new AutoConfigurationClassVisitor();
61+
classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
62+
return visitor.autoConfigurationClass;
63+
}
64+
catch (IOException ex) {
65+
throw new UncheckedIOException(ex);
66+
}
67+
}
68+
69+
private static final class AutoConfigurationClassVisitor extends ClassVisitor {
70+
71+
private AutoConfigurationClass autoConfigurationClass;
72+
73+
private String name;
74+
75+
private AutoConfigurationClassVisitor() {
76+
super(SpringAsmInfo.ASM_VERSION);
77+
}
78+
79+
@Override
80+
public void visit(int version, int access, String name, String signature, String superName,
81+
String[] interfaces) {
82+
this.name = Type.getObjectType(name).getClassName();
83+
}
84+
85+
@Override
86+
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
87+
String annotationClassName = Type.getType(descriptor).getClassName();
88+
if ("org.springframework.boot.autoconfigure.AutoConfiguration".equals(annotationClassName)) {
89+
return new AutoConfigurationAnnotationVisitor();
90+
}
91+
return null;
92+
}
93+
94+
private final class AutoConfigurationAnnotationVisitor extends AnnotationVisitor {
95+
96+
private Map<String, List<String>> attributes = new HashMap<>();
97+
98+
private static final Set<String> INTERESTING_ATTRIBUTES = Set.of("before", "beforeName", "after",
99+
"afterName");
100+
101+
private AutoConfigurationAnnotationVisitor() {
102+
super(SpringAsmInfo.ASM_VERSION);
103+
}
104+
105+
@Override
106+
public void visitEnd() {
107+
AutoConfigurationClassVisitor.this.autoConfigurationClass = new AutoConfigurationClass(
108+
AutoConfigurationClassVisitor.this.name, this.attributes);
109+
}
110+
111+
@Override
112+
public AnnotationVisitor visitArray(String attributeName) {
113+
if (INTERESTING_ATTRIBUTES.contains(attributeName)) {
114+
return new AnnotationVisitor(SpringAsmInfo.ASM_VERSION) {
115+
116+
@Override
117+
public void visit(String name, Object value) {
118+
if (value instanceof Type type) {
119+
value = type.getClassName();
120+
}
121+
AutoConfigurationAnnotationVisitor.this.attributes
122+
.computeIfAbsent(attributeName, (n) -> new ArrayList<>())
123+
.add(Objects.toString(value));
124+
}
125+
126+
};
127+
}
128+
return null;
129+
}
130+
131+
}
132+
133+
}
134+
135+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 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.autoconfigure;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.UncheckedIOException;
22+
import java.nio.file.Files;
23+
import java.util.List;
24+
25+
import org.gradle.api.DefaultTask;
26+
import org.gradle.api.Task;
27+
import org.gradle.api.file.FileCollection;
28+
import org.gradle.api.file.FileTree;
29+
import org.gradle.api.tasks.InputFiles;
30+
import org.gradle.api.tasks.PathSensitive;
31+
import org.gradle.api.tasks.PathSensitivity;
32+
import org.gradle.api.tasks.SkipWhenEmpty;
33+
34+
/**
35+
* A {@link Task} that uses a project's auto-configuration imports.
36+
*
37+
* @author Andy Wilkinson
38+
*/
39+
public abstract class AutoConfigurationImportsTask extends DefaultTask {
40+
41+
static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
42+
43+
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
44+
45+
@InputFiles
46+
@SkipWhenEmpty
47+
@PathSensitive(PathSensitivity.RELATIVE)
48+
public FileTree getSource() {
49+
return this.sourceFiles.getAsFileTree().matching((filter) -> filter.include(IMPORTS_FILE));
50+
}
51+
52+
public void setSource(Object source) {
53+
this.sourceFiles = getProject().getObjects().fileCollection().from(source);
54+
}
55+
56+
protected List<String> loadImports() {
57+
File importsFile = getSource().getSingleFile();
58+
try {
59+
return Files.readAllLines(importsFile.toPath());
60+
}
61+
catch (IOException ex) {
62+
throw new UncheckedIOException(ex);
63+
}
64+
}
65+
66+
}

buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java

Lines changed: 36 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,20 @@
1616

1717
package org.springframework.boot.build.autoconfigure;
1818

19-
import java.io.File;
20-
import java.io.IOException;
21-
import java.nio.file.Files;
22-
import java.nio.file.Path;
2319
import java.util.Collections;
24-
import java.util.List;
20+
import java.util.Map;
2521

26-
import com.tngtech.archunit.core.domain.JavaClass;
27-
import com.tngtech.archunit.core.domain.JavaModifier;
28-
import com.tngtech.archunit.lang.ArchCondition;
29-
import com.tngtech.archunit.lang.ArchRule;
30-
import com.tngtech.archunit.lang.ConditionEvents;
31-
import com.tngtech.archunit.lang.SimpleConditionEvent;
32-
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
3322
import org.gradle.api.Plugin;
3423
import org.gradle.api.Project;
3524
import org.gradle.api.artifacts.Configuration;
3625
import org.gradle.api.plugins.JavaPlugin;
3726
import org.gradle.api.plugins.JavaPluginExtension;
38-
import org.gradle.api.provider.Provider;
39-
import org.gradle.api.tasks.PathSensitivity;
4027
import org.gradle.api.tasks.SourceSet;
28+
import org.gradle.api.tasks.TaskProvider;
4129

4230
import org.springframework.boot.build.DeployedPlugin;
43-
import org.springframework.boot.build.architecture.ArchitectureCheck;
4431
import org.springframework.boot.build.architecture.ArchitecturePlugin;
32+
import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
4533

4634
/**
4735
* {@link Plugin} for projects that define auto-configuration. When applied, the plugin
@@ -71,14 +59,16 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
7159
*/
7260
public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata";
7361

74-
private static final String AUTO_CONFIGURATION_IMPORTS_PATH = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
75-
7662
@Override
7763
public void apply(Project project) {
7864
project.getPlugins().apply(DeployedPlugin.class);
7965
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
8066
Configuration annotationProcessors = project.getConfigurations()
8167
.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
68+
SourceSet main = project.getExtensions()
69+
.getByType(JavaPluginExtension.class)
70+
.getSourceSets()
71+
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
8272
annotationProcessors.getDependencies()
8373
.add(project.getDependencies()
8474
.project(Collections.singletonMap("path",
@@ -88,10 +78,6 @@ public void apply(Project project) {
8878
.project(Collections.singletonMap("path",
8979
":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
9080
project.getTasks().register("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> {
91-
SourceSet main = project.getExtensions()
92-
.getByType(JavaPluginExtension.class)
93-
.getSourceSets()
94-
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
9581
task.setSourceSet(main);
9682
task.dependsOn(main.getClassesTaskName());
9783
task.getOutputFile()
@@ -100,76 +86,37 @@ public void apply(Project project) {
10086
.add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, task.getOutputFile(),
10187
(artifact) -> artifact.builtBy(task));
10288
});
89+
project.getTasks()
90+
.register("checkAutoConfigurationImports", CheckAutoConfigurationImports.class, (task) -> {
91+
task.setSource(main.getResources());
92+
task.setClasspath(main.getOutput().getClassesDirs());
93+
task.setDescription("Checks the %s file of the main source set."
94+
.formatted(AutoConfigurationImportsTask.IMPORTS_FILE));
95+
});
96+
Configuration requiredClasspath = project.getConfigurations()
97+
.create("autoConfigurationRequiredClasspath")
98+
.extendsFrom(project.getConfigurations().getByName(main.getImplementationConfigurationName()),
99+
project.getConfigurations().getByName(main.getRuntimeOnlyConfigurationName()));
100+
requiredClasspath.getDependencies()
101+
.add(project.getDependencies()
102+
.project(Map.of("path", ":spring-boot-project:spring-boot-autoconfigure")));
103+
TaskProvider<CheckAutoConfigurationClasses> checkAutoConfigurationClasses = project.getTasks()
104+
.register("checkAutoConfigurationClasses", CheckAutoConfigurationClasses.class, (task) -> {
105+
task.setSource(main.getResources());
106+
task.setClasspath(main.getOutput().getClassesDirs());
107+
task.setRequiredDependencies(requiredClasspath);
108+
task.setDescription("Checks the auto-configuration classes of the main source set.");
109+
});
103110
project.getPlugins()
104-
.withType(ArchitecturePlugin.class, (plugin) -> configureArchitecturePluginTasks(project));
105-
});
106-
}
107-
108-
private void configureArchitecturePluginTasks(Project project) {
109-
project.getTasks().configureEach((task) -> {
110-
if ("checkArchitectureMain".equals(task.getName()) && task instanceof ArchitectureCheck architectureCheck) {
111-
configureCheckArchitectureMain(project, architectureCheck);
112-
}
111+
.withType(OptionalDependenciesPlugin.class,
112+
(plugin) -> checkAutoConfigurationClasses.configure((check) -> {
113+
Configuration optionalClasspath = project.getConfigurations()
114+
.create("autoConfigurationOptionalClassPath")
115+
.extendsFrom(project.getConfigurations()
116+
.getByName(OptionalDependenciesPlugin.OPTIONAL_CONFIGURATION_NAME));
117+
check.setOptionalDependencies(optionalClasspath);
118+
}));
113119
});
114120
}
115121

116-
private void configureCheckArchitectureMain(Project project, ArchitectureCheck architectureCheck) {
117-
SourceSet main = project.getExtensions()
118-
.getByType(JavaPluginExtension.class)
119-
.getSourceSets()
120-
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
121-
File resourcesDirectory = main.getOutput().getResourcesDir();
122-
architectureCheck.dependsOn(main.getProcessResourcesTaskName());
123-
architectureCheck.getInputs()
124-
.files(resourcesDirectory)
125-
.optional()
126-
.withPathSensitivity(PathSensitivity.RELATIVE);
127-
architectureCheck.getRules()
128-
.add(allConcreteClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
129-
autoConfigurationImports(project, resourcesDirectory)));
130-
}
131-
132-
private ArchRule allConcreteClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
133-
Provider<AutoConfigurationImports> imports) {
134-
return ArchRuleDefinition.classes()
135-
.that()
136-
.doNotHaveModifier(JavaModifier.ABSTRACT)
137-
.and()
138-
.areAnnotatedWith("org.springframework.boot.autoconfigure.AutoConfiguration")
139-
.should(beListedInAutoConfigurationImports(imports))
140-
.allowEmptyShould(true);
141-
}
142-
143-
private ArchCondition<JavaClass> beListedInAutoConfigurationImports(Provider<AutoConfigurationImports> imports) {
144-
return new ArchCondition<>("be listed in " + AUTO_CONFIGURATION_IMPORTS_PATH) {
145-
146-
@Override
147-
public void check(JavaClass item, ConditionEvents events) {
148-
AutoConfigurationImports autoConfigurationImports = imports.get();
149-
if (!autoConfigurationImports.imports.contains(item.getName())) {
150-
events.add(SimpleConditionEvent.violated(item,
151-
item.getName() + " was not listed in " + autoConfigurationImports.importsFile));
152-
}
153-
}
154-
155-
};
156-
}
157-
158-
private Provider<AutoConfigurationImports> autoConfigurationImports(Project project, File resourcesDirectory) {
159-
Path importsFile = new File(resourcesDirectory, AUTO_CONFIGURATION_IMPORTS_PATH).toPath();
160-
return project.provider(() -> {
161-
try {
162-
return new AutoConfigurationImports(project.getProjectDir().toPath().relativize(importsFile),
163-
Files.readAllLines(importsFile));
164-
}
165-
catch (IOException ex) {
166-
throw new RuntimeException("Failed to read AutoConfiguration.imports", ex);
167-
}
168-
});
169-
}
170-
171-
private record AutoConfigurationImports(Path importsFile, List<String> imports) {
172-
173-
}
174-
175122
}

0 commit comments

Comments
 (0)