[core] Disable Caching in URLConnection for ClasspathClassLoader
Fixes #4899
This commit is contained in:
@ -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 {
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user