Skip to content

Commit 5464812

Browse files
wilkinsonaphilwebb
authored andcommitted
Improve checking of auto-configuration
1 parent 644aeab commit 5464812

File tree

5 files changed

+575
-86
lines changed

5 files changed

+575
-86
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 & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +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.lang.ArchCondition;
28-
import com.tngtech.archunit.lang.ArchRule;
29-
import com.tngtech.archunit.lang.ConditionEvents;
30-
import com.tngtech.archunit.lang.SimpleConditionEvent;
31-
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
3222
import org.gradle.api.Plugin;
3323
import org.gradle.api.Project;
3424
import org.gradle.api.artifacts.Configuration;
3525
import org.gradle.api.plugins.JavaPlugin;
3626
import org.gradle.api.plugins.JavaPluginExtension;
37-
import org.gradle.api.provider.Provider;
38-
import org.gradle.api.tasks.PathSensitivity;
3927
import org.gradle.api.tasks.SourceSet;
28+
import org.gradle.api.tasks.TaskProvider;
4029

4130
import org.springframework.boot.build.DeployedPlugin;
42-
import org.springframework.boot.build.architecture.ArchitectureCheck;
4331
import org.springframework.boot.build.architecture.ArchitecturePlugin;
32+
import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
4433

4534
/**
4635
* {@link Plugin} for projects that define auto-configuration. When applied, the plugin
@@ -70,14 +59,16 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
7059
*/
7160
public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata";
7261

73-
private static final String AUTO_CONFIGURATION_IMPORTS_PATH = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
74-
7562
@Override
7663
public void apply(Project project) {
7764
project.getPlugins().apply(DeployedPlugin.class);
7865
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
7966
Configuration annotationProcessors = project.getConfigurations()
8067
.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
68+
SourceSet main = project.getExtensions()
69+
.getByType(JavaPluginExtension.class)
70+
.getSourceSets()
71+
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
8172
annotationProcessors.getDependencies()
8273
.add(project.getDependencies()
8374
.project(Collections.singletonMap("path",
@@ -87,10 +78,6 @@ public void apply(Project project) {
8778
.project(Collections.singletonMap("path",
8879
":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
8980
project.getTasks().register("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> {
90-
SourceSet main = project.getExtensions()
91-
.getByType(JavaPluginExtension.class)
92-
.getSourceSets()
93-
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
9481
task.setSourceSet(main);
9582
task.dependsOn(main.getClassesTaskName());
9683
task.getOutputFile()
@@ -99,74 +86,37 @@ public void apply(Project project) {
9986
.add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, task.getOutputFile(),
10087
(artifact) -> artifact.builtBy(task));
10188
});
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+
});
102110
project.getPlugins()
103-
.withType(ArchitecturePlugin.class, (plugin) -> configureArchitecturePluginTasks(project));
104-
});
105-
}
106-
107-
private void configureArchitecturePluginTasks(Project project) {
108-
project.getTasks().configureEach((task) -> {
109-
if ("checkArchitectureMain".equals(task.getName()) && task instanceof ArchitectureCheck architectureCheck) {
110-
configureCheckArchitectureMain(project, architectureCheck);
111-
}
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+
}));
112119
});
113120
}
114121

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

0 commit comments

Comments
 (0)