Merge branch '3.4.x'

Closes gh-44695
This commit is contained in:
Andy Wilkinson 2025-03-12 12:54:27 +00:00
commit b61578d183
11 changed files with 305 additions and 24 deletions

View File

@ -32,6 +32,7 @@ import io.spring.javaformat.gradle.tasks.CheckFormat;
import io.spring.javaformat.gradle.tasks.Format; import io.spring.javaformat.gradle.tasks.Format;
import org.gradle.api.JavaVersion; import org.gradle.api.JavaVersion;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.Dependency;
@ -54,6 +55,8 @@ import org.gradle.external.javadoc.CoreJavadocOptions;
import org.springframework.boot.build.architecture.ArchitecturePlugin; import org.springframework.boot.build.architecture.ArchitecturePlugin;
import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies; import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies;
import org.springframework.boot.build.optional.OptionalDependenciesPlugin; import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
import org.springframework.boot.build.springframework.CheckAotFactories;
import org.springframework.boot.build.springframework.CheckSpringFactories;
import org.springframework.boot.build.testing.TestFailuresPlugin; import org.springframework.boot.build.testing.TestFailuresPlugin;
import org.springframework.boot.build.toolchain.ToolchainPlugin; import org.springframework.boot.build.toolchain.ToolchainPlugin;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -98,6 +101,19 @@ import org.springframework.util.StringUtils;
* <li>{@code Implementation-Version} * <li>{@code Implementation-Version}
* </ul> * </ul>
* <li>{@code spring-boot-parent} is used for dependency management</li> * <li>{@code spring-boot-parent} is used for dependency management</li>
* <li>Additional checks are configured:
* <ul>
* <li>For all source sets:
* <ul>
* <li>Prohibited dependencies on the compile classpath
* <li>Prohibited dependencies on the runtime classpath
* </ul>
* <li>For the {@code main} source set:
* <ul>
* <li>{@code META-INF/spring/aot.factories}
* <li>{@code META-INF/spring.factories}
* </ul>
* </ul>
* </ul> * </ul>
* *
* <p/> * <p/>
@ -123,6 +139,7 @@ class JavaConventions {
configureDependencyManagement(project); configureDependencyManagement(project);
configureToolchain(project); configureToolchain(project);
configureProhibitedDependencyChecks(project); configureProhibitedDependencyChecks(project);
configureFactoriesFilesChecks(project);
}); });
} }
@ -304,4 +321,26 @@ class JavaConventions {
project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForProhibitedDependencies); project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForProhibitedDependencies);
} }
private void configureFactoriesFilesChecks(Project project) {
SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets();
sourceSets.matching((sourceSet) -> SourceSet.MAIN_SOURCE_SET_NAME.equals(sourceSet.getName()))
.configureEach((main) -> {
TaskProvider<Task> check = project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME);
TaskProvider<CheckAotFactories> checkAotFactories = project.getTasks()
.register("checkAotFactories", CheckAotFactories.class, (task) -> {
task.setSource(main.getResources());
task.setClasspath(main.getOutput().getClassesDirs());
task.setDescription("Checks the META-INF/spring/aot.factories file of the main source set.");
});
check.configure((task) -> task.dependsOn(checkAotFactories));
TaskProvider<CheckSpringFactories> checkSpringFactories = project.getTasks()
.register("checkSpringFactories", CheckSpringFactories.class, (task) -> {
task.setSource(main.getResources());
task.setClasspath(main.getOutput().getClassesDirs());
task.setDescription("Checks the META-INF/spring.factories file of the main source set.");
});
check.configure((task) -> task.dependsOn(checkSpringFactories));
});
}
} }

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}

View File

@ -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

View File

@ -38,8 +38,8 @@ org.springframework.boot.autoconfigure.ssl.BundleContentNotWatchableFailureAnaly
# Template Availability Providers # Template Availability Providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider

View File

@ -1,7 +1,7 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityRuntimeHints,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider$FreeMarkerTemplateAvailabilityRuntimeHints,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider.GroovyTemplateAvailabilityRuntimeHints,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider$GroovyTemplateAvailabilityRuntimeHints,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints,\ org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonAutoConfigurationRuntimeHints,\
org.springframework.boot.autoconfigure.template.TemplateRuntimeHints org.springframework.boot.autoconfigure.template.TemplateRuntimeHints
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\

View File

@ -4,8 +4,8 @@ org.springframework.boot.devtools.restart.RestartScopeInitializer
# Application Listeners # Application Listeners
org.springframework.context.ApplicationListener=\ org.springframework.context.ApplicationListener=\
org.springframework.boot.devtools.restart.RestartApplicationListener,\ org.springframework.boot.devtools.logger.DevToolsLogFactory$Listener,\
org.springframework.boot.devtools.logger.DevToolsLogFactory.Listener org.springframework.boot.devtools.restart.RestartApplicationListener
# Environment Post Processors # Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.boot.env.EnvironmentPostProcessor=\

View File

@ -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

View File

@ -1,8 +1,8 @@
# Logging Systems # Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\ org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\ org.springframework.boot.logging.java.JavaLoggingSystem$Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\ org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory
# PropertySource Loaders # PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertySourceLoader=\
@ -99,10 +99,10 @@ org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializerDetector
# Depends On Database Initialization Detectors # Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector,\
org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\ org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\
org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\ org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\
org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector,\
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector
# Resource Locator Protocol Resolvers # Resource Locator Protocol Resolvers
org.springframework.core.io.ProtocolResolver=\ org.springframework.core.io.ProtocolResolver=\

View File

@ -1,7 +1,7 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.ApplicationProperties.ApplicationPropertiesRuntimeHints,\ org.springframework.boot.ApplicationProperties$ApplicationPropertiesRuntimeHints,\
org.springframework.boot.SpringApplicationBannerPrinter.SpringApplicationBannerPrinterRuntimeHints,\ org.springframework.boot.SpringApplicationBannerPrinter$SpringApplicationBannerPrinterRuntimeHints,\
org.springframework.boot.WebApplicationType.WebApplicationTypeRuntimeHints,\ org.springframework.boot.WebApplicationType$WebApplicationTypeRuntimeHints,\
org.springframework.boot.context.config.ConfigDataLocationRuntimeHints,\ org.springframework.boot.context.config.ConfigDataLocationRuntimeHints,\
org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\ org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\
org.springframework.boot.env.PropertySourceRuntimeHints,\ org.springframework.boot.env.PropertySourceRuntimeHints,\
@ -10,16 +10,16 @@ org.springframework.boot.jdbc.DataSourceBuilderRuntimeHints,\
org.springframework.boot.json.JacksonRuntimeHints,\ org.springframework.boot.json.JacksonRuntimeHints,\
org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\ org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\
org.springframework.boot.logging.logback.LogbackRuntimeHints,\ org.springframework.boot.logging.logback.LogbackRuntimeHints,\
org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.ElasticCommonSchemaPropertiesRuntimeHints,\ org.springframework.boot.logging.structured.ElasticCommonSchemaProperties$ElasticCommonSchemaPropertiesRuntimeHints,\
org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.GraylogExtendedLogFormatPropertiesRuntimeHints,\ org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties$GraylogExtendedLogFormatPropertiesRuntimeHints,\
org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints,\ org.springframework.boot.logging.structured.StructuredLoggingJsonProperties$StructuredLoggingJsonPropertiesRuntimeHints,\
org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServerRuntimeHints,\ org.springframework.boot.web.embedded.undertow.UndertowWebServer$UndertowWebServerRuntimeHints,\
org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints org.springframework.boot.web.server.MimeMappings$MimeMappingsRuntimeHints
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.EnvironmentBeanFactoryInitializationAotProcessor,\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener$EnvironmentBeanFactoryInitializationAotProcessor,\
org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor,\ org.springframework.boot.jackson.JsonComponentModule$JsonComponentBeanFactoryInitializationAotProcessor,\
org.springframework.boot.logging.structured.StructuredLoggingJsonPropertiesBeanFactoryInitializationAotProcessor org.springframework.boot.logging.structured.StructuredLoggingJsonPropertiesBeanFactoryInitializationAotProcessor
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\