[core] Disable Caching in URLConnection for ClasspathClassLoader

Fixes #4899
This commit is contained in:
Andreas Dangel
2024-04-11 13:11:54 +02:00
parent 6d9c49858f
commit 62f215929c
5 changed files with 112 additions and 9 deletions

View File

@ -53,6 +53,16 @@ public class ClasspathClassLoader extends URLClassLoader {
static {
registerAsParallelCapable();
// Disable caching for jar files to prevent issues like #4899
try {
// Uses a pseudo URL to be able to call URLConnection#setDefaultUseCaches
// with Java9+ there is a static method for that per protocol:
// https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/URLConnection.html#setDefaultUseCaches(java.lang.String,boolean)
URI.create("jar:file:file.jar!/").toURL().openConnection().setDefaultUseCaches(false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public ClasspathClassLoader(List<File> files, ClassLoader parent) throws IOException {

View File

@ -17,6 +17,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -60,27 +65,111 @@ class ClasspathClassLoaderTest {
}
}
@Test
void loadFromJar() throws IOException {
final String RESOURCE_NAME = "net/sourceforge/pmd/Sample.txt";
final String TEST_CONTENT = "Test\n";
private static final String CUSTOM_JAR_RESOURCE = "net/sourceforge/pmd/Sample.txt";
private static final String CUSTOM_JAR_RESOURCE2 = "net/sourceforge/pmd/Sample2.txt";
private static final String CUSTOM_JAR_RESOURCE_CONTENT = "Test\n";
private Path prepareCustomJar() throws IOException {
Path jarPath = tempDir.resolve("custom.jar");
try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(jarPath))) {
out.putNextEntry(new ZipEntry(RESOURCE_NAME));
out.write(TEST_CONTENT.getBytes(StandardCharsets.UTF_8));
out.putNextEntry(new ZipEntry(CUSTOM_JAR_RESOURCE));
out.write(CUSTOM_JAR_RESOURCE_CONTENT.getBytes(StandardCharsets.UTF_8));
out.putNextEntry(new ZipEntry(CUSTOM_JAR_RESOURCE2));
out.write(CUSTOM_JAR_RESOURCE_CONTENT.getBytes(StandardCharsets.UTF_8));
}
return jarPath;
}
@Test
void loadFromJar() throws IOException {
Path jarPath = prepareCustomJar();
String classpath = jarPath.toString();
try (ClasspathClassLoader loader = new ClasspathClassLoader(classpath, null)) {
try (InputStream in = loader.getResourceAsStream(RESOURCE_NAME)) {
try (InputStream in = loader.getResourceAsStream(CUSTOM_JAR_RESOURCE)) {
assertNotNull(in);
String s = IOUtil.readToString(in, StandardCharsets.UTF_8);
assertEquals(TEST_CONTENT, s);
assertEquals(CUSTOM_JAR_RESOURCE_CONTENT, s);
}
}
}
/**
* @see <a href="https://github.com/pmd/pmd/issues/4899">[java] Parsing failed in ParseLock#doParse() java.io.IOException: Stream closed #4899</a>
*/
@Test
void loadMultithreadedFromJar() throws IOException, InterruptedException {
Path jarPath = prepareCustomJar();
String classpath = jarPath.toString();
int numberOfThreads = 2;
final CyclicBarrier waitForClosed = new CyclicBarrier(numberOfThreads);
final Semaphore grabResource = new Semaphore(1);
final List<Exception> caughtExceptions = new ArrayList<>();
class ThreadRunnable extends Thread {
private final int number;
ThreadRunnable(int number) {
super("Thread" + number);
this.number = number;
}
@Override
public void run() {
try (ClasspathClassLoader loader = new ClasspathClassLoader(classpath, null)) {
// Make sure, the threads get the resource stream one after another, so that the
// underlying Jar File is definitively cached (if caching is enabled).
grabResource.acquire();
InputStream stream;
try {
stream = loader.getResourceAsStream(CUSTOM_JAR_RESOURCE);
} finally {
grabResource.release();
}
try (InputStream in = stream) {
assertNotNull(in);
if (number > 0) {
// all except the first thread should wait until the first thread is finished
// and has closed the ClasspathClassLoader
waitForClosed.await();
}
String s = IOUtil.readToString(in, StandardCharsets.UTF_8);
assertEquals(CUSTOM_JAR_RESOURCE_CONTENT, s);
}
} catch (Exception e) {
caughtExceptions.add(e);
throw new RuntimeException(e);
} finally {
try {
if (number == 0) {
// signal the other waiting threads to continue. Here, we have closed
// already the ClasspathClassLoader.
waitForClosed.await();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
}
List<Thread> threads = new ArrayList<>(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
threads.add(new ThreadRunnable(i));
}
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
assertTrue(caughtExceptions.isEmpty());
}
/**
* Verifies, that we load the class files from the runtime image of the correct java home.
* This tests multiple versions, in order to avoid that the test accidentally is successful when