Close class resource InputStream in ThrowawayClassLoader

Prior to this commit, ThrowawayClassLoader#loadClassFromResource opened
an InputStream via getResourceAsStream(...) but never closed it. The
stream leaked on both the success path (after defineClass) and the
IOException path, as the surrounding try-block had neither a finally
nor a try-with-resources clause.

This commit adapts the existing inputStream variable as a
try-with-resources resource so that it is closed on every path, leaving
the loading logic unchanged.

Closes gh-36933

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
This commit is contained in:
junhyeong9812
2026-06-17 13:12:23 +02:00
committed by Sam Brannen
parent b611fcf114
commit 03d80feed0
2 changed files with 89 additions and 2 deletions
@@ -65,12 +65,11 @@ class ThrowawayClassLoader extends ClassLoader {
if (inputStream == null) {
return null;
}
try {
try (inputStream) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
inputStream.transferTo(outputStream);
byte[] bytes = outputStream.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
}
catch (IOException ex) {
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
@@ -0,0 +1,88 @@
/*
* Copyright 2002-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.aot.nativex.feature;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ThrowawayClassLoader}.
*
* @author Junhyeong Kim
*/
class ThrowawayClassLoaderTests {
@Test
void loadingClassFromResourceClosesInputStream() throws Exception {
String className = Probe.class.getName();
byte[] classBytes = classBytesOf(className);
AtomicBoolean closed = new AtomicBoolean();
// The grandparent resolves bootstrap classes only, so super.loadClass(...)
// fails for the probe class and ThrowawayClassLoader falls back to loading it
// from the resource stream provided below.
ClassLoader resourceLoader = new ClassLoader(new ClassLoader(null) {}) {
@Override
public InputStream getResourceAsStream(String name) {
return new TrackingInputStream(new ByteArrayInputStream(classBytes), closed);
}
};
ThrowawayClassLoader classLoader = new ThrowawayClassLoader(resourceLoader);
Class<?> loaded = classLoader.loadClass(className);
assertThat(loaded.getName()).isEqualTo(className);
assertThat(closed).as("InputStream closed").isTrue();
}
private static byte[] classBytesOf(String className) throws IOException {
String resourceName = className.replace('.', '/') + ".class";
try (InputStream in = ThrowawayClassLoaderTests.class.getClassLoader().getResourceAsStream(resourceName)) {
assertThat(in).as("class bytes for %s", className).isNotNull();
return in.readAllBytes();
}
}
static class Probe {
}
private static final class TrackingInputStream extends FilterInputStream {
private final AtomicBoolean closed;
TrackingInputStream(InputStream in, AtomicBoolean closed) {
super(in);
this.closed = closed;
}
@Override
public void close() throws IOException {
this.closed.set(true);
super.close();
}
}
}