Search code examples
javareflectionminecraft

How could I run the constructor of all the classes in a package without knowing the names of all of the classes?


I want to make a class that would run the constructor of each class in the package, excluding itself of course. So I would be able to add another class to the package and the constructor of the class would be run without having to go and explicitly call it in the main class. Its for a minecraft plugin so its being compiled into a jar and run that way chat gpt said that made some kind of difference.

I've tried to get the package name and use that to get a path which would look for all of the files using a class loader. I'm able to get a list of the classes in a different project but not in the plugin.

public static List<Class<?>> getClassList() {
        List<Class<?>> classList = new ArrayList<>();
        String packageName=Loader.class.getPackage().getName();
        String packagePath = packageName.replace('.', '/');

        try {
            java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            if (classLoader == null) {
                throw new ClassNotFoundException("Unable to get class loader.");
            }

            // Get all resources (files and directories) in the package directory
            java.util.Enumeration<java.net.URL> resources = classLoader.getResources(packagePath);
            while (resources.hasMoreElements()) {
                java.net.URL resource = resources.nextElement();
                if (resource.getProtocol().equals("file")) {
                    // If the resource is a file, get class objects
                    getClassObjectsFromFile(packageName, resource.getPath(), classList);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return classList;
    }

    private static void getClassObjectsFromFile(String packageName, String filePath, List<Class<?>> classList)
            throws ClassNotFoundException {
        java.io.File directory = new java.io.File(filePath);
        if (directory.exists()) {
            java.io.File[] files = directory.listFiles();
            if (files != null) {
                for (java.io.File file : files) {
                    if (file.isFile() && file.getName().endsWith(".class")) {
                        String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
                        Class<?> clazz = Class.forName(className);
                        classList.add(clazz);
                    }
                }
            }
        }
    }

Thanks


Solution

  • What you want is in basis completely impossible.

    That's because of how the ClassLoader API works: A classloader must be capable of loading a resource given a resource name. However, there is no need for a classloader to be able to list the contents of a directory - it doesn't even have a method for this.

    And classloaders are a pluggable concept. You can't 'enumerate every particular kind of classloader' because it's infinitely extensible.

    It gets worse: packages are by default 'open' in java - any classloader can load things in it if they want. You're not just having to list all files in a 'dir' for a given loader, you have to do it for all loaders, now and forever. Completely impossible.

    You have two broad options.

    Sod it, I want to do it

    What you can do, is write detectors for various kinds of classloaders and, if the underlying abstraction they represent does support it, write explicit code for that kind of classloader to 'list the contents'.

    This is more practical than it sounds - while classloaders are extensible and any given classloader does not need to support a 'list' primitive, in practice just about every classloader reads from disk or from jar, and both of those concepts (folders in jars, dirs on disk) support a 'list' concept.

    It does mean you will have to accept that if your code runs on any classloader you haven't written a 'list' implementation for, it simply cannot work. It also means you can only load all types in a package given a context - 'whatever classloader loaded this class, find me all classes in this package that that loader can load and construct them all' - that is doable, if the classloader is a type you know.

    The easiest way to go about it, is something along these lines:

    • Get a well known resource. Something you know has to exist, in the jar you are interested in. Possibly there is nothing but the entrypoint (other code calls your '.loadAllClassesInPackage("package.name.here")` method) can pass along its own class as a base context - now there is something you know. This resource has to exist in the same place as the classes in the package you want to run.
    • call ThatClass.class.getResource(ThatClass.class.getName() + ".class");. This gets you a URL object representing the location of that.
    • Parse it out. It'll be file:///com/foo/bar/pkg/name/ThatClass.class or jar:///com/foo/bar/myjarfile.jar!/pkg/name/ThatClass.class. Figure out the 'root' (the dir /com/foo/bar for the first one, the jar file /com/foo/bar/myjarfile.jar for the second).
    • Now open that using Files.newInputStream or Files.newDirectoryStream or however you would list the contents of a any dir on the file system / open any jar file.
    • List the contents of the relevant location.
    • Now you know the class names. Ask that loader (ContextCLass.class.getClassLoader() will get you this loader) to load them all and construct them using reflection. Consider that there may be abstract classes and the like, that you cannot construct.

    Do it right

    The solution to the 'cannot list' dilemma also solves the problem that you might want to create a helper type or an abstract class - something you did not intend for this system to 'just construct': The SPI system (Service Provider Interface).

    The trick is this:

    Decree that a specific file path is to exist with a text file, each line of it containing the full name of a type that 'implements some service' - the types you want to construct.

    Because it's a specific location and all class loaders can be asked 'get me the bytes of this resource', you can load it. You ask any classloader for that specific file, read it as a text file, and now you have a list of class names. Ask the classloader to load each of those and use reflection to construct them.

    This is the right way - it's used by JDBC drivers, charset impls, security stuff, and more - this is what java has for the general notion of 'I have some abstract concept and I want to find all implementations for it - it needs to be pluggable'.

    java.util.ServiceLoader is a class that can be used to load them. By convention the 'text file' is in /META-INF/services/com.foo.Bar where com.foo.Bar is the name of an abstract class or interface that all the 'service implementations' have to extend/implement.

    Some third party annotation based processors exist where you slap an @Provider annotation or similar on any class and the annotation processor will make that file appear automatically as part of the build process, which is pretty optimal. The process for 'I want to make a new doohickey' is now: Make a class. extend 'Doohickey'. Slap @Provider on it. Done. The system will find it and load it.