Java动态加载jar

本文深入探讨了Java中动态加载类的两种方法:通过自定义ClassLoader和使用当前ClassLoader。详细讲解了自定义ClassLoader如何打破双亲委派机制,并讨论了由此可能引发的类转换异常。同时,对比了两种方法的优缺点,为读者提供了实际项目中动态加载类的选择依据。

前言

       最近做项目,有需求需要运行过程动态加载jar,然后执行后加载别的jar,来达到实现不同的动态能力,下面实现了2种方式,各有各的优势。

1. bean

 

 随手写了一个bean,打个jar,扔在了resources下

 

2. 新建classloader方式

public class LoadMain {
    public static void main(String[] args) {
        loadByNewClassLoader();
    }

    public static void loadByNewClassLoader() {
        try {
            File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
            URL[] urls = new URL[]{file.toURI().toURL()};
            URLClassLoader urlClassLoader = new URLClassLoader(urls);

            Class<?> c = urlClassLoader.loadClass("com.feng.classloader.common.Cat");
            Object o = c.newInstance();
            Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
            Method getName = c.getDeclaredMethod("getName", new Class[]{});
            setName.invoke(o, "Tom");
            System.out.println(getName.invoke(o));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

这种方式classloader

我们自定义了classloader,装载,卸载很方便,直接操作class loader即可,缺陷是

URLClassLoader加载的class,只能当前classloader能访问;由于双亲委派,APPClassLoader加载的是可以直接访问的。

2.1 双亲委派原理

核心方法urlClassLoader.loadClass("com.feng.collections.Cat"),实际上是调用父类ClassLoader类的loadClass方法

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //加锁
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //检查是否已加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //有父加载器,优先父加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                        //Bootstrap去找
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //自己去找了
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //解析class
                resolveClass(c);
            }
            return c;
        }
    }

优先父加载器加载,比如URLClassLoader就是默认使用父加载器加载的。只是这里我们加载的是jar,没有依赖;所以只能URLClassLoader自己去加载,也只能在URLClassLoader中访问。

parent来源getSystemClassLoader()

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
    }

看看我们加载的类

2.2 覆盖loadClass方法

打破双亲委派机制

public class LoadMain {
    public static void main(String[] args) {
        loadByNewClassLoader();
    }

    public static void loadByNewClassLoader() {
        try {
            File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
            URL[] urls = new URL[]{file.toURI().toURL()};
            URLClassLoader urlClassLoader = new DefURLClassLoader(urls);

            Class<?> c = urlClassLoader.loadClass("com.feng.classloader.common.Cat");
            Object o = c.newInstance();
            Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
            Method getName = c.getDeclaredMethod("getName", new Class[]{});
            setName.invoke(o, "Tom");
            System.out.println(getName.invoke(o));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private static class DefURLClassLoader extends URLClassLoader{

        public DefURLClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }

        public DefURLClassLoader(URL[] urls) {
            super(urls);
        }

        public DefURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
            super(urls, parent, factory);
        }

        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            //加锁
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                //检查是否已加载
                Class<?> c = findLoadedClass(name);

                try {
                    if (this.getParent() != null && name.startsWith("java.")) {
                        c = super.loadClass(name, false);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //自己去找
                    c = findClass(name);
                }

                if (resolve) {
                    //解析class
                    resolveClass(c);
                }
                return c;
            }
        }
    }
}

可以看见,我们自己实现的类不让父加载器优先加载了,直接自己干活,这就打破了双亲委派 

 

测试ok

这就会造成一个严重的问题,由于我们没有按照JDK官方的加载顺序,很容易出现同一个类的对象不能转换的问题

2.3 不能转换异常

在load的module依赖common的module

随意写一个类

public class Dog {

    public String getCatName(Cat cat){
        return cat.getName();
    }
}

写个main方法,就在上面的改一改

public static void loadByNewClassLoader() {
        try {
            File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
            URL[] urls = new URL[]{file.toURI().toURL()};
            URLClassLoader urlClassLoader = new DefURLClassLoader(urls);

            Class<?> c = urlClassLoader.loadClass("com.feng.classloader.common.Cat");
            Object o = c.newInstance();
            Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
            Method getName = c.getDeclaredMethod("getName", new Class[]{});
            setName.invoke(o, "Tom");
            System.out.println(getName.invoke(o));

            //就加这一句代码
            System.out.println(new Dog().getCatName((Cat) o));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

报错了:是不是很有意思,相同的类居然不能转换

这是因为,这个对象是我们自定义的classloader加载的,且打破双亲委派机制, Class是由我们的classloader自己干活加载的,而当前线程的Class是APPClassLoader加载器加载的,所以不能转换。

 

如果不覆写loadClass,双亲委派生效,则符合JDK的标准,代码运行正常,因为是父加载器去加载的。

直接看也是这样

3. 使用当前的classloader加载

public static void loadByCurrentClassLoader(){
        Method method = null;

        try {
            File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
            
            URL[] urls = new URL[]{file.toURI().toURL()};

            //use current classloader load jar
            URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
            method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            //set method reach
            method.setAccessible(true);

            method.invoke(urlClassLoader, urls);

            Class<?> c = Class.forName("com.feng.classloader.common.Cat", false, urlClassLoader);
            Object o = c.newInstance();
            Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
            Method getName = c.getDeclaredMethod("getName", new Class[]{});
            setName.invoke(o, "Tom");
            System.out.println(getName.invoke(o));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (method != null) {
                method.setAccessible(false);
            }
        }
    }

这里不能直接调用addURL方法,因为是protected修饰,但反射可以 

 运行后结果

使用系统的classloader,加载jar,当前classloader均能访问,缺陷很明显,卸载这些class loader加载的class异常麻烦。在ASM或者JDK的动态代理就是使用这种方式生成字节码加载class的。

总结

       各有各的好处吧,通过新创建classloader适合热加载,加载卸载方便;通过当前classloader加载方便业务的调用,常在AOP中使用。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值