Skip to content

Commit b61578d

Browse files
committedMar 12, 2025
Merge branch '3.4.x'
Closes gh-44695
2 parents 901fead + 08b358e commit b61578d

File tree

11 files changed

+305
-24
lines changed

11 files changed

+305
-24
lines changed
 

‎buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java

+39
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.spring.javaformat.gradle.tasks.Format;
3333
import org.gradle.api.JavaVersion;
3434
import org.gradle.api.Project;
35+
import org.gradle.api.Task;
3536
import org.gradle.api.artifacts.Configuration;
3637
import org.gradle.api.artifacts.ConfigurationContainer;
3738
import org.gradle.api.artifacts.Dependency;
@@ -54,6 +55,8 @@
5455
import org.springframework.boot.build.architecture.ArchitecturePlugin;
5556
import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies;
5657
import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
58+
import org.springframework.boot.build.springframework.CheckAotFactories;
59+
import org.springframework.boot.build.springframework.CheckSpringFactories;
5760
import org.springframework.boot.build.testing.TestFailuresPlugin;
5861
import org.springframework.boot.build.toolchain.ToolchainPlugin;
5962
import org.springframework.util.StringUtils;
@@ -98,6 +101,19 @@
98101
* <li>{@code Implementation-Version}
99102
* </ul>
100103
* <li>{@code spring-boot-parent} is used for dependency management</li>
104+
* <li>Additional checks are configured:
105+
* <ul>
106+
* <li>For all source sets:
107+
* <ul>
108+
* <li>Prohibited dependencies on the compile classpath
109+
* <li>Prohibited dependencies on the runtime classpath
110+
* </ul>
111+
* <li>For the {@code main} source set:
112+
* <ul>
113+
* <li>{@code META-INF/spring/aot.factories}
114+
* <li>{@code META-INF/spring.factories}
115+
* </ul>
116+
* </ul>
101117
* </ul>
102118
*
103119
* <p/>
@@ -123,6 +139,7 @@ void apply(Project project) {
123139
configureDependencyManagement(project);
124140
configureToolchain(project);
125141
configureProhibitedDependencyChecks(project);
142+
configureFactoriesFilesChecks(project);
126143
});
127144
}
128145

@@ -304,4 +321,26 @@ private void createProhibitedDependenciesCheck(Configuration classpath, Project
304321
project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForProhibitedDependencies);
305322
}
306323

324+
private void configureFactoriesFilesChecks(Project project) {
325+
SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets();
326+
sourceSets.matching((sourceSet) -> SourceSet.MAIN_SOURCE_SET_NAME.equals(sourceSet.getName()))
327+
.configureEach((main) -> {
328+
TaskProvider<Task> check = project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME);
329+
TaskProvider<CheckAotFactories> checkAotFactories = project.getTasks()
330+
.register("checkAotFactories", CheckAotFactories.class, (task) -> {
331+
task.setSource(main.getResources());
332+
task.setClasspath(main.getOutput().getClassesDirs());
333+
task.setDescription("Checks the META-INF/spring/aot.factories file of the main source set.");
334+
});
335+
check.configure((task) -> task.dependsOn(checkAotFactories));
336+
TaskProvider<CheckSpringFactories> checkSpringFactories = project.getTasks()
337+
.register("checkSpringFactories", CheckSpringFactories.class, (task) -> {
338+
task.setSource(main.getResources());
339+
task.setClasspath(main.getOutput().getClassesDirs());
340+
task.setDescription("Checks the META-INF/spring.factories file of the main source set.");
341+
});
342+
check.configure((task) -> task.dependsOn(checkSpringFactories));
343+
});
344+
}
345+
307346
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2012-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.springframework;
18+
19+
import org.gradle.api.Task;
20+
21+
/**
22+
* {@link Task} that checks {@code META-INF/spring/aot.factories}.
23+
*
24+
* @author Andy Wilkinson
25+
*/
26+
public abstract class CheckAotFactories extends CheckFactoriesFile {
27+
28+
public CheckAotFactories() {
29+
super("META-INF/spring/aot.factories");
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2012-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.springframework;
18+
19+
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.IOException;
22+
import java.io.UncheckedIOException;
23+
import java.nio.file.Files;
24+
import java.nio.file.StandardOpenOption;
25+
import java.util.ArrayList;
26+
import java.util.Arrays;
27+
import java.util.Collections;
28+
import java.util.LinkedHashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.Properties;
32+
33+
import org.gradle.api.DefaultTask;
34+
import org.gradle.api.GradleException;
35+
import org.gradle.api.Task;
36+
import org.gradle.api.file.DirectoryProperty;
37+
import org.gradle.api.file.FileCollection;
38+
import org.gradle.api.file.FileTree;
39+
import org.gradle.api.tasks.Classpath;
40+
import org.gradle.api.tasks.InputFiles;
41+
import org.gradle.api.tasks.OutputDirectory;
42+
import org.gradle.api.tasks.PathSensitive;
43+
import org.gradle.api.tasks.PathSensitivity;
44+
import org.gradle.api.tasks.SkipWhenEmpty;
45+
import org.gradle.api.tasks.TaskAction;
46+
import org.gradle.language.base.plugins.LifecycleBasePlugin;
47+
48+
import org.springframework.core.io.support.SpringFactoriesLoader;
49+
import org.springframework.util.StringUtils;
50+
51+
/**
52+
* {@link Task} that checks files loaded by {@link SpringFactoriesLoader}.
53+
*
54+
* @author Andy Wilkinson
55+
*/
56+
public abstract class CheckFactoriesFile extends DefaultTask {
57+
58+
private final String path;
59+
60+
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
61+
62+
private FileCollection classpath = getProject().getObjects().fileCollection();
63+
64+
protected CheckFactoriesFile(String path) {
65+
this.path = path;
66+
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
67+
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
68+
}
69+
70+
@InputFiles
71+
@SkipWhenEmpty
72+
@PathSensitive(PathSensitivity.RELATIVE)
73+
public FileTree getSource() {
74+
return this.sourceFiles.getAsFileTree().matching((filter) -> filter.include(this.path));
75+
}
76+
77+
public void setSource(Object source) {
78+
this.sourceFiles = getProject().getObjects().fileCollection().from(source);
79+
}
80+
81+
@Classpath
82+
public FileCollection getClasspath() {
83+
return this.classpath;
84+
}
85+
86+
public void setClasspath(Object classpath) {
87+
this.classpath = getProject().getObjects().fileCollection().from(classpath);
88+
}
89+
90+
@OutputDirectory
91+
public abstract DirectoryProperty getOutputDirectory();
92+
93+
@TaskAction
94+
void execute() {
95+
getSource().forEach(this::check);
96+
}
97+
98+
private void check(File factoriesFile) {
99+
Properties factories = load(factoriesFile);
100+
Map<String, List<String>> problems = new LinkedHashMap<>();
101+
for (String key : factories.stringPropertyNames()) {
102+
List<String> values = Arrays
103+
.asList(StringUtils.commaDelimitedListToStringArray(factories.getProperty(key)));
104+
for (String value : values) {
105+
boolean found = find(value);
106+
if (!found) {
107+
List<String> problemsForKey = problems.computeIfAbsent(key, (k) -> new ArrayList<>());
108+
String binaryName = binaryNameOf(value);
109+
found = find(binaryName);
110+
if (found) {
111+
problemsForKey
112+
.add("'%s' should be listed using its binary name '%s'".formatted(value, binaryName));
113+
}
114+
else {
115+
problemsForKey.add("'%s' was not found".formatted(value));
116+
}
117+
}
118+
}
119+
List<String> sortedValues = new ArrayList<>(values);
120+
Collections.sort(sortedValues);
121+
if (!sortedValues.equals(values)) {
122+
List<String> problemsForKey = problems.computeIfAbsent(key, (k) -> new ArrayList<>());
123+
problemsForKey.add("Entries should be sorted alphabetically");
124+
}
125+
}
126+
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
127+
writeReport(factoriesFile, problems, outputFile);
128+
if (!problems.isEmpty()) {
129+
throw new GradleException("%s check failed. See '%s' for details".formatted(this.path, outputFile));
130+
}
131+
}
132+
133+
private boolean find(String className) {
134+
for (File root : this.classpath.getFiles()) {
135+
String classFilePath = className.replace(".", "/") + ".class";
136+
if (new File(root, classFilePath).isFile()) {
137+
return true;
138+
}
139+
}
140+
return false;
141+
}
142+
143+
private String binaryNameOf(String className) {
144+
int lastDotIndex = className.lastIndexOf('.');
145+
return className.substring(0, lastDotIndex) + "$" + className.substring(lastDotIndex + 1);
146+
}
147+
148+
private Properties load(File aotFactories) {
149+
Properties properties = new Properties();
150+
try (FileInputStream input = new FileInputStream(aotFactories)) {
151+
properties.load(input);
152+
return properties;
153+
}
154+
catch (IOException ex) {
155+
throw new UncheckedIOException(ex);
156+
}
157+
}
158+
159+
private void writeReport(File factoriesFile, Map<String, List<String>> problems, File outputFile) {
160+
outputFile.getParentFile().mkdirs();
161+
StringBuilder report = new StringBuilder();
162+
if (!problems.isEmpty()) {
163+
report.append("Found problems in '%s':%n".formatted(factoriesFile));
164+
problems.forEach((key, problemsForKey) -> {
165+
report.append(" - %s:%n".formatted(key));
166+
problemsForKey.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
167+
});
168+
}
169+
try {
170+
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
171+
StandardOpenOption.TRUNCATE_EXISTING);
172+
}
173+
catch (IOException ex) {
174+
throw new UncheckedIOException(ex);
175+
}
176+
}
177+
178+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2012-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.springframework;
18+
19+
import org.gradle.api.Task;
20+
21+
/**
22+
* {@link Task} that checks {@code META-INF/spring.factories}.
23+
*
24+
* @author Andy Wilkinson
25+
*/
26+
public abstract class CheckSpringFactories extends CheckFactoriesFile {
27+
28+
public CheckSpringFactories() {
29+
super("META-INF/spring.factories");
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
org.springframework.aot.hint.RuntimeHintsRegistrar=\
2-
org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary.ServiceLevelObjectiveBoundaryHints
2+
org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary$ServiceLevelObjectiveBoundaryHints

‎spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ org.springframework.boot.autoconfigure.ssl.BundleContentNotWatchableFailureAnaly
3838
# Template Availability Providers
3939
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
4040
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
41-
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
4241
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
42+
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
4343
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
4444
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
4545

‎spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
org.springframework.aot.hint.RuntimeHintsRegistrar=\
2-
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityRuntimeHints,\
3-
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider.GroovyTemplateAvailabilityRuntimeHints,\
4-
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints,\
2+
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider$FreeMarkerTemplateAvailabilityRuntimeHints,\
3+
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider$GroovyTemplateAvailabilityRuntimeHints,\
4+
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonAutoConfigurationRuntimeHints,\
55
org.springframework.boot.autoconfigure.template.TemplateRuntimeHints
66

77
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\

‎spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ org.springframework.boot.devtools.restart.RestartScopeInitializer
44

55
# Application Listeners
66
org.springframework.context.ApplicationListener=\
7-
org.springframework.boot.devtools.restart.RestartApplicationListener,\
8-
org.springframework.boot.devtools.logger.DevToolsLogFactory.Listener
7+
org.springframework.boot.devtools.logger.DevToolsLogFactory$Listener,\
8+
org.springframework.boot.devtools.restart.RestartApplicationListener
99

1010
# Environment Post Processors
1111
org.springframework.boot.env.EnvironmentPostProcessor=\
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\
2-
org.springframework.boot.testcontainers.service.connection.ConnectionDetailsRegistrar.ServiceConnectionBeanRegistrationExcludeFilter
2+
org.springframework.boot.testcontainers.service.connection.ConnectionDetailsRegistrar$ServiceConnectionBeanRegistrationExcludeFilter
33

44
org.springframework.aot.hint.RuntimeHintsRegistrar=\
5-
org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory.ContainerConnectionDetailsFactoriesRuntimeHints
5+
org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory$ContainerConnectionDetailsFactoriesRuntimeHints

‎spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Logging Systems
22
org.springframework.boot.logging.LoggingSystemFactory=\
3-
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
4-
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
5-
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
3+
org.springframework.boot.logging.java.JavaLoggingSystem$Factory,\
4+
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory,\
5+
org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory
66

77
# PropertySource Loaders
88
org.springframework.boot.env.PropertySourceLoader=\
@@ -99,10 +99,10 @@ org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializerDetector
9999

100100
# Depends On Database Initialization Detectors
101101
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
102-
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector,\
103102
org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\
104103
org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\
105-
org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector
104+
org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector,\
105+
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector
106106

107107
# Resource Locator Protocol Resolvers
108108
org.springframework.core.io.ProtocolResolver=\

0 commit comments

Comments
 (0)
Failed to load comments.