[core] Support loading classes from runtime images

That's one missing piece to support running PMD on a different JRE
than the one used for analyzing. For Java up to version 8, one can put
lib/rt.jar on the auxclasspath. But since Java 9, the runtime classes
are stored in runtime images, that need to be loaded through the
jrt:/ file system.

Since the jrt-URLs are not connected anymore to a specific runtime
image, AsmSymbolResolver needs to use directly the streams instead
of URLs.

The ClasspathClassLoader is basically disabled to actually load
classes for reflection (#loadClass). In PMD 7, we shouldn't use that
anymore.

See also https://openjdk.org/jeps/220
This commit is contained in:
Andreas Dangel committed 2023-07-06 17:38:09 +02:00
1 parent 3e2de67190
commit 1fcb1077d0
6 files changed
+266 -60

No files matched your search

@@ -7,14 +7,27 @@ package net.sourceforge.pmd.internal.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@@ -33,29 +46,37 @@ public class ClasspathClassLoader extends URLClassLoader {
private static final Logger LOG = LoggerFactory.getLogger(ClasspathClassLoader.class);
private FileSystem fileSystem;
String javaHome;
private Map<String, Set<String>> packagesDirsToModules;
static {
registerAsParallelCapable();
}
public ClasspathClassLoader(List<File> files, ClassLoader parent) throws IOException {
super(fileToURL(files), parent);
super(new URL[0], parent);
for (URL url : fileToURL(files)) {
addURL(url);
}
}
public ClasspathClassLoader(String classpath, ClassLoader parent) throws IOException {
super(initURLs(classpath), parent);
super(new URL[0], parent);
for (URL url : initURLs(classpath)) {
addURL(url);
}
}
private static URL[] fileToURL(List<File> files) throws IOException {
private List<URL> fileToURL(List<File> files) throws IOException {
List<URL> urlList = new ArrayList<>();
for (File f : files) {
urlList.add(f.toURI().toURL());
}
return urlList.toArray(new URL[0]);
return urlList;
}
private static URL[] initURLs(String classpath) {
private List<URL> initURLs(String classpath) {
AssertionUtil.requireParamNotNull("classpath", classpath);
final List<URL> urls = new ArrayList<>();
try {
@@ -69,10 +90,10 @@ public class ClasspathClassLoader extends URLClassLoader {
} catch (IOException e) {
throw new IllegalArgumentException("Cannot prepend classpath " + classpath + "\n" + e.getMessage(), e);
}
return urls.toArray(new URL[0]);
return urls;
}
private static void addClasspathURLs(final List<URL> urls, final String classpath) throws MalformedURLException {
private void addClasspathURLs(final List<URL> urls, final String classpath) throws MalformedURLException {
StringTokenizer toker = new StringTokenizer(classpath, File.pathSeparator);
while (toker.hasMoreTokens()) {
String token = toker.nextToken();
@@ -81,7 +102,7 @@ public class ClasspathClassLoader extends URLClassLoader {
}
}
private static void addFileURLs(List<URL> urls, URL fileURL) throws IOException {
private void addFileURLs(List<URL> urls, URL fileURL) throws IOException {
try (BufferedReader in = new BufferedReader(new InputStreamReader(fileURL.openStream()))) {
String line;
while ((line = in.readLine()) != null) {
@@ -95,9 +116,67 @@ public class ClasspathClassLoader extends URLClassLoader {
}
}
private static URL createURLFromPath(String path) throws MalformedURLException {
File file = new File(path);
return file.getAbsoluteFile().toURI().normalize().toURL();
private URL createURLFromPath(String path) throws MalformedURLException {
Path filePath = Paths.get(path).toAbsolutePath();
if (filePath.endsWith(Paths.get("lib", "jrt-fs.jar"))) {
initializeJrtFilesystem(filePath);
// don't add jrt-fs.jar to the normal aux classpath
return null;
}
return filePath.toUri().normalize().toURL();
}
/**
* Initializes a Java Runtime Filesystem that will be used to load class files.
* This allows end users to provide in the aux classpath another Java Runtime version
* than the one used for executing PMD.
*
* @param filePath path to the file "lib/jrt-fs.jar" inside the java installation directory.
* @see <a href="https://openjdk.org/jeps/220">JEP 220: Modular Run-Time Images</a>
*/
private void initializeJrtFilesystem(Path filePath) {
try {
LOG.debug("Detect Java Runtime Filesystem Provider in {}", filePath);
if (fileSystem != null) {
throw new IllegalStateException("There is already a jrt filesystem. Do you have multiple jrt-fs.jar files on the classpath?");
}
if (filePath.getNameCount() < 2) {
throw new IllegalArgumentException("Can't determine java home from " + filePath + " - please provide a complete path.");
}
try (URLClassLoader loader = new URLClassLoader(new URL[] { filePath.toUri().toURL() })) {
Map<String, String> env = new HashMap<>();
// note: providing java.home here is crucial, so that the correct runtime image is loaded.
// the class loader is only used to provide an implementation of JrtFileSystemProvider, if the current
// Java runtime doesn't provide one (e.g. if running in Java 8).
this.javaHome = filePath.getParent().getParent().toString();
env.put("java.home", javaHome);
LOG.debug("Creating jrt-fs with env {}", env);
fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env, loader);
}
packagesDirsToModules = new HashMap<>();
Path packages = fileSystem.getPath("packages");
try (Stream<Path> packagesStream = Files.list(packages)) {
packagesStream.forEach(p -> {
String packageName = p.getFileName().toString().replace('.', '/');
try (Stream<Path> modulesStream = Files.list(p)) {
Set<String> modules = modulesStream
.map(Path::getFileName)
.map(Path::toString)
.collect(Collectors.toSet());
packagesDirsToModules.put(packageName, modules);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
@@ -105,7 +184,36 @@ public class ClasspathClassLoader extends URLClassLoader {
return getClass().getSimpleName()
+ "[["
+ StringUtils.join(getURLs(), ":")
+ "] parent: " + getParent() + ']';
+ "] jrt-fs: " + javaHome + " parent: " + getParent() + ']';
}
@Override
public InputStream getResourceAsStream(String name) {
// always first search in jrt-fs, if available
if (fileSystem != null) {
String packageName = name.substring(0, name.lastIndexOf('/'));
Set<String> moduleNames = packagesDirsToModules.get(packageName);
if (moduleNames != null) {
LOG.trace("Trying to find {} in jrt-fs with packageName={} and modules={}",
name, packageName, moduleNames);
for (String moduleCandidate : moduleNames) {
Path candidate = fileSystem.getPath("modules", moduleCandidate, name);
if (Files.exists(candidate)) {
LOG.trace("Found {}", candidate);
try {
return Files.newInputStream(candidate);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}
}
// search in the other jars of the aux classpath.
// this will call this.getResource, which will do a child-first search, see below.
return super.getResourceAsStream(name);
}
@Override
@@ -126,24 +234,14 @@ public class ClasspathClassLoader extends URLClassLoader {
@Override
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// checking local
c = findClass(name);
} catch (final ClassNotFoundException | SecurityException e) {
// checking parent
// This call to loadClass may eventually call findClass again, in case the parent doesn't find anything.
c = super.loadClass(name, resolve);
}
}
throw new IllegalStateException("This class loader shouldn't be used to load classes");
}
if (resolve) {
resolveClass(c);
}
return c;
@Override
public void close() throws IOException {
if (fileSystem != null) {
fileSystem.close();
}
super.close();
}
}
@@ -0,0 +1,113 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.internal.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class ClasspathClassLoaderTest {
@TempDir
private Path tempDir;
@Test
void loadEmptyClasspathWithParent() throws IOException {
try (ClasspathClassLoader loader = new ClasspathClassLoader("", ClasspathClassLoader.class.getClassLoader())) {
try (InputStream resource = loader.getResourceAsStream("java/lang/Object.class")) {
assertNotNull(resource);
try (DataInputStream data = new DataInputStream(resource)) {
assertClassFile(data, Integer.valueOf(System.getProperty("java.specification.version")));
}
}
}
}
/**
* This test case just documents the current behavior: Eventually we load
* the class files from the system class loader, even if the auxclasspath
* is essentially empty.
*/
@Test
void loadEmptyClasspathNoParent() throws IOException {
try (ClasspathClassLoader loader = new ClasspathClassLoader("", null)) {
try (InputStream resource = loader.getResourceAsStream("java/lang/Object.class")) {
assertNotNull(resource);
try (DataInputStream data = new DataInputStream(resource)) {
assertClassFile(data, Integer.valueOf(System.getProperty("java.specification.version")));
}
}
}
}
@Test
void loadFromJar() throws IOException {
final String RESOURCE_NAME = "net/sourceforge/pmd/Sample.txt";
final String TEST_CONTENT = "Test\n";
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));
}
String classpath = jarPath.toString();
try (ClasspathClassLoader loader = new ClasspathClassLoader(classpath, null)) {
try (InputStream in = loader.getResourceAsStream(RESOURCE_NAME)) {
assertNotNull(in);
String s = IOUtil.readToString(in, StandardCharsets.UTF_8);
assertEquals(TEST_CONTENT, s);
}
}
}
/**
* Verifies, that we load the class files from the runtime image of the correct java home.
*
* <p>
* This test only runs, if you have a folder ${HOME}/openjdk17.
* </p>
*/
@Test
void loadFromJava17() throws IOException {
Path java17Home = Paths.get(System.getProperty("user.home"), "openjdk17");
assumeTrue(Files.isDirectory(java17Home), "Couldn't find java17 installation at " + java17Home);
Path jrtfsPath = java17Home.resolve("lib/jrt-fs.jar");
assertTrue(Files.isRegularFile(jrtfsPath), "java17 installation is incomplete. " + jrtfsPath + " not found!");
String classPath = jrtfsPath.toString();
try (ClasspathClassLoader loader = new ClasspathClassLoader(classPath, null)) {
assertEquals(java17Home.toString(), loader.javaHome);
try (InputStream stream = loader.getResourceAsStream("java/lang/Object.class")) {
assertNotNull(stream);
try (DataInputStream data = new DataInputStream(stream)) {
assertClassFile(data, 17);
}
}
}
}
private void assertClassFile(DataInputStream data, int javaVersion) throws IOException {
int magicNumber = data.readInt();
assertEquals(0xcafebabe, magicNumber);
data.readUnsignedShort(); // minorVersion
int majorVersion = data.readUnsignedShort();
assertEquals(44 + javaVersion, majorVersion);
}
}
@@ -5,7 +5,7 @@
package net.sourceforge.pmd.lang.java.symbols.internal.asm;
import java.net.URL;
import java.io.InputStream;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -16,7 +16,7 @@ import org.objectweb.asm.Opcodes;
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
import net.sourceforge.pmd.lang.java.symbols.SymbolResolver;
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Loader.FailedLoader;
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Loader.UrlLoader;
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Loader.StreamLoader;
import net.sourceforge.pmd.lang.java.types.TypeSystem;
import net.sourceforge.pmd.util.AssertionUtil;
@@ -53,12 +53,12 @@ public class AsmSymbolResolver implements SymbolResolver {
String internalName = getInternalName(binaryName);
ClassStub found = knownStubs.computeIfAbsent(internalName, iname -> {
@Nullable URL url = getUrlOfInternalName(iname);
if (url == null) {
@Nullable InputStream inputStream = getStreamOfInternalName(iname);
if (inputStream == null) {
return failed;
}
return new ClassStub(this, iname, new UrlLoader(url), ClassStub.UNKNOWN_ARITY);
return new ClassStub(this, iname, new StreamLoader(binaryName, inputStream), ClassStub.UNKNOWN_ARITY);
});
if (!found.hasCanonicalName()) {
@@ -84,7 +84,7 @@ public class AsmSymbolResolver implements SymbolResolver {
}
@Nullable
URL getUrlOfInternalName(String internalName) {
InputStream getStreamOfInternalName(String internalName) {
return classLoader.findResource(internalName + ".class");
}
@@ -105,8 +105,8 @@ public class AsmSymbolResolver implements SymbolResolver {
if (prev != failed && prev != null) {
return prev;
}
@Nullable URL url = getUrlOfInternalName(iname);
Loader loader = url == null ? FailedLoader.INSTANCE : new UrlLoader(url);
@Nullable InputStream inputStream = getStreamOfInternalName(iname);
Loader loader = inputStream == null ? FailedLoader.INSTANCE : new StreamLoader(internalName, inputStream);
return new ClassStub(this, iname, loader, observedArity);
});
}
@@ -4,7 +4,7 @@
package net.sourceforge.pmd.lang.java.symbols.internal.asm;
import java.net.URL;
import java.io.InputStream;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -23,9 +23,9 @@ public interface Classpath {
*
* @param resourcePath Resource path, as described in {@link ClassLoader#getResource(String)}
*
* @return A URL if the resource exists, otherwise null
* @return A InputStream if the resource exists, otherwise null
*/
@Nullable URL findResource(String resourcePath);
@Nullable InputStream findResource(String resourcePath);
// <editor-fold defaultstate="collapsed" desc="Transformation methods (defaults)">
@@ -42,7 +42,7 @@ public interface Classpath {
default Classpath delegateTo(Classpath c) {
return path -> {
URL p = findResource(path);
InputStream p = findResource(path);
if (p != null) {
return p;
}
@@ -56,11 +56,11 @@ public interface Classpath {
/**
* Returns a classpath instance that uses {@link ClassLoader#getResource(String)}
* Returns a classpath instance that uses {@link ClassLoader#getResourceAsStream(String)}
* to find resources.
*/
static Classpath forClassLoader(ClassLoader classLoader) {
return classLoader::getResource;
return classLoader::getResourceAsStream;
}
static Classpath contextClasspath() {
@@ -7,7 +7,6 @@ package net.sourceforge.pmd.lang.java.symbols.internal.asm;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -35,27 +34,23 @@ abstract class Loader {
}
static class UrlLoader extends Loader {
static class StreamLoader extends Loader {
private final @NonNull String name;
private final @NonNull InputStream stream;
private final @NonNull URL url;
UrlLoader(@NonNull URL url) {
assert url != null : "Null url";
this.url = url;
StreamLoader(@NonNull String name, @NonNull InputStream stream) {
this.name = name;
this.stream = stream;
}
@Override
@Nullable
InputStream getInputStream() throws IOException {
return url.openStream();
@NonNull InputStream getInputStream() {
return stream;
}
@Override
public String toString() {
return "(URL loader)";
return "(StreamLoader for " + name + ")";
}
}
}
@@ -38,7 +38,7 @@ class AsmLoaderTest : IntelliMarker, FunSpec({
// access flags
// method reference with static ctdecl & zero formal parameters (asInstanceMethod)
val contextClasspath = Classpath { Thread.currentThread().contextClassLoader.getResource(it) }
val contextClasspath = Classpath { Thread.currentThread().contextClassLoader.getResourceAsStream(it) }
test("First ever ASM test") {