Kezz
Kezz

Reputation: 1678

Java: Adding classes from external source to classpath

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:

  1. Using the Reflections library to add search the jar file for classes extending the Command class
    This threw many an error and just didn't find the majority of the classes in my jar. I think this has something to do with all the SomeClass$1.class stuff that goes on.
  2. Iterating through every file in the jar and adding it to the classpath
    I couldn't find a way for this to work because I simply couldn't turn the iterations of ZipEntry into anything that could be added to the classpath, e.g URLs.
  3. Adding the whole jar to the classpath
    This didn't work either because the program won't know the names of these classes, therefore, it can't turn them into commands.

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

Answers (4)

Kezz
Kezz

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:

  1. We got the file
  2. We added it to the classpath using a new URLClassLoader
  3. We iterated through entries in the jar and found the classes we wanted
  4. We closed the URLClassLoader and the JarFile to prevent memory leaks

Huzzah!

Upvotes: 1

Sotirios Delimanolis
Sotirios Delimanolis

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

  1. Don't close the jar jar.close() within your loop, while you're iterating.
  2. This 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()
  3. 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

Jules
Jules

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

Grim
Grim

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

Related Questions