diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index 557d2628a60..73fee9d6624 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -32,6 +32,7 @@ import io.spring.javaformat.gradle.tasks.CheckFormat; import io.spring.javaformat.gradle.tasks.Format; import org.gradle.api.JavaVersion; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; 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.classpath.CheckClasspathForProhibitedDependencies; 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.toolchain.ToolchainPlugin; import org.springframework.util.StringUtils; @@ -98,6 +101,19 @@ import org.springframework.util.StringUtils; *
  • {@code Implementation-Version} * *
  • {@code spring-boot-parent} is used for dependency management
  • + *
  • Additional checks are configured: + * * * *

    @@ -123,6 +139,7 @@ class JavaConventions { configureDependencyManagement(project); configureToolchain(project); configureProhibitedDependencyChecks(project); + configureFactoriesFilesChecks(project); }); } @@ -304,4 +321,26 @@ class JavaConventions { 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 check = project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME); + TaskProvider 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 = 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)); + }); + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckAotFactories.java b/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckAotFactories.java new file mode 100644 index 00000000000..643f2930253 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckAotFactories.java @@ -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"); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckFactoriesFile.java b/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckFactoriesFile.java new file mode 100644 index 00000000000..766f443aea7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckFactoriesFile.java @@ -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> problems = new LinkedHashMap<>(); + for (String key : factories.stringPropertyNames()) { + List values = Arrays + .asList(StringUtils.commaDelimitedListToStringArray(factories.getProperty(key))); + for (String value : values) { + boolean found = find(value); + if (!found) { + List 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 sortedValues = new ArrayList<>(values); + Collections.sort(sortedValues); + if (!sortedValues.equals(values)) { + List 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> 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); + } + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckSpringFactories.java b/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckSpringFactories.java new file mode 100644 index 00000000000..6436b8756e1 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/springframework/CheckSpringFactories.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/aot.factories index 2625462c354..e5251df8307 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,2 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ -org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary.ServiceLevelObjectiveBoundaryHints +org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary$ServiceLevelObjectiveBoundaryHints diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 9eb9419cdbe..23dc687cea1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -38,8 +38,8 @@ org.springframework.boot.autoconfigure.ssl.BundleContentNotWatchableFailureAnaly # Template Availability Providers org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\ -org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\ +org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories index 80ce2b10358..17302dfd9b9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories @@ -1,7 +1,7 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ -org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityRuntimeHints,\ -org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider.GroovyTemplateAvailabilityRuntimeHints,\ -org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints,\ +org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider$FreeMarkerTemplateAvailabilityRuntimeHints,\ +org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider$GroovyTemplateAvailabilityRuntimeHints,\ +org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonAutoConfigurationRuntimeHints,\ org.springframework.boot.autoconfigure.template.TemplateRuntimeHints org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ diff --git a/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories index f96fa063b40..baec9665ae1 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories @@ -4,8 +4,8 @@ org.springframework.boot.devtools.restart.RestartScopeInitializer # Application Listeners 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 org.springframework.boot.env.EnvironmentPostProcessor=\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories index 5b3d49bd502..4d6de8757e6 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/aot.factories @@ -1,5 +1,5 @@ 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.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory.ContainerConnectionDetailsFactoriesRuntimeHints +org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory$ContainerConnectionDetailsFactoriesRuntimeHints diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index 6ec2d815a64..5852cdce2d5 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -1,8 +1,8 @@ # Logging Systems org.springframework.boot.logging.LoggingSystemFactory=\ -org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\ -org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\ -org.springframework.boot.logging.java.JavaLoggingSystem.Factory +org.springframework.boot.logging.java.JavaLoggingSystem$Factory,\ +org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory,\ +org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory # PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ @@ -99,10 +99,10 @@ org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializerDetector # Depends On Database Initialization Detectors org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ -org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector,\ org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\ 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 org.springframework.core.io.ProtocolResolver=\ diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index 4e5f3ad071e..5d4127122e5 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -1,7 +1,7 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ -org.springframework.boot.ApplicationProperties.ApplicationPropertiesRuntimeHints,\ -org.springframework.boot.SpringApplicationBannerPrinter.SpringApplicationBannerPrinterRuntimeHints,\ -org.springframework.boot.WebApplicationType.WebApplicationTypeRuntimeHints,\ +org.springframework.boot.ApplicationProperties$ApplicationPropertiesRuntimeHints,\ +org.springframework.boot.SpringApplicationBannerPrinter$SpringApplicationBannerPrinterRuntimeHints,\ +org.springframework.boot.WebApplicationType$WebApplicationTypeRuntimeHints,\ org.springframework.boot.context.config.ConfigDataLocationRuntimeHints,\ org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\ org.springframework.boot.env.PropertySourceRuntimeHints,\ @@ -10,16 +10,16 @@ org.springframework.boot.jdbc.DataSourceBuilderRuntimeHints,\ org.springframework.boot.json.JacksonRuntimeHints,\ org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\ org.springframework.boot.logging.logback.LogbackRuntimeHints,\ -org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.ElasticCommonSchemaPropertiesRuntimeHints,\ -org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.GraylogExtendedLogFormatPropertiesRuntimeHints,\ -org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints,\ -org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServerRuntimeHints,\ -org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints +org.springframework.boot.logging.structured.ElasticCommonSchemaProperties$ElasticCommonSchemaPropertiesRuntimeHints,\ +org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties$GraylogExtendedLogFormatPropertiesRuntimeHints,\ +org.springframework.boot.logging.structured.StructuredLoggingJsonProperties$StructuredLoggingJsonPropertiesRuntimeHints,\ +org.springframework.boot.web.embedded.undertow.UndertowWebServer$UndertowWebServerRuntimeHints,\ +org.springframework.boot.web.server.MimeMappings$MimeMappingsRuntimeHints org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\ -org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.EnvironmentBeanFactoryInitializationAotProcessor,\ -org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor,\ +org.springframework.boot.env.EnvironmentPostProcessorApplicationListener$EnvironmentBeanFactoryInitializationAotProcessor,\ +org.springframework.boot.jackson.JsonComponentModule$JsonComponentBeanFactoryInitializationAotProcessor,\ org.springframework.boot.logging.structured.StructuredLoggingJsonPropertiesBeanFactoryInitializationAotProcessor org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\