Improve checking of auto-configuration
This commit is contained in:
committed by
Phillip Webb
parent
644aeabf7b
commit
5464812f80
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2025 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.autoconfigure;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.asm.AnnotationVisitor;
|
||||
import org.springframework.asm.ClassReader;
|
||||
import org.springframework.asm.ClassVisitor;
|
||||
import org.springframework.asm.SpringAsmInfo;
|
||||
import org.springframework.asm.Type;
|
||||
|
||||
/**
|
||||
* An {@code @AutoConfiguration} class.
|
||||
*
|
||||
* @param name name of the auto-configuration class
|
||||
* @param before values of the {@code before} attribute
|
||||
* @param beforeName values of the {@code beforeName} attribute
|
||||
* @param after values of the {@code after} attribute
|
||||
* @param afterName values of the {@code afterName} attribute
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public record AutoConfigurationClass(String name, List<String> before, List<String> beforeName, List<String> after,
|
||||
List<String> afterName) {
|
||||
|
||||
private AutoConfigurationClass(String name, Map<String, List<String>> attributes) {
|
||||
this(name, attributes.getOrDefault("before", Collections.emptyList()),
|
||||
attributes.getOrDefault("beforeName", Collections.emptyList()),
|
||||
attributes.getOrDefault("after", Collections.emptyList()),
|
||||
attributes.getOrDefault("afterName", Collections.emptyList()));
|
||||
}
|
||||
|
||||
static AutoConfigurationClass of(File classFile) {
|
||||
try (FileInputStream input = new FileInputStream(classFile)) {
|
||||
ClassReader classReader = new ClassReader(input);
|
||||
AutoConfigurationClassVisitor visitor = new AutoConfigurationClassVisitor();
|
||||
classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
|
||||
return visitor.autoConfigurationClass;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AutoConfigurationClassVisitor extends ClassVisitor {
|
||||
|
||||
private AutoConfigurationClass autoConfigurationClass;
|
||||
|
||||
private String name;
|
||||
|
||||
private AutoConfigurationClassVisitor() {
|
||||
super(SpringAsmInfo.ASM_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String signature, String superName,
|
||||
String[] interfaces) {
|
||||
this.name = Type.getObjectType(name).getClassName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
|
||||
String annotationClassName = Type.getType(descriptor).getClassName();
|
||||
if ("org.springframework.boot.autoconfigure.AutoConfiguration".equals(annotationClassName)) {
|
||||
return new AutoConfigurationAnnotationVisitor();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private final class AutoConfigurationAnnotationVisitor extends AnnotationVisitor {
|
||||
|
||||
private Map<String, List<String>> attributes = new HashMap<>();
|
||||
|
||||
private static final Set<String> INTERESTING_ATTRIBUTES = Set.of("before", "beforeName", "after",
|
||||
"afterName");
|
||||
|
||||
private AutoConfigurationAnnotationVisitor() {
|
||||
super(SpringAsmInfo.ASM_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
AutoConfigurationClassVisitor.this.autoConfigurationClass = new AutoConfigurationClass(
|
||||
AutoConfigurationClassVisitor.this.name, this.attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitArray(String attributeName) {
|
||||
if (INTERESTING_ATTRIBUTES.contains(attributeName)) {
|
||||
return new AnnotationVisitor(SpringAsmInfo.ASM_VERSION) {
|
||||
|
||||
@Override
|
||||
public void visit(String name, Object value) {
|
||||
if (value instanceof Type type) {
|
||||
value = type.getClassName();
|
||||
}
|
||||
AutoConfigurationAnnotationVisitor.this.attributes
|
||||
.computeIfAbsent(attributeName, (n) -> new ArrayList<>())
|
||||
.add(Objects.toString(value));
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 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.autoconfigure;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.SkipWhenEmpty;
|
||||
|
||||
/**
|
||||
* A {@link Task} that uses a project's auto-configuration imports.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class AutoConfigurationImportsTask extends DefaultTask {
|
||||
|
||||
static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
|
||||
|
||||
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
|
||||
|
||||
@InputFiles
|
||||
@SkipWhenEmpty
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
public FileTree getSource() {
|
||||
return this.sourceFiles.getAsFileTree().matching((filter) -> filter.include(IMPORTS_FILE));
|
||||
}
|
||||
|
||||
public void setSource(Object source) {
|
||||
this.sourceFiles = getProject().getObjects().fileCollection().from(source);
|
||||
}
|
||||
|
||||
protected List<String> loadImports() {
|
||||
File importsFile = getSource().getSingleFile();
|
||||
try {
|
||||
return Files.readAllLines(importsFile.toPath());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,31 +16,20 @@
|
||||
|
||||
package org.springframework.boot.build.autoconfigure;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.lang.ArchCondition;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import com.tngtech.archunit.lang.ConditionEvents;
|
||||
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
|
||||
import org.springframework.boot.build.DeployedPlugin;
|
||||
import org.springframework.boot.build.architecture.ArchitectureCheck;
|
||||
import org.springframework.boot.build.architecture.ArchitecturePlugin;
|
||||
import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
|
||||
|
||||
/**
|
||||
* {@link Plugin} for projects that define auto-configuration. When applied, the plugin
|
||||
@@ -70,14 +59,16 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
|
||||
*/
|
||||
public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata";
|
||||
|
||||
private static final String AUTO_CONFIGURATION_IMPORTS_PATH = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(DeployedPlugin.class);
|
||||
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
|
||||
Configuration annotationProcessors = project.getConfigurations()
|
||||
.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
|
||||
SourceSet main = project.getExtensions()
|
||||
.getByType(JavaPluginExtension.class)
|
||||
.getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
annotationProcessors.getDependencies()
|
||||
.add(project.getDependencies()
|
||||
.project(Collections.singletonMap("path",
|
||||
@@ -87,10 +78,6 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
|
||||
.project(Collections.singletonMap("path",
|
||||
":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
|
||||
project.getTasks().register("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> {
|
||||
SourceSet main = project.getExtensions()
|
||||
.getByType(JavaPluginExtension.class)
|
||||
.getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
task.setSourceSet(main);
|
||||
task.dependsOn(main.getClassesTaskName());
|
||||
task.getOutputFile()
|
||||
@@ -99,74 +86,37 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
|
||||
.add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, task.getOutputFile(),
|
||||
(artifact) -> artifact.builtBy(task));
|
||||
});
|
||||
project.getTasks()
|
||||
.register("checkAutoConfigurationImports", CheckAutoConfigurationImports.class, (task) -> {
|
||||
task.setSource(main.getResources());
|
||||
task.setClasspath(main.getOutput().getClassesDirs());
|
||||
task.setDescription("Checks the %s file of the main source set."
|
||||
.formatted(AutoConfigurationImportsTask.IMPORTS_FILE));
|
||||
});
|
||||
Configuration requiredClasspath = project.getConfigurations()
|
||||
.create("autoConfigurationRequiredClasspath")
|
||||
.extendsFrom(project.getConfigurations().getByName(main.getImplementationConfigurationName()),
|
||||
project.getConfigurations().getByName(main.getRuntimeOnlyConfigurationName()));
|
||||
requiredClasspath.getDependencies()
|
||||
.add(project.getDependencies()
|
||||
.project(Map.of("path", ":spring-boot-project:spring-boot-autoconfigure")));
|
||||
TaskProvider<CheckAutoConfigurationClasses> checkAutoConfigurationClasses = project.getTasks()
|
||||
.register("checkAutoConfigurationClasses", CheckAutoConfigurationClasses.class, (task) -> {
|
||||
task.setSource(main.getResources());
|
||||
task.setClasspath(main.getOutput().getClassesDirs());
|
||||
task.setRequiredDependencies(requiredClasspath);
|
||||
task.setDescription("Checks the auto-configuration classes of the main source set.");
|
||||
});
|
||||
project.getPlugins()
|
||||
.withType(ArchitecturePlugin.class, (plugin) -> configureArchitecturePluginTasks(project));
|
||||
.withType(OptionalDependenciesPlugin.class,
|
||||
(plugin) -> checkAutoConfigurationClasses.configure((check) -> {
|
||||
Configuration optionalClasspath = project.getConfigurations()
|
||||
.create("autoConfigurationOptionalClassPath")
|
||||
.extendsFrom(project.getConfigurations()
|
||||
.getByName(OptionalDependenciesPlugin.OPTIONAL_CONFIGURATION_NAME));
|
||||
check.setOptionalDependencies(optionalClasspath);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private void configureArchitecturePluginTasks(Project project) {
|
||||
project.getTasks().configureEach((task) -> {
|
||||
if ("checkArchitectureMain".equals(task.getName()) && task instanceof ArchitectureCheck architectureCheck) {
|
||||
configureCheckArchitectureMain(project, architectureCheck);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCheckArchitectureMain(Project project, ArchitectureCheck architectureCheck) {
|
||||
SourceSet main = project.getExtensions()
|
||||
.getByType(JavaPluginExtension.class)
|
||||
.getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
File resourcesDirectory = main.getOutput().getResourcesDir();
|
||||
architectureCheck.dependsOn(main.getProcessResourcesTaskName());
|
||||
architectureCheck.getInputs()
|
||||
.files(resourcesDirectory)
|
||||
.optional()
|
||||
.withPathSensitivity(PathSensitivity.RELATIVE);
|
||||
architectureCheck.getRules()
|
||||
.add(allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
|
||||
autoConfigurationImports(project, resourcesDirectory)));
|
||||
}
|
||||
|
||||
private ArchRule allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
|
||||
Provider<AutoConfigurationImports> imports) {
|
||||
return ArchRuleDefinition.classes()
|
||||
.that()
|
||||
.areAnnotatedWith("org.springframework.boot.autoconfigure.AutoConfiguration")
|
||||
.should(beListedInAutoConfigurationImports(imports))
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private ArchCondition<JavaClass> beListedInAutoConfigurationImports(Provider<AutoConfigurationImports> imports) {
|
||||
return new ArchCondition<>("be listed in " + AUTO_CONFIGURATION_IMPORTS_PATH) {
|
||||
|
||||
@Override
|
||||
public void check(JavaClass item, ConditionEvents events) {
|
||||
AutoConfigurationImports autoConfigurationImports = imports.get();
|
||||
if (!autoConfigurationImports.imports.contains(item.getName())) {
|
||||
events.add(SimpleConditionEvent.violated(item,
|
||||
item.getName() + " was not listed in " + autoConfigurationImports.importsFile));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private Provider<AutoConfigurationImports> autoConfigurationImports(Project project, File resourcesDirectory) {
|
||||
Path importsFile = new File(resourcesDirectory, AUTO_CONFIGURATION_IMPORTS_PATH).toPath();
|
||||
return project.provider(() -> {
|
||||
try {
|
||||
return new AutoConfigurationImports(project.getProjectDir().toPath().relativize(importsFile),
|
||||
Files.readAllLines(importsFile));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException("Failed to read AutoConfiguration.imports", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private record AutoConfigurationImports(Path importsFile, List<String> imports) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.autoconfigure;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.api.tasks.Classpath;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.VerificationException;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
|
||||
/**
|
||||
* Task to check a project's {@code @AutoConfiguration} classes.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class CheckAutoConfigurationClasses extends AutoConfigurationImportsTask {
|
||||
|
||||
private FileCollection classpath = getProject().getObjects().fileCollection();
|
||||
|
||||
private FileCollection optionalDependencies = getProject().getObjects().fileCollection();
|
||||
|
||||
private FileCollection requiredDependencies = getProject().getObjects().fileCollection();
|
||||
|
||||
private SetProperty<String> optionalDependencyClassNames = getProject().getObjects().setProperty(String.class);
|
||||
|
||||
private SetProperty<String> requiredDependencyClassNames = getProject().getObjects().setProperty(String.class);
|
||||
|
||||
public CheckAutoConfigurationClasses() {
|
||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
||||
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
this.optionalDependencyClassNames.set(getProject().provider(() -> classNamesOf(this.optionalDependencies)));
|
||||
this.requiredDependencyClassNames.set(getProject().provider(() -> classNamesOf(this.requiredDependencies)));
|
||||
}
|
||||
|
||||
private static List<String> classNamesOf(FileCollection classpath) {
|
||||
return classpath.getFiles().stream().flatMap((file) -> {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
return Collections.list(jarFile.entries())
|
||||
.stream()
|
||||
.filter((entry) -> !entry.isDirectory())
|
||||
.map(JarEntry::getName)
|
||||
.filter((entryName) -> entryName.endsWith(".class"))
|
||||
.map((entryName) -> entryName.substring(0, entryName.length() - ".class".length()))
|
||||
.map((entryName) -> entryName.replace("/", "."));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Classpath
|
||||
public FileCollection getClasspath() {
|
||||
return this.classpath;
|
||||
}
|
||||
|
||||
public void setClasspath(Object classpath) {
|
||||
this.classpath = getProject().getObjects().fileCollection().from(classpath);
|
||||
}
|
||||
|
||||
@Classpath
|
||||
public FileCollection getOptionalDependencies() {
|
||||
return this.optionalDependencies;
|
||||
}
|
||||
|
||||
public void setOptionalDependencies(Object classpath) {
|
||||
this.optionalDependencies = getProject().getObjects().fileCollection().from(classpath);
|
||||
}
|
||||
|
||||
@Classpath
|
||||
public FileCollection getRequiredDependencies() {
|
||||
return this.requiredDependencies;
|
||||
}
|
||||
|
||||
public void setRequiredDependencies(Object classpath) {
|
||||
this.requiredDependencies = getProject().getObjects().fileCollection().from(classpath);
|
||||
}
|
||||
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDirectory();
|
||||
|
||||
@TaskAction
|
||||
void execute() {
|
||||
Map<String, List<String>> problems = new TreeMap<>();
|
||||
Set<String> optionalOnlyClassNames = new HashSet<>(this.optionalDependencyClassNames.get());
|
||||
Set<String> requiredClassNames = this.requiredDependencyClassNames.get();
|
||||
optionalOnlyClassNames.removeAll(requiredClassNames);
|
||||
classFiles().forEach((classFile) -> {
|
||||
AutoConfigurationClass autoConfigurationClass = AutoConfigurationClass.of(classFile);
|
||||
if (autoConfigurationClass != null) {
|
||||
check(autoConfigurationClass, optionalOnlyClassNames, requiredClassNames, problems);
|
||||
}
|
||||
});
|
||||
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
|
||||
writeReport(problems, outputFile);
|
||||
if (!problems.isEmpty()) {
|
||||
throw new VerificationException(
|
||||
"Auto-configuration class check failed. See '%s' for details".formatted(outputFile));
|
||||
}
|
||||
}
|
||||
|
||||
private List<File> classFiles() {
|
||||
List<File> classFiles = new ArrayList<>();
|
||||
for (File root : this.classpath.getFiles()) {
|
||||
try (Stream<Path> files = Files.walk(root.toPath())) {
|
||||
files.forEach((file) -> {
|
||||
if (Files.isRegularFile(file) && file.getFileName().toString().endsWith(".class")) {
|
||||
classFiles.add(file.toFile());
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
return classFiles;
|
||||
}
|
||||
|
||||
private void check(AutoConfigurationClass autoConfigurationClass, Set<String> optionalOnlyClassNames,
|
||||
Set<String> requiredClassNames, Map<String, List<String>> problems) {
|
||||
if (!autoConfigurationClass.name().endsWith("AutoConfiguration")) {
|
||||
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>())
|
||||
.add("Name of a class annotated with @AutoConfiguration should end with AutoConfiguration");
|
||||
}
|
||||
autoConfigurationClass.before().forEach((before) -> {
|
||||
if (optionalOnlyClassNames.contains(before)) {
|
||||
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>())
|
||||
.add("before '%s' is from an optional dependency and should be declared in beforeName"
|
||||
.formatted(before));
|
||||
}
|
||||
});
|
||||
autoConfigurationClass.beforeName().forEach((beforeName) -> {
|
||||
if (!optionalOnlyClassNames.contains(beforeName)) {
|
||||
String problem = requiredClassNames.contains(beforeName)
|
||||
? "beforeName '%s' is from a required dependency and should be declared in before"
|
||||
.formatted(beforeName)
|
||||
: "beforeName '%s' not found".formatted(beforeName);
|
||||
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>()).add(problem);
|
||||
}
|
||||
});
|
||||
autoConfigurationClass.after().forEach((after) -> {
|
||||
if (optionalOnlyClassNames.contains(after)) {
|
||||
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>())
|
||||
.add("after '%s' is from an optional dependency and should be declared in afterName"
|
||||
.formatted(after));
|
||||
}
|
||||
});
|
||||
autoConfigurationClass.afterName().forEach((afterName) -> {
|
||||
if (!optionalOnlyClassNames.contains(afterName)) {
|
||||
String problem = requiredClassNames.contains(afterName)
|
||||
? "afterName '%s' is from a required dependency and should be declared in after"
|
||||
.formatted(afterName)
|
||||
: "afterName '%s' not found".formatted(afterName);
|
||||
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>()).add(problem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void writeReport(Map<String, List<String>> problems, File outputFile) {
|
||||
outputFile.getParentFile().mkdirs();
|
||||
StringBuilder report = new StringBuilder();
|
||||
if (!problems.isEmpty()) {
|
||||
report.append("Found auto-configuration class problems:%n".formatted());
|
||||
problems.forEach((className, classProblems) -> {
|
||||
report.append(" - %s:%n".formatted(className));
|
||||
classProblems.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
|
||||
});
|
||||
}
|
||||
try {
|
||||
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2025 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.autoconfigure;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.tasks.Classpath;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.VerificationException;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
|
||||
/**
|
||||
* Task to check the contents of a project's
|
||||
* {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}
|
||||
* file.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class CheckAutoConfigurationImports extends AutoConfigurationImportsTask {
|
||||
|
||||
private FileCollection classpath = getProject().getObjects().fileCollection();
|
||||
|
||||
public CheckAutoConfigurationImports() {
|
||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
||||
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
}
|
||||
|
||||
@Classpath
|
||||
public FileCollection getClasspath() {
|
||||
return this.classpath;
|
||||
}
|
||||
|
||||
public void setClasspath(Object classpath) {
|
||||
this.classpath = getProject().getObjects().fileCollection().from(classpath);
|
||||
}
|
||||
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDirectory();
|
||||
|
||||
@TaskAction
|
||||
void execute() {
|
||||
File importsFile = getSource().getSingleFile();
|
||||
check(importsFile);
|
||||
}
|
||||
|
||||
private void check(File importsFile) {
|
||||
List<String> imports = loadImports();
|
||||
List<String> problems = new ArrayList<>();
|
||||
for (String imported : imports) {
|
||||
File classFile = find(imported);
|
||||
if (classFile == null) {
|
||||
problems.add("'%s' was not found".formatted(imported));
|
||||
}
|
||||
else if (!correctlyAnnotated(classFile)) {
|
||||
problems.add("'%s' is not annotated with @AutoConfiguration".formatted(imported));
|
||||
}
|
||||
}
|
||||
List<String> sortedValues = new ArrayList<>(imports);
|
||||
Collections.sort(sortedValues);
|
||||
if (!sortedValues.equals(imports)) {
|
||||
problems.add("Entries should be sorted alphabetically");
|
||||
}
|
||||
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
|
||||
writeReport(importsFile, problems, outputFile);
|
||||
if (!problems.isEmpty()) {
|
||||
throw new VerificationException("%s check failed. See '%s' for details"
|
||||
.formatted(AutoConfigurationImportsTask.IMPORTS_FILE, outputFile));
|
||||
}
|
||||
}
|
||||
|
||||
private File find(String className) {
|
||||
for (File root : this.classpath.getFiles()) {
|
||||
String classFilePath = className.replace(".", "/") + ".class";
|
||||
File classFile = new File(root, classFilePath);
|
||||
if (classFile.isFile()) {
|
||||
return classFile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean correctlyAnnotated(File classFile) {
|
||||
return AutoConfigurationClass.of(classFile) != null;
|
||||
}
|
||||
|
||||
private void writeReport(File importsFile, List<String> problems, File outputFile) {
|
||||
outputFile.getParentFile().mkdirs();
|
||||
StringBuilder report = new StringBuilder();
|
||||
if (!problems.isEmpty()) {
|
||||
report.append("Found problems in '%s':%n".formatted(importsFile));
|
||||
problems.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
|
||||
}
|
||||
try {
|
||||
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user