Scanning Classpath/Modulepath in Runtime in Java 9

Scanning classpath/modulepath in runtime in Java 9

The following code achieves module path scanning in Java 9+ (Jigsaw / JPMS). It finds all classes on the callstack, then for each class reference, calls classRef.getModule().getLayer().getConfiguration().modules(), which returns a a List<ResolvedModule>, rather than just a List<Module>. (ResolvedModule gives you access to the module resources, whereas Module does not.) Given a ResolvedModule reference for each module, you can call the .reference() method to get the ModuleReference for a module. ModuleReference#open() gives you a ModuleReader, which allows you to list the resources in a module, using ModuleReader#list(), or to open a resource using Optional<InputStream> ModuleReader#open(resourcePath) or Optional<ByteBuffer> ModuleReader#read(resourcePath). You then close the ModuleReader when you're done with the module. This is not documented anywhere that I have seen. It was very difficult to figure all this out. But here is the code, in the hope that someone else will benefit from this.

Note that even in JDK9+, you can still utilize traditional classpath elements along with module path elements, so for a complete module path + classpath scan, you should probably use a proper classpath scanning solution, such as ClassGraph, which supports module scanning using the below mechanism (disclaimer, I am the author). You can find a reflection-based version of the following code here.

Also note that there was a bug in StackWalker in several JDK releases after JDK 9 that has to be worked around, see the above reflection-based code for details.

package main;

import java.lang.StackWalker;
import java.lang.StackWalker.Option;
import java.lang.StackWalker.StackFrame;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

public class Java9Scanner {

/** Recursively find the topological sort order of ancestral layers. */
private static void findLayerOrder(ModuleLayer layer,
Set<ModuleLayer> visited, Deque<ModuleLayer> layersOut) {
if (visited.add(layer)) {
List<ModuleLayer> parents = layer.parents();
for (int i = 0; i < parents.size(); i++) {
findLayerOrder(parents.get(i), visited, layersOut);
}
layersOut.push(layer);
}
}

/** Get ModuleReferences from a Class reference. */
private static List<Entry<ModuleReference, ModuleLayer>> findModuleRefs(
Class<?>[] callStack) {
Deque<ModuleLayer> layerOrder = new ArrayDeque<>();
Set<ModuleLayer> visited = new HashSet<>();
for (int i = 0; i < callStack.length; i++) {
ModuleLayer layer = callStack[i].getModule().getLayer();
findLayerOrder(layer, visited, layerOrder);
}
Set<ModuleReference> addedModules = new HashSet<>();
List<Entry<ModuleReference, ModuleLayer>> moduleRefs = new ArrayList<>();
for (ModuleLayer layer : layerOrder) {
Set<ResolvedModule> modulesInLayerSet = layer.configuration()
.modules();
final List<Entry<ModuleReference, ModuleLayer>> modulesInLayer =
new ArrayList<>();
for (ResolvedModule module : modulesInLayerSet) {
modulesInLayer
.add(new SimpleEntry<>(module.reference(), layer));
}
// Sort modules in layer by name for consistency
Collections.sort(modulesInLayer,
(e1, e2) -> e1.getKey().descriptor().name()
.compareTo(e2.getKey().descriptor().name()));
// To be safe, dedup ModuleReferences, in case a module occurs in multiple
// layers and reuses its ModuleReference (no idea if this can happen)
for (Entry<ModuleReference, ModuleLayer> m : modulesInLayer) {
if (addedModules.add(m.getKey())) {
moduleRefs.add(m);
}
}
}
return moduleRefs;
}

/** Get the classes in the call stack. */
private static Class<?>[] getCallStack() {
// Try StackWalker (JDK 9+)
PrivilegedAction<Class<?>[]> stackWalkerAction =
(PrivilegedAction<Class<?>[]>) () ->
StackWalker.getInstance(
Option.RETAIN_CLASS_REFERENCE)
.walk(s -> s.map(
StackFrame::getDeclaringClass)
.toArray(Class[]::new));
try {
// Try with doPrivileged()
return AccessController
.doPrivileged(stackWalkerAction);
} catch (Exception e) {
}
try {
// Try without doPrivileged()
return stackWalkerAction.run();
} catch (Exception e) {
}

// Try SecurityManager
PrivilegedAction<Class<?>[]> callerResolverAction =
(PrivilegedAction<Class<?>[]>) () ->
new SecurityManager() {
@Override
public Class<?>[] getClassContext() {
return super.getClassContext();
}
}.getClassContext();
try {
// Try with doPrivileged()
return AccessController
.doPrivileged(callerResolverAction);
} catch (Exception e) {
}
try {
// Try without doPrivileged()
return callerResolverAction.run();
} catch (Exception e) {
}

// As a fallback, use getStackTrace() to try to get the call stack
try {
throw new Exception();
} catch (final Exception e) {
final List<Class<?>> classes = new ArrayList<>();
for (final StackTraceElement elt : e.getStackTrace()) {
try {
classes.add(Class.forName(elt.getClassName()));
} catch (final Throwable e2) {
// Ignore
}
}
if (classes.size() > 0) {
return classes.toArray(new Class<?>[0]);
} else {
// Last-ditch effort -- include just this class
return new Class<?>[] { Java9Scanner.class };
}
}
}

/**
* Return true if the given module name is a system module.
* There can be system modules in layers above the boot layer.
*/
private static boolean isSystemModule(
final ModuleReference moduleReference) {
String name = moduleReference.descriptor().name();
if (name == null) {
return false;
}
return name.startsWith("java.") || name.startsWith("jdk.")
|| name.startsWith("javafx.") || name.startsWith("oracle.");
}

public static void main(String[] args) throws Exception {
// Get ModuleReferences for modules of all classes in call stack,
List<Entry<ModuleReference, ModuleLayer>> systemModuleRefs = new ArrayList<>();
List<Entry<ModuleReference, ModuleLayer>> nonSystemModuleRefs = new ArrayList<>();

Class<?>[] callStack = getCallStack();
List<Entry<ModuleReference, ModuleLayer>> moduleRefs = findModuleRefs(
callStack);
// Split module refs into system and non-system modules based on module name
for (Entry<ModuleReference, ModuleLayer> m : moduleRefs) {
(isSystemModule(m.getKey()) ? systemModuleRefs
: nonSystemModuleRefs).add(m);
}

// List system modules
System.out.println("\nSYSTEM MODULES:\n");
for (Entry<ModuleReference, ModuleLayer> e : systemModuleRefs) {
ModuleReference ref = e.getKey();
System.out.println(" " + ref.descriptor().name());
}

// Show info for non-system modules
System.out.println("\nNON-SYSTEM MODULES:");
for (Entry<ModuleReference, ModuleLayer> e : nonSystemModuleRefs) {
ModuleReference ref = e.getKey();
ModuleLayer layer = e.getValue();
System.out.println("\n " + ref.descriptor().name());
System.out.println(
" Version: " + ref.descriptor().toNameAndVersion());
System.out.println(
" Packages: " + ref.descriptor().packages());
System.out.println(" ClassLoader: "
+ layer.findLoader(ref.descriptor().name()));
Optional<URI> location = ref.location();
if (location.isPresent()) {
System.out.println(" Location: " + location.get());
}
try (ModuleReader moduleReader = ref.open()) {
Stream<String> stream = moduleReader.list();
stream.forEach(s -> System.out.println(" File: " + s));
}
}
}
}

Loading classes and resources in Java 9

First, to set the record straight, I neither said nor wrote the text
quoted above. I’d never have put it that way. That’s just sloppy
reporting on the part of the publications involved.

The most important thing to understand about class loading and resource
lookup in Java 9 is that, at a fundamental level, they have not changed.
You can search for classes and resources in the same way that you always
have, by invoking Class::forName and the various getResource* methods
in the Class and ClassLoader classes, regardless of whether your code
is loaded from the class path or the module path. There are still three
built-in class loaders, just as there were in JDK 1.2, and they have the
same delegation relationships. Lots of existing code therefore just
works, out-of-the-box.

There are some nuances, as noted in JEP
261: The concrete type
of the built-in class loaders has changed, and some classes formerly
loaded by the bootstrap class loader are now loaded by the platform class
loader in order to improve security. Existing code which assumes that a
built-in class loader is a URLClassLoader, or that a class is loaded by
the bootstrap class loader, may therefore require minor adjustments.

A final important difference is that non-class-file resources in a module
are encapsulated by default, and hence cannot be located from outside
the module unless their effective package is
open.
To load resources from your own module it’s best to use the
resource-lookup methods in Class or Module, which can locate any
resource in your module, rather than those in ClassLoader, which can
only locate non-class-file resources in the open packages of a module.

How to get classpath from classloader?

If the classloader uses URLs, it must be a URLClassloader. What you have access to is the URLs which defines the classpath for it along side with its parent ClassLoader.

To get the URLs, simply do the following:

((URLClassLoader) (Thread.currentThread().getContextClassLoader())).getURLs()

Patch Java 9 module with test-code to work with reflections

From Oracle's Java 13 java command these are the two options you tried to use:

--add-opens module/package=target-module(,target-module)*
--patch-module module=file(;file)*

but:

  • --add-opens doesn't help you because it opens up one module to other modules. You only have one module.
  • --patch-module must specify the directories (or jarfiles) you want to patch into the module. I noticed that there's a ; not a : like you used. It seems to me that you've been telling java to patch files from same the directory where your module is at with :ModuleInfoExample.

You only need to add the files from ModuleInfoTest/ into your module.
I created the structure from your Question and ran it:

Compiling:

javac -d target/ModuleInfoExample src/ModuleInfoExample/*.java src/ModuleInfoExample/com/stackoverflow/examplepackage/*.java
javac -cp target/ModuleInfoExample -d target/ModuleInfoTest src/ModuleInfoTest/com/stackoverflow/examplepackage/*.java

Running Main from module - no added classes:

java --module-path target/ModuleInfoExample --module com.stackoverflow.examplemodule/com.stackoverflow.examplepackage.Main

prints:
Hello world - I'm private

Running AnyClass from module - no added classes - exception expected

java --module-path target/ModuleInfoExample --module com.stackoverflow.examplemodule/com.stackoverflow.examplepackage.AnyClass

Error: Could not find or load main class com.stackoverflow.examplepackage.AnyClass in module com.stackoverflow.examplemodule

Running AnyClass from module - adding AnyClass into the package:

java --module-path target/ModuleInfoExample --patch-module com.stackoverflow.examplemodule=target/ModuleInfoTest --module com.stackoverflow.examplemodule/com.stackoverflow.examplepackage.AnyClass

prints:
Inside AnyClass - calling Main: Hello world - I'm private

field.get() = I'm private
field.get() = I'm not private anymore

Total structure:

>tree /f
..snip..
C:.
+---src
¦ +---ModuleInfoExample
¦ ¦ ¦ module-info.java
¦ ¦ ¦
¦ ¦ +---com
¦ ¦ +---stackoverflow
¦ ¦ +---examplepackage
¦ ¦ Main.java
¦ ¦
¦ +---ModuleInfoTest
¦ +---com
¦ +---stackoverflow
¦ +---examplepackage
¦ AnyClass.java
¦
+---target
+---ModuleInfoExample
¦ ¦ module-info.class
¦ ¦
¦ +---com
¦ +---stackoverflow
¦ +---examplepackage
¦ Main.class
¦
+---ModuleInfoTest
+---com
+---stackoverflow
+---examplepackage
AnyClass.class

src\ModuleInfoExample\module-info.java:

module com.stackoverflow.examplemodule {
// exports com.stackoverflow.examplepackage; // no need to export. Nothing is using this
}

src\ModuleInfoExample\com\stackoverflow\examplepackage\Main.java:

package com.stackoverflow.examplepackage;

public class Main {
private String privateString = "I'm private";

public static void main(String[] args) {
new Main().hello();
}
public void hello(){
System.out.println("Hello world - " + privateString);
}
}

src\ModuleInfoTest\com\stackoverflow\examplepackage\AnyClass.java:

package com.stackoverflow.examplepackage;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class AnyClass {
public static void main(String[] args) {
testhello();
System.out.println();
breakhello();
}

public static void testhello(){
System.out.print("Inside AnyClass - calling Main: ");
Main test = new Main();
test.hello();
}

public static void breakhello(){
try {
// Not necessary - same package, but..
Class<?> mainClass = Class.forName("com.stackoverflow.examplepackage.Main");
Constructor<?> constructor = mainClass.getConstructor();
Object main = constructor.newInstance();

// Getting, printing and changing the field..
Field field = mainClass.getDeclaredField("privateString");
field.setAccessible(true);
System.out.println(" field.get() = " + field.get(main));
field.set(main,"I'm not private anymore");
System.out.println(" field.get() = " + field.get(main));

} catch (Exception e) { // Sorry, all in one big bucket
System.out.println("Error: " + e);
}
}
}


Related Topics



Leave a reply



Submit