1

I'm using java to do some thing as same as C++ Dynamic Library usage.

I didn't find the way to directly use the Same Class Object without reflect invoke style code.

this is my dynamic library code, I make it a jar.

package com.demo;

public class Logic {
    public String doWork() {
        System.out.println("Hello from Dll");
        return "Dll";
    }
}

In my main application, I can create the instance by URLClassLoader, invoke by reflect is fine:

public class Main {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        File file = new File("C:\\plugin.jar");
        URL url = file.toURI().toURL();
        URL[] urls = {url};

        ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader loader = new URLClassLoader(urls, parentLoader);
        Thread.currentThread().setContextClassLoader(loader);

        Class<?> clazz = loader.loadClass("com.demo.Logic");

        System.out.println("New Instance!!");
        Object logic = clazz.newInstance();
        Method method = logic.getClass().getMethod("doWork");
        method.invoke(logic);
}

Output:

New Instance!!
Hello from Dll

But when I change the code without using Reflect invoke:

public class Main {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        File file = new File("C:\\plugin.jar");
        URL url = file.toURI().toURL();
        URL[] urls = {url};

        ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader loader = new URLClassLoader(urls, parentLoader);
        Thread.currentThread().setContextClassLoader(loader);

        Class<?> clazz = loader.loadClass("com.demo.Logic");

        Logic logic = (Logic)clazz.newInstance();
        logic.doWork();
    }
}

Compile success(compile with external modules), but when I run the program, it failed at the line Logic logic = (Logic)clazz.newInstance();

Exception:

Exception in thread "main" java.lang.NoClassDefFoundError: com/demo/Logic
    at Main.main(Main.java:31)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Caused by: java.lang.ClassNotFoundException: com.demo.Logic
    at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
    ... 6 more

Is there any way to make it work? Without reflect/interface.(In C++ I can easily achieve this, share same struct/class declare, make sure using same compiler compile two parts. IMHO Java would do it also)


additional explanation 1

I want change the current classloader behavior to make it recognize the dynamic loaded class, this try is to simple and naive, can't find other direction:

        ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader loader = new URLClassLoader(urls, parentLoader);
        Thread.currentThread().setContextClassLoader(loader);
Gohan
  • 2,422
  • 2
  • 26
  • 45
  • welcome to the world of classloaders ... you'll can test to see that `Logic.class != clazz` - they are loaded by different classloaders, so they are different classes (even though they have the same class name) – ZhongYu May 14 '15 at 14:25
  • relevant - http://stackoverflow.com/questions/30227510/sandbox-for-memory/30227614#30227614 – ZhongYu May 14 '15 at 14:27
  • @bayou.io Yes, they are different classloaders, I want to make the origin classloader recognized the class dynamicly – Gohan May 15 '15 at 02:22

2 Answers2

2

In order to make this work, you have to logically split the classes up into three sets:

  1. Your main class
  2. Your plugin classes
  3. Your plugin-dependent classes

When you creating a new class loader, you have to ensure that classes #2 and #3 are both loaded by the same class loader because URLClassLoader delegation only goes towards the parent. That means classes in the JVM application class loader, which loaded your main class, cannot see the class in your new class loader. In order to work like C, you have to update the classpath of your main class, and this is not supported (it is possible, but it is not supported; I've read that Java 9 will remove this capability).

In practice, you should split your main class into two pieces (#1 and #3), and then use reflection to load/invoke the plugin-dependent class (one reflection call, and if your plugin-dependent class implements Runnable, you could use ((Runnable)loadClass("PluginDependent").newInstance()).run() to reduce even that). However, you need to ensure your URLClassLoader does not delegate the load of the plugin-dependent class, for example:

  1. Split your application into the three discrete sets listed above (main.jar, plugin.jar, and main-plugin-dependent.jar), and list all of them on the URLClassLoader.

  2. Change the creation of the URLClassLoader to specify an explicit null parent so that it will not delegate to the JVM application class loader, and then specify both plugin.jar and your main JAR.

  3. Write a custom URLClassLoader that overrides loadClass to ensure that your plugin-dependent classes are loaded by that class loader rather than being delegated to the JVM application class loader.

Brett Kail
  • 33,593
  • 2
  • 85
  • 90
  • I think I may understand your solution, make the `direct call code` && `code implement` loaded by same ClassLoader, call once to do all other `direct call` things. What I hardly want is make the `direct call code` in Main project as same as normal java code, looks like it need a few changes in the Main project for each `direct call code` – Gohan May 15 '15 at 02:18
  • If you need to call across several times, then create an interface in your "main" code and implement it in your "plugin-dependent class" that calls code in the plugin class. i.e., this is the same as the Runnable suggestion I gave, except you control the interface, so you can add as many cross-class-loader calls as needed. – Brett Kail May 15 '15 at 02:21
1

I tested your code with a small change (used default class loader and placed the Logic in my classpath). This works:

public class Main {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();

        Class<?> clazz = parentLoader.loadClass("com.demo.Logic");

        Logic logic = (Logic)clazz.newInstance();
        logic.doWork();
    }
}

Your problem lies in retrieving the class from the plugin.jar.


Updated:

I also tried retrieving the class from the jar with the code from the second example. I put the compiled Logic.class in com\demo and built the jar with jar cvf plugin.jar .\com\demo\Logic.class and put in the same path as you did. It worked without an issue as well.

Also, you don't need to set the current's thread class loader to loader. At least not for the purposes of this example.

Your plugin.jar may actually not contain the class.


Update to:

additional explanation 1

I want change the current classloader behavior to make it recognize the dynamic loaded class, this try is to simple and naive, can't find other direction:

ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();
ClassLoader loader = new URLClassLoader(urls, parentLoader);
Thread.currentThread().setContextClassLoader(loader);

You are doing it right with child class loader.

But if you want to "change the current classloader behavior" you should read this answer

Classloaders are meant to be immutable; you shouldn't be able to willy-nilly add classes to it at runtime.

thus the solution you came up with a child class loader.

That is why i said "you don't need to set the current's thread class loader to loader (child classloader)."

Community
  • 1
  • 1
Laurentiu L.
  • 6,566
  • 1
  • 34
  • 60
  • Plugin.jar is created by IDEA artifacts jar. `Class> clazz = loader.loadClass("com.demo.Logic");` Worked so the class is in the jar. I'll try jar the class manually – Gohan May 15 '15 at 02:05
  • I find that your solution didn't use dynamic load jar by Main class. Your ClassLoader already recognized the class by presetting classpath – Gohan May 15 '15 at 02:51