From 1d102349e90200d0f24b44e734e7a0d62acd9f22 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 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. Re-throw the original ClassNotFoundException when the resource fallback yields no class. 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 1d12a525a8bb..0fe6d7a1f2d2 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 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE 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 71fe5732372a..9adaa688583a 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 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 @@ public InputStream getResourceAsStream(String name) { 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)) {