How to load unknown class from downloaded jar file during runtime?

2.3k views Asked by At

I'm building a client server application. During runtime the client application loads a jar file from the server application and stores it. I run both the client and the server application as jar files. I now want to load the class contained in this downloaded jar file.

For example I have an interface A and a class B implementing A. The client application does not know about the class B, its name or even its existence. After the start of the client application the client applications downloads a jar file containing a jar file with content: server/package/B.class where server and package are folders.

Now the client Application should load this class B from the downloaded jar file with the following code:

URL downloadURL = downloadFolder.toURI().toURL();
URL[] downloadURLs = new URL[] { ruleSetFolderURL };
URLClassLoader loader =
    new URLClassLoader(downloadURLs);
Class tmp = loadClass(server.package.B);

But then I get a ClassNotFoundException in the last line. Do I first have to extract the jar file? the folder structure in the jar file is like the folder structure in the bin directory of the server application.

2

There are 2 answers

0
Roman Vottner On BEST ANSWER

To load a class dynamically from a jar file that implements a certain interface, but you do not know in advance which class that will be and the jar file itself does not specify any default "plugin" class, you can iterate through the downloaded jar and get a list of classes contained in the jar like this:

    /**
     * Scans a JAR file for .class-files and returns a {@link List} containing
     * the full name of found classes (in the following form:
     * packageName.className)
     *
     * @param file
     * JAR-file which should be searched for .class-files
     * @return Returns all found class-files with their full-name as a List of
     *         Strings
     * @throws IOException If during processing of the Jar-file an error occurred
     * @throws IllegalArgumentException If either the provided file is null, does 
     *                                  not exist or is no Jar file 
     */
    public List<String> scanJarFileForClasses(File file) throws IOException, IllegalArgumentException
    {
            if (file == null || !file.exists())
                    throw new IllegalArgumentException("Invalid jar-file to scan provided");
            if (file.getName().endsWith(".jar"))
            {
                    List<String> foundClasses = new ArrayList<String>();
                    try (JarFile jarFile = new JarFile(file))
                    {
                            Enumeration<JarEntry> entries = jarFile.entries();
                            while (entries.hasMoreElements())
                            {
                                    JarEntry entry = entries.nextElement();
                                    if (entry.getName().endsWith(".class"))
                                    {
                                            String name = entry.getName();
                                            name = name.substring(0,name.lastIndexOf(".class"));
                                            if (name.indexOf("/")!= -1)
                                                    name = name.replaceAll("/", ".");
                                            if (name.indexOf("\\")!= -1)
                                                    name = name.replaceAll("\\", ".");
                                            foundClasses.add(name);
                                    }
                            }
                    }
                    return foundClasses;
            }
            throw new IllegalArgumentException("No jar-file provided");
    }

once the classes are known which are included in the jar file, you need to load each class and check if they implement the desired interface like this:

    /**
     * <p>
     * Looks inside a jar file and looks for implementing classes of the provided interface.
     * </p>
     *
     * @param file
     * The Jar-File containing the classes to scan for implementation of the given interface
     * @param iface
     * The interface classes have to implement
     * @param loader
     * The class loader the implementing classes got loaded with
     * @return A {@link List} of implementing classes for the provided interface
     * inside jar files of the <em>ClassFinder</em>s class path
     *
     * @throws Exception If during processing of the Jar-file an error occurred
     */
    public List<Class<?>> findImplementingClassesInJarFile(File file, Class<?> iface, ClassLoader loader) throws Exception
    {
        List<Class<?>> implementingClasses = new ArrayList<Class<?>>();
        // scan the jar file for all included classes
        for (String classFile : scanJarFileForClasses(file))
        {
            Class<?> clazz;
            try
            {
                // now try to load the class
                if (loader == null)
                    clazz = Class.forName(classFile);
                else
                    clazz = Class.forName(classFile, true, loader);

                // and check if the class implements the provided interface
                if (iface.isAssignableFrom(clazz) && !clazz.equals(iface))
                    implementingClasses.add(clazz);
            }
            catch (ClassNotFoundException e)
            {
                e.printStackTrace();
            }
        }
        return implementingClasses;
    }

as you can now collect all implementations of a certain interface you can simple initialize a new instance via

public void executeImplementationsOfAInJarFile(File downloadedJarFile)
{
    If (downloadedJarFile == null || !downloadedJarFile.exists())
        throw new IllegalArgumentException("Invalid jar file provided");

    URL downloadURL = downloadedJarFile.toURI().toURL();
    URL[] downloadURLs = new URL[] { downloadURL };
    URLClassLoader loader = URLClassLoader.newInstance(downloadURLs, getClass().getClassLoader());
    try
    {
        List<Class<?>> implementingClasses = findImplementingClassesInJarFile(downloadedJarFile, A.class, loader);
        for (Class<?> clazz : implementingClasses)
        {
            // assume there is a public default constructor available
            A instance = clazz.newInstance();
            // ... do whatever you like here
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}

Note that this example assumes that A is an interface. If no implementing class could be found within the Jar-File the jar file will be loaded by the classloader but no instantiation of an object will happen.

Note further that it is always good practice to provide a parent classloader - especially with URLClassLoader. Else it might happen that certain classes which are not contained in the Jar-File might be missing and therefore you will get a ClassNotFoundException on trying to access them. This is due to the delegation mechanism used by classloaders which first ask their parent if they know the class definition for the required class. If so, the class will be loaded by the parent; if not, the class will be loaded by the created URLClassLoader instead.

Keep in mind that loading the same class multiple times with different ClassLoaders is possible (peer-classloaders). But although the Name and bytes of the class might be the same, the classes are not compatible as different classloader instances are used - so trying to cast an instance loaded by classloder A to a type loaded by classloader B will fail.

@Edit: modified the code to avoid null values from being returned, instead more-or-less appropriate exceptions are thrown. @Edit2: as I am not able to accept code review suggestions I edited the review directly into the post

6
Andrey Chaschev On

To load a jar on needs to reference a URL to the downloaded file on a disk or on the web:

ClassLoader loader = URLClassLoader.newInstance(
    new URL[]{new File("my.jar").toURI().toURL()},
    getClass().getClassLoader()
);

Class<?> clazz = Class.forName("mypackage.MyClass", true, loader);

More in the post: How to load a jar file at runtime.

Update

How to scan a jar with Reflections: How to scan JAR's, which are loaded at runtime, with Google reflections library?