Browse Source
META-INF/spring.factories and META-INF/spring/aot.factories in the main source set are now checked. The checks verify that: - Each class listed in the values exists in the source set's output - The classes are listed alphabetically - Nested classes are identified using their binary name Closes gh-44676pull/42891/head
14 changed files with 304 additions and 23 deletions
@ -0,0 +1,32 @@ |
|||||||
|
/* |
||||||
|
* 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.springframework; |
||||||
|
|
||||||
|
import org.gradle.api.Task; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Task} that checks {@code META-INF/spring/aot.factories}. |
||||||
|
* |
||||||
|
* @author Andy Wilkinson |
||||||
|
*/ |
||||||
|
public abstract class CheckAotFactories extends CheckFactoriesFile { |
||||||
|
|
||||||
|
public CheckAotFactories() { |
||||||
|
super("META-INF/spring/aot.factories"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,178 @@ |
|||||||
|
/* |
||||||
|
* 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.springframework; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.FileInputStream; |
||||||
|
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.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Properties; |
||||||
|
|
||||||
|
import org.gradle.api.DefaultTask; |
||||||
|
import org.gradle.api.GradleException; |
||||||
|
import org.gradle.api.Task; |
||||||
|
import org.gradle.api.file.DirectoryProperty; |
||||||
|
import org.gradle.api.file.FileCollection; |
||||||
|
import org.gradle.api.file.FileTree; |
||||||
|
import org.gradle.api.tasks.Classpath; |
||||||
|
import org.gradle.api.tasks.InputFiles; |
||||||
|
import org.gradle.api.tasks.OutputDirectory; |
||||||
|
import org.gradle.api.tasks.PathSensitive; |
||||||
|
import org.gradle.api.tasks.PathSensitivity; |
||||||
|
import org.gradle.api.tasks.SkipWhenEmpty; |
||||||
|
import org.gradle.api.tasks.TaskAction; |
||||||
|
import org.gradle.language.base.plugins.LifecycleBasePlugin; |
||||||
|
|
||||||
|
import org.springframework.core.io.support.SpringFactoriesLoader; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Task} that checks files loaded by {@link SpringFactoriesLoader}. |
||||||
|
* |
||||||
|
* @author Andy Wilkinson |
||||||
|
*/ |
||||||
|
public abstract class CheckFactoriesFile extends DefaultTask { |
||||||
|
|
||||||
|
private final String path; |
||||||
|
|
||||||
|
private FileCollection sourceFiles = getProject().getObjects().fileCollection(); |
||||||
|
|
||||||
|
private FileCollection classpath = getProject().getObjects().fileCollection(); |
||||||
|
|
||||||
|
protected CheckFactoriesFile(String path) { |
||||||
|
this.path = path; |
||||||
|
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName())); |
||||||
|
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); |
||||||
|
} |
||||||
|
|
||||||
|
@InputFiles |
||||||
|
@SkipWhenEmpty |
||||||
|
@PathSensitive(PathSensitivity.RELATIVE) |
||||||
|
public FileTree getSource() { |
||||||
|
return this.sourceFiles.getAsFileTree().matching((filter) -> filter.include(this.path)); |
||||||
|
} |
||||||
|
|
||||||
|
public void setSource(Object source) { |
||||||
|
this.sourceFiles = getProject().getObjects().fileCollection().from(source); |
||||||
|
} |
||||||
|
|
||||||
|
@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() { |
||||||
|
getSource().forEach(this::check); |
||||||
|
} |
||||||
|
|
||||||
|
private void check(File factoriesFile) { |
||||||
|
Properties factories = load(factoriesFile); |
||||||
|
Map<String, List<String>> problems = new LinkedHashMap<>(); |
||||||
|
for (String key : factories.stringPropertyNames()) { |
||||||
|
List<String> values = Arrays |
||||||
|
.asList(StringUtils.commaDelimitedListToStringArray(factories.getProperty(key))); |
||||||
|
for (String value : values) { |
||||||
|
boolean found = find(value); |
||||||
|
if (!found) { |
||||||
|
List<String> problemsForKey = problems.computeIfAbsent(key, (k) -> new ArrayList<>()); |
||||||
|
String binaryName = binaryNameOf(value); |
||||||
|
found = find(binaryName); |
||||||
|
if (found) { |
||||||
|
problemsForKey |
||||||
|
.add("'%s' should be listed using its binary name '%s'".formatted(value, binaryName)); |
||||||
|
} |
||||||
|
else { |
||||||
|
problemsForKey.add("'%s' was not found".formatted(value)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
List<String> sortedValues = new ArrayList<>(values); |
||||||
|
Collections.sort(sortedValues); |
||||||
|
if (!sortedValues.equals(values)) { |
||||||
|
List<String> problemsForKey = problems.computeIfAbsent(key, (k) -> new ArrayList<>()); |
||||||
|
problemsForKey.add("Entries should be sorted alphabetically"); |
||||||
|
} |
||||||
|
} |
||||||
|
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile(); |
||||||
|
writeReport(factoriesFile, problems, outputFile); |
||||||
|
if (!problems.isEmpty()) { |
||||||
|
throw new GradleException("%s check failed. See '%s' for details".formatted(this.path, outputFile)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private boolean find(String className) { |
||||||
|
for (File root : this.classpath.getFiles()) { |
||||||
|
String classFilePath = className.replace(".", "/") + ".class"; |
||||||
|
if (new File(root, classFilePath).isFile()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private String binaryNameOf(String className) { |
||||||
|
int lastDotIndex = className.lastIndexOf('.'); |
||||||
|
return className.substring(0, lastDotIndex) + "$" + className.substring(lastDotIndex + 1); |
||||||
|
} |
||||||
|
|
||||||
|
private Properties load(File aotFactories) { |
||||||
|
Properties properties = new Properties(); |
||||||
|
try (FileInputStream input = new FileInputStream(aotFactories)) { |
||||||
|
properties.load(input); |
||||||
|
return properties; |
||||||
|
} |
||||||
|
catch (IOException ex) { |
||||||
|
throw new UncheckedIOException(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void writeReport(File factoriesFile, Map<String, List<String>> problems, File outputFile) { |
||||||
|
outputFile.getParentFile().mkdirs(); |
||||||
|
StringBuilder report = new StringBuilder(); |
||||||
|
if (!problems.isEmpty()) { |
||||||
|
report.append("Found problems in '%s':%n".formatted(factoriesFile)); |
||||||
|
problems.forEach((key, problemsForKey) -> { |
||||||
|
report.append(" - %s:%n".formatted(key)); |
||||||
|
problemsForKey.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,32 @@ |
|||||||
|
/* |
||||||
|
* 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.springframework; |
||||||
|
|
||||||
|
import org.gradle.api.Task; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Task} that checks {@code META-INF/spring.factories}. |
||||||
|
* |
||||||
|
* @author Andy Wilkinson |
||||||
|
*/ |
||||||
|
public abstract class CheckSpringFactories extends CheckFactoriesFile { |
||||||
|
|
||||||
|
public CheckSpringFactories() { |
||||||
|
super("META-INF/spring.factories"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -1,2 +1,2 @@ |
|||||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\ |
org.springframework.aot.hint.RuntimeHintsRegistrar=\ |
||||||
org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary.ServiceLevelObjectiveBoundaryHints |
org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary$ServiceLevelObjectiveBoundaryHints |
||||||
|
|||||||
@ -1,2 +1,2 @@ |
|||||||
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ |
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ |
||||||
org.springframework.boot.test.context.SpringBootContextLoader.MainMethodBeanFactoryInitializationAotProcessor |
org.springframework.boot.test.context.SpringBootContextLoader$MainMethodBeanFactoryInitializationAotProcessor |
||||||
|
|||||||
@ -1,5 +1,5 @@ |
|||||||
org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\ |
org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\ |
||||||
org.springframework.boot.testcontainers.service.connection.ConnectionDetailsRegistrar.ServiceConnectionBeanRegistrationExcludeFilter |
org.springframework.boot.testcontainers.service.connection.ConnectionDetailsRegistrar$ServiceConnectionBeanRegistrationExcludeFilter |
||||||
|
|
||||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\ |
org.springframework.aot.hint.RuntimeHintsRegistrar=\ |
||||||
org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory.ContainerConnectionDetailsFactoriesRuntimeHints |
org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory$ContainerConnectionDetailsFactoriesRuntimeHints |
||||||
|
|||||||
Loading…
Reference in new issue