From 2a907ba36985178d01d979fd69786420426bbcb3 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 14 Aug 2025 14:27:18 +0200 Subject: [PATCH] Improve null-safety of build-plugin/spring-boot-gradle-plugin See gh-46926 --- .../plugin/ApplicationPluginAction.java | 10 ++++++++- .../tasks/buildinfo/BuildInfoProperties.java | 21 ++++++++++++++----- .../gradle/tasks/bundling/BootBuildImage.java | 16 ++++++++------ .../tasks/bundling/BootZipCopyAction.java | 11 ++++++---- .../tasks/bundling/LoaderZipEntries.java | 9 +++++++- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java index 3ba1990a45a..82e3f245dfb 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java @@ -17,6 +17,7 @@ package org.springframework.boot.gradle.plugin; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.util.concurrent.Callable; @@ -38,6 +39,7 @@ import org.gradle.jvm.application.scripts.TemplateBasedScriptGenerator; import org.gradle.util.GradleVersion; import org.springframework.boot.gradle.tasks.run.BootRun; +import org.springframework.util.Assert; /** * Action that is executed in response to the {@link ApplicationPlugin} being applied. @@ -110,7 +112,7 @@ final class ApplicationPluginAction implements PluginApplicationAction { } private String loadResource(String name) { - try (InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream(name))) { + try (InputStreamReader reader = new InputStreamReader(getResourceAsStream(name))) { char[] buffer = new char[4096]; int read; StringWriter writer = new StringWriter(); @@ -124,6 +126,12 @@ final class ApplicationPluginAction implements PluginApplicationAction { } } + private InputStream getResourceAsStream(String name) { + InputStream stream = getClass().getResourceAsStream(name); + Assert.state(stream != null, "Resource '%s' not found'".formatted(name)); + return stream; + } + private void configureFilePermissions(CopySpec copySpec, int mode) { if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { copySpec.filePermissions((filePermissions) -> filePermissions.unix(Integer.toString(mode, 8))); diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java index 007233ddde0..1b7e8ec6021 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java @@ -48,7 +48,7 @@ public abstract class BuildInfoProperties implements Serializable { private final SetProperty excludes; - private final Supplier creationTime = () -> DateTimeFormatter.ISO_INSTANT.format(Instant.now()); + private final Supplier<@Nullable String> creationTime = () -> DateTimeFormatter.ISO_INSTANT.format(Instant.now()); @Inject public BuildInfoProperties(Project project, SetProperty excludes) { @@ -142,15 +142,19 @@ public abstract class BuildInfoProperties implements Serializable { return coerceToStringValues(applyExclusions(getAdditional().getOrElse(Collections.emptyMap()))); } + @SuppressWarnings("NullAway") // Doesn't detect lambda with correct nullability private @Nullable T getIfNotExcluded(Property property, String name) { return getIfNotExcluded(property, name, () -> null); } - private @Nullable T getIfNotExcluded(Property property, String name, Supplier defaultValue) { + private @Nullable T getIfNotExcluded(Property property, String name, Supplier<@Nullable T> defaultValue) { if (this.excludes.getOrElse(Collections.emptySet()).contains(name)) { return null; } - return property.getOrElse(defaultValue.get()); + if (property.isPresent()) { + return property.get(); + } + return defaultValue.get(); } private Map coerceToStringValues(Map input) { @@ -159,7 +163,9 @@ public abstract class BuildInfoProperties implements Serializable { if (value instanceof Provider provider) { value = provider.getOrNull(); } - output.put(key, (value != null) ? value.toString() : null); + if (value != null) { + output.put(key, value.toString()); + } }); return output; } @@ -167,7 +173,12 @@ public abstract class BuildInfoProperties implements Serializable { private Map applyExclusions(Map input) { Map output = new HashMap<>(); Set exclusions = this.excludes.getOrElse(Collections.emptySet()); - input.forEach((key, value) -> output.put(key, (!exclusions.contains(key)) ? value : null)); + input.forEach((key, value) -> { + boolean isExcluded = exclusions.contains(key); + if (!isExcluded) { + output.put(key, value); + } + }); return output; } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index db3767b3d33..1945c156bd6 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -41,6 +41,7 @@ import org.gradle.work.DisableCachingByDefault; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; @@ -460,14 +461,17 @@ public abstract class BootBuildImage extends DefaultTask { } private BuildRequest customizeCaches(BuildRequest request) { - if (this.buildWorkspace.asCache() != null) { - request = request.withBuildWorkspace((this.buildWorkspace.asCache())); + Cache buildWorkspaceCache = this.buildWorkspace.asCache(); + if (buildWorkspaceCache != null) { + request = request.withBuildWorkspace(buildWorkspaceCache); } - if (this.buildCache.asCache() != null) { - request = request.withBuildCache(this.buildCache.asCache()); + Cache buildCache = this.buildCache.asCache(); + if (buildCache != null) { + request = request.withBuildCache(buildCache); } - if (this.launchCache.asCache() != null) { - request = request.withLaunchCache(this.launchCache.asCache()); + Cache launchCache = this.launchCache.asCache(); + if (launchCache != null) { + request = request.withLaunchCache(launchCache); } return request; } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 509df0fbc0f..8974fe202bd 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.OffsetDateTime; @@ -111,7 +112,7 @@ class BootZipCopyAction implements CopyAction { private final Function compressionResolver; - private final String encoding; + private final @Nullable String encoding; private final ResolvedDependencies resolvedDependencies; @@ -125,7 +126,7 @@ class BootZipCopyAction implements CopyAction { @Nullable Integer fileMode, boolean includeDefaultLoader, @Nullable String jarmodeToolsLocation, Spec requiresUnpack, Spec exclusions, @Nullable LaunchScriptConfiguration launchScript, Spec librarySpec, - Function compressionResolver, String encoding, + Function compressionResolver, @Nullable String encoding, ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile, @Nullable LayerResolver layerResolver, LoaderImplementation loaderImplementation) { this.output = output; @@ -390,7 +391,8 @@ class BootZipCopyAction implements CopyAction { if (classPathIndex != null) { Set libraryNames = this.writtenLibraries.keySet(); List lines = libraryNames.stream().map((line) -> "- \"" + line + "\"").toList(); - ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines); + ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines((BootZipCopyAction.this.encoding != null) + ? BootZipCopyAction.this.encoding : StandardCharsets.UTF_8.name(), lines); writeEntry(classPathIndex, writer, true); } } @@ -415,7 +417,8 @@ class BootZipCopyAction implements CopyAction { } NativeImageArgFile argFile = new NativeImageArgFile(excludes); argFile.writeIfNecessary((lines) -> { - ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines); + ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines((BootZipCopyAction.this.encoding != null) + ? BootZipCopyAction.this.encoding : StandardCharsets.UTF_8.name(), lines); writeEntry(NativeImageArgFile.LOCATION, writer, true); }); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 6bcf79b9480..786b9c85409 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -30,6 +30,7 @@ import org.gradle.api.file.FileTreeElement; import org.jspecify.annotations.Nullable; import org.springframework.boot.loader.tools.LoaderImplementation; +import org.springframework.util.Assert; import org.springframework.util.StreamUtils; /** @@ -61,7 +62,7 @@ class LoaderZipEntries { WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( - getClass().getResourceAsStream("/" + this.loaderImplementation.getJarResourceName()))) { + getResourceAsStream("/" + this.loaderImplementation.getJarResourceName()))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { @@ -78,6 +79,12 @@ class LoaderZipEntries { return written; } + private InputStream getResourceAsStream(String name) { + InputStream stream = getClass().getResourceAsStream(name); + Assert.state(stream != null, "Resource '%s not found'".formatted(name)); + return stream; + } + private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { prepareEntry(entry, this.dirMode); out.putArchiveEntry(entry);