Skip to content

Commit 68e9460

Browse files
committed
Revisit compiler configuration in project build
This commit revisit the build configuration to enforce the following: * A single Java toolchain is used consistently with a recent Java version (here, Java 23) and language level * the main source is compiled with the Java 17 "-release" target * Multi-Release classes are compiled with their respective "-release" target. For now, only "spring-core" ships Java 21 variants. Closes gh-34507
1 parent 382caac commit 68e9460

File tree

13 files changed

+419
-126
lines changed

13 files changed

+419
-126
lines changed

build.gradle

-4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ plugins {
33
// kotlinVersion is managed in gradle.properties
44
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
55
id 'org.jetbrains.dokka' version '1.9.20'
6-
id 'com.github.ben-manes.versions' version '0.51.0'
76
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
8-
id 'de.undercouch.download' version '5.4.0'
97
id 'io.github.goooler.shadow' version '8.1.8' apply false
108
id 'me.champeau.jmh' version '0.7.2' apply false
11-
id 'me.champeau.mrjar' version '0.1.1'
129
id "net.ltgt.errorprone" version "4.1.0" apply false
1310
}
1411

@@ -64,7 +61,6 @@ configure([rootProject] + javaProjects) { project ->
6461
apply plugin: "java"
6562
apply plugin: "java-test-fixtures"
6663
apply plugin: 'org.springframework.build.conventions'
67-
apply from: "${rootDir}/gradle/toolchains.gradle"
6864
apply from: "${rootDir}/gradle/ide.gradle"
6965

7066
dependencies {

buildSrc/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ but doesn't affect the classpath of dependent projects.
3333
This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly`
3434
configurations are preferred.
3535

36+
### MultiRelease Jar
37+
38+
The `org.springframework.build.multiReleaseJar` plugin configures the project with MultiRelease JAR support.
39+
It creates a new SourceSet and dedicated tasks for each Java variant considered.
40+
This can be configured with the DSL, by setting a list of Java variants to configure:
41+
42+
```groovy
43+
plugins {
44+
id 'org.springframework.build.multiReleaseJar'
45+
}
46+
47+
multiRelease {
48+
releaseVersions 21, 24
49+
}
50+
```
51+
52+
Note, Java classes will be compiled with the toolchain pre-configured by the project, assuming that its
53+
Java language version is equal or higher than all variants we consider. Each compilation task will only
54+
set the "-release" compilation option accordingly to produce the expected bytecode version.
3655

3756
### RuntimeHints Java Agent
3857

buildSrc/build.gradle

+15-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ ext {
2020
dependencies {
2121
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
2222
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
23-
implementation "com.tngtech.archunit:archunit:1.3.0"
24-
implementation "org.gradle:test-retry-gradle-plugin:1.5.6"
23+
implementation "com.tngtech.archunit:archunit:1.4.0"
24+
implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
2525
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
2626
implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
27+
28+
testImplementation("org.assertj:assertj-core:${assertjVersion}")
29+
testImplementation("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}")
2730
}
2831

2932
gradlePlugin {
@@ -40,6 +43,10 @@ gradlePlugin {
4043
id = "org.springframework.build.localdev"
4144
implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin"
4245
}
46+
multiReleasePlugin {
47+
id = "org.springframework.build.multiReleaseJar"
48+
implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin"
49+
}
4350
optionalDependenciesPlugin {
4451
id = "org.springframework.build.optional-dependencies"
4552
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
@@ -50,3 +57,9 @@ gradlePlugin {
5057
}
5158
}
5259
}
60+
61+
test {
62+
useJUnitPlatform()
63+
}
64+
65+
jar.dependsOn check

buildSrc/gradle.properties

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
org.gradle.caching=true
22
javaFormatVersion=0.0.42
3+
junitJupiterVersion=5.11.4
4+
assertjVersion=3.27.3

buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static void configureNoHttpPlugin(Project project) {
6464
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
6565
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
6666
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
67-
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**");
67+
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**", "buildSrc/**");
6868
List<String> buildFolders = List.of("bin", "build", "out");
6969
project.allprojects(subproject -> {
7070
Path rootPath = project.getRootDir().toPath();

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

+34-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
1717
package org.springframework.build;
1818

1919
import java.util.ArrayList;
20-
import java.util.Arrays;
2120
import java.util.List;
2221

2322
import org.gradle.api.Plugin;
@@ -42,46 +41,64 @@ public class JavaConventions {
4241

4342
private static final List<String> TEST_COMPILER_ARGS;
4443

44+
/**
45+
* The Java version we should use as the JVM baseline for building the project
46+
*/
47+
private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(23);
48+
49+
/**
50+
* The Java version we should use as the baseline for the compiled bytecode (the "-release" compiler argument)
51+
*/
52+
private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17);
53+
4554
static {
46-
List<String> commonCompilerArgs = Arrays.asList(
55+
List<String> commonCompilerArgs = List.of(
4756
"-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
4857
"-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
4958
"-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options",
5059
"-parameters"
5160
);
5261
COMPILER_ARGS = new ArrayList<>();
5362
COMPILER_ARGS.addAll(commonCompilerArgs);
54-
COMPILER_ARGS.addAll(Arrays.asList(
63+
COMPILER_ARGS.addAll(List.of(
5564
"-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation",
5665
"-Xlint:unchecked", "-Werror"
5766
));
5867
TEST_COMPILER_ARGS = new ArrayList<>();
5968
TEST_COMPILER_ARGS.addAll(commonCompilerArgs);
60-
TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
69+
TEST_COMPILER_ARGS.addAll(List.of("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
6170
"-Xlint:-deprecation", "-Xlint:-unchecked"));
6271
}
6372

6473
public void apply(Project project) {
65-
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project));
74+
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> {
75+
applyToolchainConventions(project);
76+
applyJavaCompileConventions(project);
77+
});
6678
}
6779

6880
/**
69-
* Applies the common Java compiler options for main sources, test fixture sources, and
70-
* test sources.
81+
* Configure the Toolchain support for the project.
7182
* @param project the current project
7283
*/
73-
private void applyJavaCompileConventions(Project project) {
84+
private static void applyToolchainConventions(Project project) {
7485
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
7586
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
76-
toolchain.getLanguageVersion().set(JavaLanguageVersion.of(23));
87+
toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION);
7788
});
78-
SpringFrameworkExtension frameworkExtension = project.getExtensions().getByType(SpringFrameworkExtension.class);
89+
}
90+
91+
/**
92+
* Apply the common Java compiler options for main sources, test fixture sources, and
93+
* test sources.
94+
* @param project the current project
95+
*/
96+
private void applyJavaCompileConventions(Project project) {
7997
project.afterEvaluate(p -> {
8098
p.getTasks().withType(JavaCompile.class)
8199
.matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME))
82100
.forEach(compileTask -> {
83101
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
84-
compileTask.getOptions().getCompilerArgumentProviders().add(frameworkExtension.asArgumentProvider());
85102
compileTask.getOptions().setEncoding("UTF-8");
86103
setJavaRelease(compileTask);
87104
});
@@ -90,16 +107,19 @@ private void applyJavaCompileConventions(Project project) {
90107
|| compileTask.getName().equals("compileTestFixturesJava"))
91108
.forEach(compileTask -> {
92109
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
93-
compileTask.getOptions().getCompilerArgumentProviders().add(frameworkExtension.asArgumentProvider());
94110
compileTask.getOptions().setEncoding("UTF-8");
95111
setJavaRelease(compileTask);
96112
});
97113

98114
});
99115
}
100116

117+
/**
118+
* We should pick the {@link #DEFAULT_RELEASE_VERSION} for all compiled classes,
119+
* unless the current task is compiling multi-release JAR code with a higher version.
120+
*/
101121
private void setJavaRelease(JavaCompile task) {
102-
int defaultVersion = 17;
122+
int defaultVersion = DEFAULT_RELEASE_VERSION.asInt();
103123
int releaseVersion = defaultVersion;
104124
int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt();
105125
for (int version = defaultVersion ; version <= compilerVersion ; version++) {

buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@
2121

2222
import org.gradle.api.Project;
2323
import org.gradle.api.provider.Property;
24+
import org.gradle.api.tasks.compile.JavaCompile;
25+
import org.gradle.api.tasks.testing.Test;
2426
import org.gradle.process.CommandLineArgumentProvider;
2527

2628
public class SpringFrameworkExtension {
@@ -29,13 +31,18 @@ public class SpringFrameworkExtension {
2931

3032
public SpringFrameworkExtension(Project project) {
3133
this.enableJavaPreviewFeatures = project.getObjects().property(Boolean.class);
34+
project.getTasks().withType(JavaCompile.class).configureEach(javaCompile ->
35+
javaCompile.getOptions().getCompilerArgumentProviders().add(asArgumentProvider()));
36+
project.getTasks().withType(Test.class).configureEach(test ->
37+
test.getJvmArgumentProviders().add(asArgumentProvider()));
38+
3239
}
3340

3441
public Property<Boolean> getEnableJavaPreviewFeatures() {
3542
return this.enableJavaPreviewFeatures;
3643
}
3744

38-
public CommandLineArgumentProvider asArgumentProvider() {
45+
private CommandLineArgumentProvider asArgumentProvider() {
3946
return () -> {
4047
if (getEnableJavaPreviewFeatures().getOrElse(false)) {
4148
return List.of("--enable-preview");

buildSrc/src/main/java/org/springframework/build/TestConventions.java

-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ private void configureTests(Project project, Test test) {
6464
"--add-opens=java.base/java.util=ALL-UNNAMED",
6565
"-Xshare:off"
6666
);
67-
test.getJvmArgumentProviders().add(project.getExtensions()
68-
.getByType(SpringFrameworkExtension.class).asArgumentProvider());
6967
}
7068

7169
private void configureTestRetryPlugin(Project project, Test test) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2002-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.build.multirelease;
18+
19+
import javax.inject.Inject;
20+
21+
import org.gradle.api.artifacts.Configuration;
22+
import org.gradle.api.artifacts.ConfigurationContainer;
23+
import org.gradle.api.artifacts.dsl.DependencyHandler;
24+
import org.gradle.api.attributes.LibraryElements;
25+
import org.gradle.api.file.ConfigurableFileCollection;
26+
import org.gradle.api.file.FileCollection;
27+
import org.gradle.api.java.archives.Attributes;
28+
import org.gradle.api.model.ObjectFactory;
29+
import org.gradle.api.tasks.SourceSet;
30+
import org.gradle.api.tasks.SourceSetContainer;
31+
import org.gradle.api.tasks.TaskContainer;
32+
import org.gradle.api.tasks.TaskProvider;
33+
import org.gradle.api.tasks.bundling.Jar;
34+
import org.gradle.api.tasks.compile.JavaCompile;
35+
import org.gradle.api.tasks.testing.Test;
36+
import org.gradle.language.base.plugins.LifecycleBasePlugin;
37+
38+
/**
39+
* @author Cedric Champeau
40+
* @author Brian Clozel
41+
*/
42+
public abstract class MultiReleaseExtension {
43+
private final TaskContainer tasks;
44+
private final SourceSetContainer sourceSets;
45+
private final DependencyHandler dependencies;
46+
private final ObjectFactory objects;
47+
private final ConfigurationContainer configurations;
48+
49+
@Inject
50+
public MultiReleaseExtension(SourceSetContainer sourceSets,
51+
ConfigurationContainer configurations,
52+
TaskContainer tasks,
53+
DependencyHandler dependencies,
54+
ObjectFactory objectFactory) {
55+
this.sourceSets = sourceSets;
56+
this.configurations = configurations;
57+
this.tasks = tasks;
58+
this.dependencies = dependencies;
59+
this.objects = objectFactory;
60+
}
61+
62+
public void releaseVersions(int... javaVersions) {
63+
releaseVersions("src/main/", "src/test/", javaVersions);
64+
}
65+
66+
private void releaseVersions(String mainSourceDirectory, String testSourceDirectory, int... javaVersions) {
67+
for (int javaVersion : javaVersions) {
68+
addLanguageVersion(javaVersion, mainSourceDirectory, testSourceDirectory);
69+
}
70+
}
71+
72+
private void addLanguageVersion(int javaVersion, String mainSourceDirectory, String testSourceDirectory) {
73+
String javaN = "java" + javaVersion;
74+
75+
SourceSet langSourceSet = sourceSets.create(javaN, srcSet -> srcSet.getJava().srcDir(mainSourceDirectory + javaN));
76+
SourceSet testSourceSet = sourceSets.create(javaN + "Test", srcSet -> srcSet.getJava().srcDir(testSourceDirectory + javaN));
77+
SourceSet sharedSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME);
78+
SourceSet sharedTestSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME);
79+
80+
FileCollection mainClasses = objects.fileCollection().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs());
81+
dependencies.add(javaN + "Implementation", mainClasses);
82+
83+
tasks.named(langSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
84+
task.getOptions().getRelease().set(javaVersion)
85+
);
86+
tasks.named(testSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
87+
task.getOptions().getRelease().set(javaVersion)
88+
);
89+
90+
TaskProvider<Test> testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet);
91+
tasks.named("check", task -> task.dependsOn(testTask));
92+
93+
configureMultiReleaseJar(javaVersion, langSourceSet);
94+
}
95+
96+
private TaskProvider<Test> createTestTask(int javaVersion, SourceSet testSourceSet, SourceSet sharedTestSourceSet, SourceSet langSourceSet, SourceSet sharedSourceSet) {
97+
Configuration testImplementation = configurations.getByName(testSourceSet.getImplementationConfigurationName());
98+
testImplementation.extendsFrom(configurations.getByName(sharedTestSourceSet.getImplementationConfigurationName()));
99+
Configuration testCompileOnly = configurations.getByName(testSourceSet.getCompileOnlyConfigurationName());
100+
testCompileOnly.extendsFrom(configurations.getByName(sharedTestSourceSet.getCompileOnlyConfigurationName()));
101+
testCompileOnly.getDependencies().add(dependencies.create(langSourceSet.getOutput().getClassesDirs()));
102+
testCompileOnly.getDependencies().add(dependencies.create(sharedSourceSet.getOutput().getClassesDirs()));
103+
104+
Configuration testRuntimeClasspath = configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName());
105+
// so here's the deal. MRjars are JARs! Which means that to execute tests, we need
106+
// the JAR on classpath, not just classes + resources as Gradle usually does
107+
testRuntimeClasspath.getAttributes()
108+
.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR));
109+
110+
TaskProvider<Test> testTask = tasks.register("java" + javaVersion + "Test", Test.class, test -> {
111+
test.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
112+
113+
ConfigurableFileCollection testClassesDirs = objects.fileCollection();
114+
testClassesDirs.from(testSourceSet.getOutput());
115+
testClassesDirs.from(sharedTestSourceSet.getOutput());
116+
test.setTestClassesDirs(testClassesDirs);
117+
ConfigurableFileCollection classpath = objects.fileCollection();
118+
// must put the MRJar first on classpath
119+
classpath.from(tasks.named("jar"));
120+
// then we put the specific test sourceset tests, so that we can override
121+
// the shared versions
122+
classpath.from(testSourceSet.getOutput());
123+
124+
// then we add the shared tests
125+
classpath.from(sharedTestSourceSet.getRuntimeClasspath());
126+
test.setClasspath(classpath);
127+
});
128+
return testTask;
129+
}
130+
131+
private void configureMultiReleaseJar(int version, SourceSet languageSourceSet) {
132+
tasks.named("jar", Jar.class, jar -> {
133+
jar.into("META-INF/versions/" + version, s -> s.from(languageSourceSet.getOutput()));
134+
Attributes attributes = jar.getManifest().getAttributes();
135+
attributes.put("Multi-Release", "true");
136+
});
137+
}
138+
139+
}

0 commit comments

Comments
 (0)