From 55b5610dd9d5929b55c60442b92a2cc0f8fb83b2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 23:12:19 -0700 Subject: [PATCH] Add Maven and Gradle option for the loader implementation to use Add properties to the Maven and Gradle plugins so that users can switch between the two loader modules. See gh-37669 --- .../gradle/tasks/bundling/BootArchive.java | 11 ++++ .../tasks/bundling/BootArchiveSupport.java | 13 +++-- .../boot/gradle/tasks/bundling/BootJar.java | 8 ++- .../boot/gradle/tasks/bundling/BootWar.java | 8 ++- .../tasks/bundling/BootZipCopyAction.java | 10 +++- .../tasks/bundling/LoaderZipEntries.java | 9 ++- .../AbstractBootArchiveIntegrationTests.java | 10 ++++ .../bundling/AbstractBootArchiveTests.java | 12 ++++ ...otJarIntegrationTests-classicLoader.gradle | 9 +++ ...otWarIntegrationTests-classicLoader.gradle | 9 +++ .../spring-boot-loader-tools/build.gradle | 24 +++++++- .../boot/loader/tools/AbstractJarWriter.java | 14 ++--- .../boot/loader/tools/Layouts.java | 2 +- .../loader/tools/LoaderClassesWriter.java | 10 +++- .../loader/tools/LoaderImplementation.java | 51 ++++++++++++++++ .../boot/loader/tools/Packager.java | 12 +++- .../boot/maven/JarIntegrationTests.java | 20 +++++++ .../projects/jar-with-classic-loader/pom.xml | 58 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 24 ++++++++ .../boot/maven/AbstractPackagerMojo.java | 11 ++++ .../boot/maven/BuildImageMojo.java | 13 +++++ .../boot/maven/RepackageMojo.java | 13 +++++ .../build.gradle | 4 ++ 23 files changed, 330 insertions(+), 25 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java index 1f3875a45a..2da97a470c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java @@ -33,6 +33,8 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A Spring Boot "fat" archive task. * @@ -133,4 +135,13 @@ public interface BootArchive extends Task { */ void resolvedArtifacts(Provider> resolvedArtifacts); + /** + * The loader implementation that should be used with the archive. + * @return the loader implementation + * @since 3.2.0 + */ + @Input + @Optional + Property getLoaderImplementation(); + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 921e9f3d48..22b086248a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -42,6 +42,8 @@ import org.gradle.api.tasks.WorkResult; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.util.PatternSet; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * Support class for implementations of {@link BootArchive}. * @@ -116,12 +118,13 @@ class BootArchiveSupport { return (version != null) ? version : "unknown"; } - CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies) { - return createCopyAction(jar, resolvedDependencies, null, null); + CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, + LoaderImplementation loaderImplementation) { + return createCopyAction(jar, resolvedDependencies, loaderImplementation, null, null); } - CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, - String layerToolsLocation) { + CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, + LoaderImplementation loaderImplementation, LayerResolver layerResolver, String layerToolsLocation) { File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); @@ -136,7 +139,7 @@ class BootArchiveSupport { String encoding = jar.getMetadataCharset(); CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode, includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, - compressionResolver, encoding, resolvedDependencies, layerResolver); + compressionResolver, encoding, resolvedDependencies, layerResolver, loaderImplementation); return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 5cf51bb850..c76a95f1d6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -37,6 +37,8 @@ import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.bundling.Jar; import org.gradle.work.DisableCachingByDefault; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A custom {@link Jar} task that produces a Spring Boot executable jar. * @@ -141,12 +143,14 @@ public abstract class BootJar extends Jar implements BootArchive { @Override protected CopyAction createCopyAction() { + LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, + layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index d697f00a1e..d3aa0eab86 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -37,6 +37,8 @@ import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.bundling.War; import org.gradle.work.DisableCachingByDefault; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A custom {@link War} task that produces a Spring Boot executable war. * @@ -115,12 +117,14 @@ public abstract class BootWar extends War implements BootArchive { @Override protected CopyAction createCopyAction() { + LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, + layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 8e3a57e435..1f35d482fb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -57,6 +57,7 @@ import org.springframework.boot.loader.tools.JarModeLibrary; import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.LayersIndex; import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.NativeImageArgFile; import org.springframework.boot.loader.tools.ReachabilityMetadataProperties; import org.springframework.util.Assert; @@ -111,11 +112,14 @@ class BootZipCopyAction implements CopyAction { private final LayerResolver layerResolver; + private final LoaderImplementation loaderImplementation; + BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode, boolean includeDefaultLoader, String layerToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function compressionResolver, String encoding, - ResolvedDependencies resolvedDependencies, LayerResolver layerResolver) { + ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, + LoaderImplementation loaderImplementation) { this.output = output; this.manifest = manifest; this.preserveFileTimestamps = preserveFileTimestamps; @@ -131,6 +135,7 @@ class BootZipCopyAction implements CopyAction { this.encoding = encoding; this.resolvedDependencies = resolvedDependencies; this.layerResolver = layerResolver; + this.loaderImplementation = loaderImplementation; } @Override @@ -310,7 +315,8 @@ class BootZipCopyAction implements CopyAction { // Always write loader entries after META-INF directory (see gh-16698) return; } - LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode()); + LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode(), + BootZipCopyAction.this.loaderImplementation); this.writtenLoaderEntries = loaderEntries.writeTo(this.out); if (BootZipCopyAction.this.layerResolver != null) { for (String name : this.writtenLoaderEntries.getFiles()) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 7606a3d66a..8a7851f07c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -28,6 +28,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.gradle.api.file.FileTreeElement; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.StreamUtils; /** @@ -39,22 +40,26 @@ import org.springframework.util.StreamUtils; */ class LoaderZipEntries { + private final LoaderImplementation loaderImplementation; + private final Long entryTime; private final int dirMode; private final int fileMode; - LoaderZipEntries(Long entryTime, int dirMode, int fileMode) { + LoaderZipEntries(Long entryTime, int dirMode, int fileMode, LoaderImplementation loaderImplementation) { this.entryTime = entryTime; this.dirMode = dirMode; this.fileMode = fileMode; + this.loaderImplementation = (loaderImplementation != null) ? loaderImplementation + : LoaderImplementation.DEFAULT; } WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( - getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader-classic.jar"))) { + getClass().getResourceAsStream("/" + this.loaderImplementation.getJarResourceName()))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index a01d3bd415..45c10a127d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -106,6 +106,16 @@ abstract class AbstractBootArchiveIntegrationTests { assertThat(firstHash).isEqualTo(secondHash); } + @TestTemplate + void classicLoader() throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0]; + try (JarFile jarFile = new JarFile(jar)) { + assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + } + } + @TestTemplate void upToDateWhenBuiltTwice() { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 17623f9ed4..3dffc00752 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -65,6 +65,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.gradle.junit.GradleProjectBuilder; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -279,6 +280,17 @@ abstract class AbstractBootArchiveTests { } } + @Test + void loaderIsWrittenToTheRootOfTheJarWhenUsingClassicLoader() throws IOException { + this.task.getMainClass().set("com.example.Main"); + this.task.getLoaderImplementation().set(LoaderImplementation.CLASSIC); + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); + } + } + @Test void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException { this.task.getMainClass().set("com.example.Main"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle new file mode 100644 index 0000000000..2e9e26c99c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle new file mode 100644 index 0000000000..fd14cc1a64 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle @@ -0,0 +1,9 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 80311be0ac..f7968f659d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -13,6 +13,10 @@ configurations { extendsFrom dependencyManagement transitive = false } + loaderClassic { + extendsFrom dependencyManagement + transitive = false + } jarmode { extendsFrom dependencyManagement transitive = false @@ -36,7 +40,8 @@ dependencies { compileOnly("ch.qos.logback:logback-classic") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loaderClassic(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) @@ -57,6 +62,21 @@ task reproducibleLoaderJar(type: Jar) { } reproducibleFileOrder = true preserveFileTimestamps = false + archiveFileName = "spring-boot-loader.jar" + destinationDirectory = file("${generatedResources}/META-INF/loader") +} + +task reproducibleLoaderClassicJar(type: Jar) { + dependsOn configurations.loaderClassic + from { + zipTree(configurations.loaderClassic.incoming.files.singleFile).matching { + exclude "META-INF/LICENSE.txt" + exclude "META-INF/NOTICE.txt" + exclude "META-INF/spring-boot.properties" + } + } + reproducibleFileOrder = true + preserveFileTimestamps = false archiveFileName = "spring-boot-loader-classic.jar" destinationDirectory = file("${generatedResources}/META-INF/loader") } @@ -72,7 +92,7 @@ task layerToolsJar(type: Sync) { sourceSets { main { - output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar]) + output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index 23282dbea7..7e807b1f9d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -51,8 +51,6 @@ import org.apache.commons.compress.archivers.zip.UnixStat; */ public abstract class AbstractJarWriter implements LoaderClassesWriter { - private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader-classic.jar"; - private static final int BUFFER_SIZE = 32 * 1024; private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; @@ -199,13 +197,15 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { return library.getLastModified(); } - /** - * Write the required spring-boot-loader classes to the JAR. - * @throws IOException if the classes cannot be written - */ @Override public void writeLoaderClasses() throws IOException { - writeLoaderClasses(NESTED_LOADER_JAR); + writeLoaderClasses(LoaderImplementation.DEFAULT); + } + + @Override + public void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException { + writeLoaderClasses((loaderImplementation != null) ? loaderImplementation.getJarResourceName() + : LoaderImplementation.DEFAULT.getJarResourceName()); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index 82c73ef859..e6f99282a7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -23,7 +23,7 @@ import java.util.Locale; import java.util.Map; /** - * Common {@link Layout}s. + * Common {@link Layout layouts}. * * @author Phillip Webb * @author Dave Syer diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java index 187ff0b902..864992279d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -34,6 +34,14 @@ public interface LoaderClassesWriter { */ void writeLoaderClasses() throws IOException; + /** + * Write the default required spring-boot-loader classes to the JAR. + * @param loaderImplementation the specific implementation to write + * @throws IOException if the classes cannot be written + * @since 3.2.0 + */ + void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException; + /** * Write custom required spring-boot-loader classes to the JAR. * @param loaderJarResourceName the name of the resource containing the loader classes diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java new file mode 100644 index 0000000000..6414a3cfbb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.loader.tools; + +/** + * Supported loader implementations. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public enum LoaderImplementation { + + /** + * The default recommended loader implementation. + */ + DEFAULT("META-INF/loader/spring-boot-loader.jar"), + + /** + * The classic loader implementation as used with Spring Boot 3.1 and earlier. + */ + CLASSIC("META-INF/loader/spring-boot-loader-classic.jar"); + + private final String jarResourceName; + + LoaderImplementation(String jarResourceName) { + this.jarResourceName = jarResourceName; + } + + /** + * Return the name of the nested resource that can be loaded from the tools jar. + * @return the jar resource name + */ + public String getJarResourceName() { + return this.jarResourceName; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 815e0ee4a9..af4dff2330 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -88,6 +88,8 @@ public abstract class Packager { private Layout layout; + private LoaderImplementation loaderImplementation; + private LayoutFactory layoutFactory; private Layers layers; @@ -135,6 +137,14 @@ public abstract class Packager { this.layout = layout; } + /** + * Sets the loader implementation to use. + * @param loaderImplementation the loaderImplementation to set + */ + public void setLoaderImplementation(LoaderImplementation loaderImplementation) { + this.loaderImplementation = loaderImplementation; + } + /** * Sets the layout factory for the jar. The factory can be used when no specific * layout is specified. @@ -215,7 +225,7 @@ public abstract class Packager { customLoaderLayout.writeLoadedClasses(writer); } else if (layout.isExecutable()) { - writer.writeLoaderClasses(); + writer.writeLoaderClasses(this.loaderImplementation); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index 1d37949cfa..c903da23a2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -75,6 +75,26 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { }); } + @TestTemplate + void whenJarWithClassicLoaderIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-classic-loader").goals("install").execute((project) -> { + File original = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).isFile(); + File repackaged = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(launchScript(repackaged)).isEmpty(); + assertThat(jar(repackaged)).manifest((manifest) -> { + manifest.hasMainClass("org.springframework.boot.loader.launch.JarLauncher"); + manifest.hasStartClass("some.random.Main"); + manifest.hasAttribute("Not-Used", "Foo"); + }).hasEntryWithName("org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(buildLog(project)) + .contains("Replacing main artifact " + repackaged + " with repackaged archive,") + .contains("The original artifact has been renamed to " + original) + .contains("Installing " + repackaged + " to") + .doesNotContain("Installing " + original + " to"); + }); + } + @TestTemplate void whenAttachIsDisabledOnlyTheOriginalJarIsInstalled(MavenBuild mavenBuild) { mavenBuild.project("jar-attach-disabled").goals("install").execute((project) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml new file mode 100644 index 0000000000..64d9d04f99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-with-classic-loader + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + some.random.Main + + + Foo + + + CLASSIC + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..5e51546d4e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index b6dfdc04bb..28d55d213a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -47,6 +47,7 @@ import org.springframework.boot.loader.tools.Layouts.Jar; import org.springframework.boot.loader.tools.Layouts.None; import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.Packager; import org.springframework.boot.loader.tools.layer.CustomLayers; @@ -128,6 +129,15 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo return null; } + /** + * Return the loader implementation that should be used. + * @return the loader implementation or {@code null} + * @since 3.2.0 + */ + protected LoaderImplementation getLoaderImplementation() { + return null; + } + /** * Return the layout factory that will be used to determine the {@link LayoutType} if * no explicit layout is set. @@ -145,6 +155,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo */ protected

P getConfiguredPackager(Supplier

supplier) { P packager = supplier.get(); + packager.setLoaderImplementation(getLoaderImplementation()); packager.setLayoutFactory(getLayoutFactory()); packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog)); packager.setMainClass(this.mainClass); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 84589c0189..79b62bf530 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -48,6 +48,7 @@ import org.springframework.boot.loader.tools.EntryWriter; import org.springframework.boot.loader.tools.ImagePackager; import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.StringUtils; /** @@ -187,6 +188,13 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { @Parameter private LayoutType layout; + /** + * The loader implementation that should be used. + * @since 3.2.0 + */ + @Parameter + private LoaderImplementation loaderImplementation; + /** * The layout factory that will be used to create the executable archive if no * explicit layout is set. Alternative layouts implementations can be provided by 3rd @@ -206,6 +214,11 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { return this.layout; } + @Override + protected LoaderImplementation getLoaderImplementation() { + return this.loaderImplementation; + } + /** * Return the layout factory that will be used to determine the * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 5ca2ecac5a..13a16c2a14 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -36,6 +36,7 @@ import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.Repackager; /** @@ -161,6 +162,13 @@ public class RepackageMojo extends AbstractPackagerMojo { @Parameter(property = "spring-boot.repackage.layout") private LayoutType layout; + /** + * The loader implementation that should be used. + * @since 3.2.0 + */ + @Parameter + private LoaderImplementation loaderImplementation; + /** * The layout factory that will be used to create the executable archive if no * explicit layout is set. Alternative layouts implementations can be provided by 3rd @@ -180,6 +188,11 @@ public class RepackageMojo extends AbstractPackagerMojo { return this.layout; } + @Override + protected LoaderImplementation getLoaderImplementation() { + return this.loaderImplementation; + } + /** * Return the layout factory that will be used to determine the * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle index 37596c6206..16f7a57ebe 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle @@ -16,3 +16,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.webjars:jquery:3.5.0") } + +bootJar { + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} \ No newline at end of file