Load class from exploded module using custom classloader
Asked Answered
U

1

7

I have been playing around with simple custom classloaders in Java, and so far everything works as expected for non-module related classes. However, I can't seem to find any way to load a class from a module using my classloader, even though the module-related find*() methods have been overloaded. What I'm attempting to do is load a class from a module "HelloModularWorld" and run it's main. However, when I specify the directory where the package would be, it's loaded "normally" and reports as being in the unnamed module. What am I missing?

The classloader is just loading classes from elsewhere on the filesystem, nothing particularly special.

Classloader implementation:

package arch.classloaders;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;

public class ModifiableClassLoader extends ClassLoader {
    private File searchPath;
    //...other ctors...
    public ModifiableClassLoader(File path, String name, ClassLoader parent) {
        super(name, parent); //Delegate to parent classloader as required.
        if (!path.isDirectory()) throw new IllegalArgumentException("Path must be a directory");
        searchPath = path;
    }

    public void setPath(File newPath) {...}

    public File getPath() {...}

    //Responsible for actual loading
    public Class<?> findClass(String binName) throws ClassNotFoundException {
        File classfile = new File(searchPath.getPath() + File.separator 
                + binName.replace('.', File.separatorChar) + ".class");
        byte[] buf;
        FileInputStream fis;
        try {
            fis = new FileInputStream(classfile);
            buf = fis.readAllBytes();
            fis.close();
        } catch (IOException e) {
            throw new ClassNotFoundException("Error in defining " + binName + " in " + searchPath.getPath(),e);
        }
        return defineClass(binName, buf, 0, buf.length);
    }

    //Modules are expected to be in a folder with the same name as the module.
    //e.g. module hellomodularworld is expected to be in a folder
    //<SEARCHPATH>/<MODNAME>/
    //NOTE: Module-overloaded methods return null rather than throw when class isn't found.
    public Class<?> findClass(String modName, String binName) {
        if (null == modName) {
            try {
                return findClass(binName);
            } catch (ClassNotFoundException e) {
                return null;
            }
        }
        File classfile = new File(searchPath.getPath() + File.separator 
                + modName + File.separator
                + binName.replace('.', File.separatorChar) + ".class");
        byte[] buf;
        FileInputStream fis;
        try {
            fis = new FileInputStream(classfile);
            buf = fis.readAllBytes();
            fis.close();
        } catch (IOException e) {
            return null;
        }
        return defineClass(binName, buf, 0, buf.length);
    }

    //Non-module
    public URL findResource(String resPath) {...}

    //Module version
    public URL findResource(String modName, String resPath) throws IOException {...}

    //Enumeration version; does nothing.
    public java.util.Enumeration<URL> findResources(String resPath) {...}
}

Test code:

public class Test {
    public static void main(String[] args) {
        ModifiableClassLoader mcl = new ModifiableClassLoader(
                new File("C:\\Users\\archd\\Desktop\\"));
        try {
            Class<?> clazz = mcl.loadClass(
                    "hellomodularworld/com.archdukeliamus.hellomodularworld.HelloWorld"
            );
            java.lang.reflect.Method mth = clazz.getMethod("main", String[].class);
            mth.invoke(null,new Object[] {null});
            System.out.println(clazz.getModule().getName());
        } catch (...) {
                //omitted
        }
}

On line Class<?> clazz = mcl.loadClass("hellomodularworld/com.archdukeliamus.hellomodularworld.HelloWorld"); I have tried:

  • "hellomodularworld/com.archdukeliamus.hellomodularworld.HelloWorld" - Does not work. ClassNotFoundException.
  • "hellomodularworld.com.archdukeliamus.hellomodularworld.HelloWorld" - NoClassDefFoundException.
    com/archdukeliamus/hellomodularworld/HelloWorld (wrong name: hellomodularworld/com/archdukeliamus/hellomodularworld/HelloWorld)
  • "com.archdukeliamus.hellomodularworld.HelloWorld" - Works as expected (with classloading folder changed appropriately) but uses the unnamed module.

EDIT: module-info.java

module hellomodularworld {

}

The test class is not in any modules. (I'm not entirely sure why this would matter, I should get an exception to the effect of "this package isn't exported" which I am not getting.)

EDIT 2: Modified module to include exports com.archdukeliamus.hellomodularworld;. No changes in results.

Unworthy answered 6/12, 2017 at 23:48 Comment(8)
I haven't really used Java 9 modules yet, but I'm pretty sure you can't pass that kind of string to loadClass. See here.Timpani
While that does clarify that a module doesn't seem to be allowed in the binary name, it still begs the question of how the classloader gets the module to begin with. Nothing in the ClassLoader page seems to specify this.Unworthy
Could you update the question with the module declaration of the hellomodularworld and of the module from which the code above is shared?Merozoite
@nullpointer Updated question appropriately.Unworthy
The reason I was asking for that is If a request is made to load a type whose package is not defined in any known module then the module system will attempt to load it from the class path. If this succeeds then the type is considered to be a member of a special module known as the unnamed module, so as to ensure that every type is associated with some moduleMerozoite
The code fragment is just a class loader that loads classes that are deployed as modules. Where is your code to create the configuration and map the module to the class loader, you must be calling ModuleLayer's defineModule somewhere, right?Hola
As a side note, why are you going that FileInputStream detour? You can simply use byte[] buf = Files.readAllBytes(searchPath.toPath().resolve(binName.replace('.', File.separatorChar) + ".class"));. Since Java 7.Mercado
I didn't yet see about ModuleLayer but it seems where I need to be going. Also I did the "detour" since I haven't yet made much use of NIO.Unworthy
U
4

Module loading is a separate process from class loading. To load a module at runtime, you will need to create a new module layer using the ModuleFinder class, provided with a Path from a FileSystem (in this case the disk). You will then need to create a Configuration that you can use to resolve your modules. To ensure the sanity of the module loading process, you will need to derive the boot configuration. Then you will need to create your configuration, including a ModuleFinder to tell where modules are to be found, and a set of modules to be resolved. Then you instantiate the classloader you wish to use to load those modules' classes, and pass this to the define modules method. Finally you instantiate your class.

New test:

package arch.classloaders;

import java.lang.module.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.util.*;

public class Test2 {
    public static void main(String[] args) {
        //Get paths to module, and instantiate a ModuleFinder.
        Path pth = FileSystems.getDefault().getPath("C:\\Users\\archd\\Desktop");
        ModuleFinder mf = ModuleFinder.of(pth);
        //Create a new Configuration for a new module layer deriving from the boot configuration, and resolving
        //the "hellomodularworld" module.
        Configuration cfg = ModuleLayer.boot().configuration().resolve(mf,ModuleFinder.of(),Set.of("hellomodularworld"));
        //Create classloader
        ModifiableClassLoader mcl = new ModifiableClassLoader(
                new File("C:\\Users\\archd\\Desktop\\"));
        //make the module layer, using the configuration and classloader.
        ModuleLayer ml = ModuleLayer.boot().defineModulesWithOneLoader(cfg,mcl);
        //Show the configuration.
        System.out.println(ml.configuration()); //prints "hellomodularworld"
        try {
            //load and run class
            Class<?> clazz = ml.findLoader("hellomodularworld").loadClass(
                    "com.archdukeliamus.hellomodularworld.HelloWorld"
            );
            java.lang.reflect.Method mth = clazz.getMethod("main", String[].class);
            mth.invoke(null,new Object[] {null});
            //show the module this class is part of and list packages
            System.out.println(clazz.getModule()); //prints "module hellomodularworld"
            for (String pkgn : clazz.getModule().getPackages()) {
                System.out.println(pkgn); //prints "com.archdukeliamus.hellomodularworld"
            }
        } catch (ClassNotFoundException e) {
            ...omitted...
        }
    }
}

Interestingly this still doesn't call the overloaded module-based findClass() methods, although it seems to work.

Unworthy answered 7/12, 2017 at 2:43 Comment(4)
There is nothing in the API to restrict modules to the file system, they can of course be loaded from anywhere. In any case, the code fragment in this answer doesn't mention anything about dynamic configuration and module layers - these are the concepts that are needed before using a custom class loader to load classes from modules.Hola
I see. The JEPs weren't entirely enlightening on how custom classloaders interacted with the module system. Is there a link where the module layer is properly explained?Unworthy
In the javadoc search, look for "ModuleLayer". Note that this is a very advanced topic so take the time to get familiar with the concepts.Hola
In my experience from implementing similar things myself is you don't even need a custom class loader. I have a project that needs to load classes from custom archive format and the way I implement this is with a custom ModuleReader and ModuleFinder. The custom ModuleReader overrides open(String) and that gets called by Java's regular module classloader any time it wants to read a class or any other data from the module. No custom ClassLoader is ever used despite the fact that I'm loading from a custom file format.Gildus

© 2022 - 2024 — McMap. All rights reserved.