4

From Java 11, how can I read the content of another runtime image?

In order to list the content of a Java runtime image, JEP 220 suggests the following solution:

A built-in NIO FileSystem provider for the jrt URL scheme ensures that development tools can enumerate and read the class and resource files in a run-time image by loading the FileSystem named by the URL jrt:/, as follows:

FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
byte[] jlo = Files.readAllBytes(fs.getPath("modules", "java.base",
                                           "java/lang/Object.class"));

This snippet works and will allow me to read the content of java/lang/Object.class in the runtime image of the Java installation that is executing the code.

How can I get it to read the content of java/lang/Object.class in another Java installation, given its java home?

I have read this SO answer which explains how to read a Java runtime image's content from a Java 8 runtime. Unfortunately, this won't work with newer Java runtimes, since, I believe, the filesystem for jrt:/ will always point to the current runtime image.

Martin
  • 1,868
  • 1
  • 15
  • 20

2 Answers2

7

You may still use jrt:/ scheme as described in this answer, you just need to provide an alternative java.home path in the environment argument when creating a FileSystem object:

public static void listModules(String javaHome) throws IOException {
    FileSystem fs = FileSystems.newFileSystem(
            URI.create("jrt:/"),
            Collections.singletonMap("java.home", javaHome));
    try (Stream<Path> stream = Files.list(fs.getPath("/modules"))) {
        stream.forEach(System.out::println);
    }
}

Or, to read a single resource:

public static byte[] readResource(String javaHome, String module, String path) throws IOException {
    FileSystem fs = FileSystems.newFileSystem(
            URI.create("jrt:/"),
            Collections.singletonMap("java.home", javaHome));
    return Files.readAllBytes(fs.getPath("modules", module, path));
}
apangin
  • 92,924
  • 10
  • 193
  • 247
  • This is working great, thank you. With hindsight, this makes a lot of sense. Do you know where this is documented by any chance? I see this environment is used in OpenJDK: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/19fb8f93c59dfd791f62d41f332db9e306bc1422/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystemProvider.java#L105-L107, but couldn't find it mentioned in documentation anywhere. – Martin Jun 18 '21 at 07:58
  • 1
    When you only want to read a single resource, you likely want to use `try(…)` to close the filesystem afterwards. Also, in Java 9 you can use `Map.of("java.home", javaHome)`. – Holger Jun 18 '21 at 12:43
  • I believe this is causing some memory to be leaked: https://stackoverflow.com/questions/68083239/how-to-free-all-resources-after-reading-a-jrt – Martin Jun 22 '21 at 12:06
1

I think what you want is impossible. To wit:

  • Up to JDK8, you can rely on e.g. Paths.get(pathToJdk8Home, "jre", "lib", "rt.jar") to exist, which you can then turn into a URL (you're looking for jar:file:/that/path), and you can then toss that URL at FileSystems.newFileSystem), see this documentation for more.
  • But from JDK9 and up, the core java API is loaded in jmod files, and jmod files have an unspecified format by design - right now jmods are just zips, but unlike jars you explicitly get no guarantees that they will remain zip formatted, and there is no jmod URL scheme and no JmodFileSystemProvider. It is, in effect, impossible to read a jmod file in a way that is future compatible. Unfortunately the OpenJDK project has been on a murderous spree turning a ton of useful things, such as 'read a jmod', into implementation details. Bit user-hostile - just be aware of that, and I'm trying to do some expectation management: Stuff like this is way, way harder, and a huge maintenance burden (as you're forced to dip into workarounds, hacks, and going beyond spec thus needing to check it still works for every point release). See also this SO answer.

The jrt scheme can only load data from jmods that are actually 'loaded' into the VM's mod base, which I gather is explicitly not what you want (in fact, I'm pretty sure you cannot load e.g. the JDK11 core jmods into a JDK14, as it already loaded its jmods, and you'd get a split package violation). The jrt:// URL scheme, per its spec, isn't base file system related. You specify a module name (or nothing, and you get all loaded modules as one file system). There is no place for you to list a JDK installation path or jmod file, so that can't help you either.

Thus, you have only two options:

  • Accept that what you want cannot be done.
  • Accept that you're going to have to write hackery (as in, go beyond things that specifications guarantee you), and you accept the large maintenance burden that comes with the territory.

The hackery would involve:

  • Detect targeted JDK version or go on a hunting spree within the provided JDK installation directory (using e.g. Files.walk) to find a file named rt.jar. If it's there, load it up as ZipFileSystem and carry on. Modules 'do not exist', just turn any desired class into a path by replacing dots with slashes and appending .class (note that you'll need the binary name; e.g. package com.foo; class Outer { class Inner {}} means you want the name of Inner to be com.foo.Outer$Inner, so that you turn that into /com/foo/Outer$Inner.class).
  • For JDK9 and up, hunt for a file at JDK_HOME/jmods/java.base.jmod, and throw that at ZipFileSystem. A given class is in subdir classes. So, you're looking for e.g. the entry classes/java/lang/Object.class within the zip (that jmod is the zip). However, festoon this code with comments stating that this is a total hack and there is zero guarantee that this will work in the future. I can tell you, however, that JDK16, at least, still has zip-based jmod files.
  • Alternatively, given that you have a JDK installation path, you can use ProcessBuilder to exec List.of("JDK_HOME/bin/jmod" /* or jmod.exe, you'll have to check which one to call! */, "extract", "JDK_HOME/jmods/java.base.jmod"), but note that this will extract all of those files into the current working directory (you can set the cwd for the invoked process to be some dir you just created for the purpose of being filled with the files inside). Quite a big bazooka if all you wanted was the one file. (You can also use the --dir option instead). The advantage is that this will still work even if hypothetically JDK17 is using some different format; presumably JDK17 will still have both bin/jmod as well as jmods/java.base.jmod, and the bin/jmod of JDK17 should be able to unpack the jmod files in your JDK17 installation. Even if you are running all this from e.g. JDK16 which wouldn't be able to read them.
rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • `jmods` directory is optional; there are JDK distributions with no `jmods` at all. JVM does not look into `jmods` when loading system classes, it reads `lib/modules` instead. – apangin Jun 17 '21 at 21:14
  • And yes, `jrt:/` paths can be used to read class files and resources from a different `java.home`. – apangin Jun 17 '21 at 21:51