Skip to content

Throw ClassNotFoundException for missing class resource in ThrowawayClassLoader#36938

Closed
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/throwawayclassloader-loadclass-contract
Closed

Throw ClassNotFoundException for missing class resource in ThrowawayClassLoader#36938
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/throwawayclassloader-loadclass-contract

Conversation

@junhyeong9812

Copy link
Copy Markdown
Contributor

Overview

ThrowawayClassLoader.loadClass can currently return null, which violates
the ClassLoader.loadClass contract (it must return a non-null Class or
throw ClassNotFoundException). This change makes the fallback path honor the
contract.

Note: I found this while working on #36933 (closing the class-resource
InputStream in the same class). Since #36933 has already been triaged into
the 7.1.0-M1 milestone, I kept it focused and split this contract fix into a
separate PR rather than expanding the accepted one. The two changes touch
different lines of loadClassFromResource and are independent.

Problem

loadClass(name, resolve) delegates to super.loadClass(name, true) and, on
ClassNotFoundException, falls back to loadClassFromResource(name):

catch (ClassNotFoundException ex) {
    return loadClassFromResource(name);
}

loadClassFromResource returns null when the .class resource is not
available:

InputStream inputStream = this.resourceLoader.getResourceAsStream(resourceName);
if (inputStream == null) {
    return null;
}

So loadClass returns null when both the standard delegation and the
resource fallback fail to find the class. The caller
PreComputeFieldFeature.provideFieldValue immediately dereferences the result:

Class<?> throwawayClass = this.throwawayClassLoader.loadClass(field.getDeclaringClass().getName());
Field throwawayField = throwawayClass.getDeclaredField(field.getName()); // NPE when null

Fix

Re-throw the original ClassNotFoundException when the resource fallback yields
no class, so loadClass never returns null:

catch (ClassNotFoundException ex) {
    Class<?> loadedFromResource = loadClassFromResource(name);
    if (loadedFromResource == null) {
        throw ex;
    }
    return loadedFromResource;
}

The original exception is preserved because it carries the most accurate
delegation-failure detail; the resource fallback only confirms there is no
class-byte resource to define.

Note on impact

This is primarily a contract-correctness and diagnostics fix, not a crash fix.
The NullPointerException in PreComputeFieldFeature is caught by a broad
catch (Throwable) that falls back to runtime evaluation of the field, so the
build does not fail today. The observable improvement is twofold:

  1. loadClass honors the documented ClassLoader contract.
  2. The verbose build-time diagnostic now reports a meaningful
    ClassNotFoundException (with the class name) instead of a confusing
    NullPointerException.

The trigger is narrow: it requires a declaring class whose .class resource is
not resolvable (e.g. a runtime-generated class) that also cannot be loaded via
delegation.

Tests

Added ThrowawayClassLoaderTests.loadClassThrowsClassNotFoundExceptionWhenClassResourceIsMissing,
which drives loadClass through the fallback with a resource loader that
returns no class bytes and asserts that ClassNotFoundException is thrown
(previously loadClass returned null). ./gradlew :spring-core:test and
checkstyleMain/checkstyleTest pass.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 16, 2026
@sbrannen sbrannen changed the title Throw ClassNotFoundException for missing class resource Throw ClassNotFoundException for missing class resource in ThrowawayClassLoader Jun 17, 2026
@sbrannen sbrannen added in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jun 17, 2026
@sbrannen sbrannen self-assigned this Jun 17, 2026
@sbrannen sbrannen added this to the 7.1.0-M1 milestone Jun 17, 2026
@sbrannen

Copy link
Copy Markdown
Member

Please rebase on main and force push your changes to the PR.

@sbrannen sbrannen added the status: waiting-for-feedback We need additional information before we can continue label Jun 17, 2026
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 <pickjog@gmail.com>
@junhyeong9812 junhyeong9812 force-pushed the fix/throwawayclassloader-loadclass-contract branch from e48ed10 to 1d10234 Compare June 17, 2026 12:56
@junhyeong9812

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look, @sbrannen! I've rebased onto the latest main and force-pushed. Since #36933 was already merged, the only conflict was in the test class, which I resolved by keeping both tests. The loadClass fix in this PR is independent of that change, so it sits cleanly on top.

Please let me know if anything else is needed. Thanks again for your time!

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 17, 2026
@sbrannen sbrannen removed the status: feedback-provided Feedback has been provided label Jun 17, 2026
@sbrannen sbrannen closed this in 233e7b9 Jun 17, 2026
sbrannen added a commit that referenced this pull request Jun 17, 2026
@sbrannen

Copy link
Copy Markdown
Member

This has been merged into main in 233e7b9 and slightly revised in 7b31e0c.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants