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
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.
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:
ThatClass.class.getResource(ThatClass.class.getName() + ".class");
. This gets you a URL object representing the location of that.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).Files.newInputStream
or Files.newDirectoryStream
or however you would list the contents of a any dir on the file system / open any jar file.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.