Reputation: 1678
So here is my issue. I am creating a piece of software that is similar to a DOS command line. I would like people to be able to add other commands by simply dropping a jar file containing classes extending the base Command
class into a folder. Nothing I have tried yet works.
Here is what I have tried:
SomeClass$1.class
stuff that goes on.ZipEntry
into anything that could be added to the classpath, e.g URLs.I would love some help here. Any suggestions on how I can make my program more extensible would be very much welcome. +1 for those including code ;)
If you need any further information just let me know.
EDIT:
New code:
URLClassLoader ucl = (URLClassLoader) ClassLoader.getSystemClassLoader();
for(File file : FileHelper.getProtectedFile("/packages/").listFiles()) {
if(file.getName().endsWith(".jar")) {
Set<Class<?>> set = new HashSet<Class<?>>();
JarFile jar = null;
try {
jar = new JarFile(file);
Enumeration<JarEntry> entries = jar.entries();
while(entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if(!entry.getName().endsWith(".class"))
continue;
Class<?> clazz;
try {
clazz = ucl.loadClass(entry.getName().replace("/", ".").replace(".class", "")); // THIS IS LINE 71
} catch(ClassNotFoundException e) {
e.printStackTrace();
continue;
}
if(entry.getName().contains("$1"))
continue;
if(clazz.getName().startsWith("CMD_"))
set.add(clazz);
jar.close();
}
} catch(Exception e) {
//Ignore file
try {
jar.close();
} catch (IOException e1) {/* Don't worry about it too much... */}
OutputHelper.log("An error occurred whilst adding package " + OutputStyle.ITALIC + file.getName() + OutputStyle.DEFAULT + ". " + e.getMessage() + ". Deleting package...", error);
e.printStackTrace();
file.delete();
}
Error thrown:
java.lang.ClassNotFoundException: com.example.MyCommands.CMD_eggtimer
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at com.MyWebsite.MyApplication.Commands.CommandList.update(CommandList.java:71)
Line 71 has been marked in the above code.
Upvotes: 4
Views: 4136
Reputation: 1678
Thanks for the help everyone but I figured it out on my own.
Firstly you need the location of the file you want to manipulate. Use this to create a File and a JarFile of the jar.
File file = new File("/location-to/file.jar");
JarFile jar = new JarFile(file);
You can do this by iterating through stuff in a folder if you want.
Then you need to create a URLClassLoader
for that file:
URLClassLoader ucl = new URLClassLoader(new URL[] {file.toURI().toURL()});
Then iterate through all the classes in the Jar file.
Enumeration<JarEntry> entries = jar.entries();
while(entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if(!entry.getName().endsWith(".class"))
continue;
Class<?> clazz;
try {
clazz = ucl.loadClass(entry.getName().replace("/", ".").replace(".class", ""));
} catch(ClassNotFoundException e) {
e.printStackTrace();
continue;
}
if(entry.getName().contains("$1"))
continue;
if(clazz.getSimpleName().startsWith("CMD_"))
set.add(clazz); //Sort the classes how you like. Here I added ones beginning with 'CMD_' to a HashSet for later manipulation
Don't forget to close stuff:
jar.close();
ucl.close();
So, let's recap over what's happened:
URLClassLoader
URLClassLoader
and the JarFile
to prevent memory leaksHuzzah!
Upvotes: 1
Reputation: 279970
If you start your program with the jar in the classpath, then you can get that jar path by getting the system property java.class.path
and creating a JarFile
object. You can then iterate over that object's entries. Each entry has a path (relative to the jar). You can parse this path to get the package and class name as com.package.Class
. You then use a ClassLoader
to load the class and do whatever you want with it. (You might want to use another ClassLoader, but it works with the one below)
public class Main {
static ClassLoader clazzLoader = Main.class.getClassLoader();
public static void main(String [] args) {
try {
List<Class<?>> classes = new LinkedList<>();
// do whatever transformation you need to get the jar file
String classpath = System.getProperty("java.class.path").split(";")[1];
// I'm just checking if it's the right jar file
System.out.println(classpath);
// get the file and make a JarFile out of it
File bin = new File(classpath);
JarFile jar = new JarFile(bin);
// get all entries in the jar file
Enumeration<JarEntry> entries = jar.entries();
// iterate over jarfile entries
while(entries.hasMoreElements()) {
try {
JarEntry entry = entries.nextElement();
// skip other files in the jar
if (!entry.getName().endsWith(".class"))
continue;
// extract the class name from its URL style name
String className = entry.getName().replace("/", ".").replace(".class", "");
// load the class
Class<?> clazz = clazzLoader.loadClass(className);
// use your Command class or any other class you want to match
if (Comparable.class.isAssignableFrom(clazz))
classes.add(clazz);
} catch (ClassNotFoundException e) {
//ignore
}
}
for (Class<?> clazz : classes) {
System.out.println(clazz);
clazz.newInstance(); // or whatever
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
I've tried this with Inner classes and it works as well, I don't know what kind of parsing the Reflections
library does.
Also, you don't need a full Jar for something like this to work. If you have a bin folder with .class
files in it, you can recursively go through its files/folders, parse class file to the proper format, and load them like above.
EDIT With your new code, a few things
jar.close()
within your loop, while you're iterating.if(clazz.getName().startsWith("CMD_"))
isn't doing what you want. The getName()
method returns the full pathname of your class, including packages. Use getSimpleName()
If you are getting a ClassNotFoundException
, it's because that class is not on the classpath. This is telling me that you are not running the program with your jar files on the classpath. When loading classes, it's not enough to open a zip/jar file and "load" the class files. You have to run your program like
java -classpath <your_jars> com.your.class.Main
Or with Eclipse (or other IDE), add the jars to your build path.
Take a look at this for more explanations on running java with classpath entries.
Upvotes: 0
Reputation: 15199
When I've needed to do this in the past, I used a combination of your points 2 and 3, i.e. I add the jar file to the classpath, and then iterate through it listing the files it contains to work out the names of classes, and use Class.forName(...) to load them so I can check if they are classes I need to perform processing on.
Another option is to require the classes to be identified in the jar file's manifest, or via some other metadata mechanism.
Upvotes: 1
Reputation: 1638
Scanning the classpath to find all classes implementing a given interface seems a rather messy affair but there seem to be some way to do so - the answers there suggest the use of the Reflections library (i premise i haven't actually ever used that)
A more sensible way, even though it implies some metadata editing, would be to use the ServiceLoader mechanism.
Edit: i missed the Reflections reference in your post, sorry... still, the suggestion about ServiceLoader still applies.
Upvotes: 0