From 233e7b91f9b5aa9fc2be92fa1adc3ed1e4180cdb Mon Sep 17 00:00:00 2001 From: junhyeong9812 Date: Wed, 17 Jun 2026 07:31:34 +0900 Subject: [PATCH] Throw ClassNotFoundException for missing class resource in ThrowawayClassLoader Prior to this commit, ThrowawayClassLoader.loadClass fell back to loadClassFromResource(), which returns null when no class resource is available. Returning null from loadClass violates the ClassLoader contract and leads to a NullPointerException in callers such as PreComputeFieldFeature. To address that, this commit rethrows the original ClassNotFoundException when the resource fallback yields no class. Closes gh-36938 Signed-off-by: junhyeong9812 --- .../nativex/feature/ThrowawayClassLoader.java | 6 +++++- .../feature/ThrowawayClassLoaderTests.java | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/feature/ThrowawayClassLoader.java b/spring-core/src/main/java/org/springframework/aot/nativex/feature/ThrowawayClassLoader.java index 1d12a525a8b..0fe6d7a1f2d 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/feature/ThrowawayClassLoader.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/feature/ThrowawayClassLoader.java @@ -54,7 +54,11 @@ class ThrowawayClassLoader extends ClassLoader { return super.loadClass(name, true); } catch (ClassNotFoundException ex) { - return loadClassFromResource(name); + Class loadedFromResource = loadClassFromResource(name); + if (loadedFromResource == null) { + throw ex; + } + return loadedFromResource; } } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/feature/ThrowawayClassLoaderTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/feature/ThrowawayClassLoaderTests.java index 71fe5732372..9adaa688583 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/feature/ThrowawayClassLoaderTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/feature/ThrowawayClassLoaderTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link ThrowawayClassLoader}. @@ -56,6 +57,23 @@ class ThrowawayClassLoaderTests { assertThat(closed).as("InputStream closed").isTrue(); } + @Test + void loadClassThrowsClassNotFoundExceptionWhenClassResourceIsMissing() { + // The grandparent resolves bootstrap classes only, so super.loadClass(...) fails, + // and the resource loader provides no class bytes. The fallback must then honor the + // ClassLoader.loadClass contract by reporting the failure instead of returning null. + ClassLoader resourceLoader = new ClassLoader(new ClassLoader(null) {}) { + @Override + public InputStream getResourceAsStream(String name) { + return null; + } + }; + ThrowawayClassLoader classLoader = new ThrowawayClassLoader(resourceLoader); + + assertThatExceptionOfType(ClassNotFoundException.class) + .isThrownBy(() -> classLoader.loadClass("com.example.MissingClass")); + } + private static byte[] classBytesOf(String className) throws IOException { String resourceName = className.replace('.', '/') + ".class"; try (InputStream in = ThrowawayClassLoaderTests.class.getClassLoader().getResourceAsStream(resourceName)) {