From 7f3cba12b90f8124e07ffcee3d981c784e6fb0ea Mon Sep 17 00:00:00 2001 From: akenra <37288280+akenra@users.noreply.github.com> Date: Thu, 30 Jul 2026 05:39:01 +0500 Subject: [PATCH] fix(gcp): register nested URL protocol handler in GcfJarLauncher GcfJarLauncher's constructor bypasses Launcher.launch() and creates a LaunchedClassLoader directly. Unlike Launcher.launch(), the constructor never called Handlers.register(), which meant the JVM could not resolve nested: URLs. When JarUrlClassLoader tried to resolve BOOT-INF/lib/ entries it threw java.net.MalformedURLException: unknown protocol: nested. Replace the commented-out JarFile.registerUrlProtocolHandler() (which registered the jar: protocol in Spring Boot 2.x) with Handlers.register(), the Boot 4.x equivalent that registers the nested: protocol handler. Fixes gh-1336 Signed-off-by: akenra <37288280+akenra@users.noreply.github.com> --- .../function/adapter/gcp/GcfJarLauncher.java | 4 +- .../adapter/gcp/GcfJarLauncherTests.java | 107 ++++++++++++++++++ .../src/test/resources/ProtocolCheck.java | 39 +++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncherTests.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/resources/ProtocolCheck.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncher.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncher.java index 53e22a6dd..3234bbf28 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncher.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncher.java @@ -23,6 +23,7 @@ import com.google.cloud.functions.RawBackgroundFunction; import org.springframework.boot.loader.launch.JarLauncher; +import org.springframework.boot.loader.net.protocol.Handlers; /** * The launcher class written at the top-level of the output JAR to be deployed to @@ -30,6 +31,7 @@ * * @author Ray Tsang * @author Daniel Zou + * @author Roman Akentev */ public class GcfJarLauncher extends JarLauncher implements HttpFunction, RawBackgroundFunction { @@ -38,7 +40,7 @@ public class GcfJarLauncher extends JarLauncher implements HttpFunction, RawBack private final Object delegate; public GcfJarLauncher() throws Exception { - //JarFile.registerUrlProtocolHandler(); + Handlers.register(); this.loader = createClassLoader(getClassPathUrls()); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncherTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncherTests.java new file mode 100644 index 000000000..95b38b5fa --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/GcfJarLauncherTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023-present 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.cloud.function.adapter.gcp; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests verifying that {@link GcfJarLauncher} registers the {@code nested:} + * URL protocol handler via {@code Handlers.register()} before creating its + * classloader. Without this, {@code JarUrlClassLoader} would fail with + * {@code java.net.MalformedURLException: unknown protocol: nested} when + * trying to resolve JARs inside the repackaged archive (GH-1336). + *
+ * The test forks a subprocess to avoid interference from other Spring Boot + * classes that may have already registered the protocol in the current JVM. + * + * @author Roman Akentev + * @see GH-1336 + */ +public class GcfJarLauncherTests { + + public static final String PROTOCOL_ALREADY_REGISTERED = "PROTOCOL_ALREADY_REGISTERED"; + + public static final String PROTOCOL_NOT_REGISTERED = "PROTOCOL_NOT_REGISTERED"; + + public static final String GCF_JAR_LAUNCHER_SUCCEEDED = "GCF_JAR_LAUNCHER_SUCCEEDED"; + + public static final String GCF_JAR_LAUNCHER_FAILED = "GCF_JAR_LAUNCHER_FAILED"; + + public static final String PROTOCOL_NOW_REGISTERED = "PROTOCOL_NOW_REGISTERED"; + + public static final String PROTOCOL_STILL_NOT_REGISTERED = "PROTOCOL_STILL_NOT_REGISTERED"; + + @TempDir + Path tempDir; + + @Test + public void nestedProtocolIsRegisteredByGcfJarLauncher() throws Exception { + String javaHome = System.getProperty("java.home"); + String classPath = System.getProperty("java.class.path"); + + Path sourceFile = tempDir.resolve("ProtocolCheck.java"); + try (InputStream in = Objects.requireNonNull( + getClass().getResourceAsStream("/ProtocolCheck.java"), + "Resource /ProtocolCheck.java not found")) { + Files.copy(in, sourceFile, StandardCopyOption.REPLACE_EXISTING); + } + + Process compile = new ProcessBuilder( + Path.of(javaHome, "bin", "javac").toString(), + "-cp", classPath, + "-d", tempDir.toString(), + sourceFile.toString()) + .redirectErrorStream(true) + .start(); + String compileOutput = new BufferedReader( + new InputStreamReader(compile.getInputStream())) + .lines().collect(Collectors.joining("\n")); + int compileExit = compile.waitFor(); + assertThat(compileExit) + .as("compilation failed:\n" + compileOutput) + .isZero(); + + Process run = new ProcessBuilder( + Path.of(javaHome, "bin", "java").toString(), + "-cp", classPath + File.pathSeparatorChar + tempDir, + "ProtocolCheck") + .redirectErrorStream(true) + .start(); + String output = new BufferedReader(new InputStreamReader(run.getInputStream())) + .lines().collect(Collectors.joining("\n")); + int exitCode = run.waitFor(); + + assertThat(exitCode).as("ProtocolCheck exit code").isZero(); + assertThat(output).as("ProtocolCheck output") + .contains(PROTOCOL_NOT_REGISTERED + ": unknown protocol: nested") + .contains(PROTOCOL_NOW_REGISTERED); + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/resources/ProtocolCheck.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/resources/ProtocolCheck.java new file mode 100644 index 000000000..731f06c01 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/resources/ProtocolCheck.java @@ -0,0 +1,39 @@ +import org.springframework.cloud.function.adapter.gcp.GcfJarLauncher; +import org.springframework.cloud.function.adapter.gcp.GcfJarLauncherTests; + +/** + * Subprocess entry point for {@link GcfJarLauncherTests}. Compiled and executed + * at runtime to verify that the {@code nested:} protocol handler is registered + * after {@code GcfJarLauncher} construction (GH-1336). + * + * @author Roman Akentev + */ +public class ProtocolCheck { + + public static void main(String[] args) throws Exception { + try { + new java.net.URL("nested:/dev/null"); + System.out.println(GcfJarLauncherTests.PROTOCOL_ALREADY_REGISTERED); + } + catch (java.net.MalformedURLException e) { + System.out.println(GcfJarLauncherTests.PROTOCOL_NOT_REGISTERED + ": " + e.getMessage()); + } + try { + new GcfJarLauncher(); + System.out.println(GcfJarLauncherTests.GCF_JAR_LAUNCHER_SUCCEEDED); + } + catch (Exception e) { + System.out.println(GcfJarLauncherTests.GCF_JAR_LAUNCHER_FAILED + ": " + + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.out); + } + try { + new java.net.URL("nested:/dev/null"); + System.out.println(GcfJarLauncherTests.PROTOCOL_NOW_REGISTERED); + } + catch (java.net.MalformedURLException e) { + System.out.println(GcfJarLauncherTests.PROTOCOL_STILL_NOT_REGISTERED + ": " + e.getMessage()); + } + } + +}