框架基础知识——ClassLoader

一、类加载过程

类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。一个类可以有多个实例,但是只有一个对应的Class对象。

 ClassLoader加载机制

类加载器不止有一个,一般程序运行时,会有三个ClassLoader,分别是:

  • 启动类加载器(Bootstrap ClassLoader)—— 由Java虚拟机创建,负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar;
  • 扩展类加载器(Extension ClassLoader)—— 对应sun.misc.Launcher$ExtClassLoader,负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext;
  • 应用类加载器(Application ClassLoader)—— 对应sun/misc.Launcher$AppClassLoader,负责加载应用程序的类。

这三个类加载器虽然不是父子继承关系,但是存在父子委派关系,即子ClassLoader委托父ClassLoader优先加载,加载失败后再自己尝试加载。比如AppClassLoader加载类时会优先通过parent变量指向的ExtClassLoader加载,ExtClassLoader又委派Bootstrap ClassLoader加载。这种“双亲委派”机制能避免Java类库被覆盖的问题。

ClassLoader主要通过loadClass方法加载类:

/**
 * Loads the class with the specified <a href="#name">binary name</a>.  The
 * default implementation of this method searches for classes in the
 * following order:
 */
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        // 首先,检查是否已经加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    //如果父类不为空,交给父类去加载
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                //如果依然找不到,调用findClass方法去加载,自定义ClassLoader一般需要重写此方法
                c = findClass(name);
            }
        }
        return c;
    }
}

 

二、自定义ClassLoader

Java类加载机制的强大之处在于,我们可以自定义ClassLoader,也就是按照自己的逻辑寻找.class字节码文件,并生成Class对象。这在发挥Java语言的动态性上起着非常重要的作用。

通过上面的例子知道,ClassLoader的最主要方法是loadClass(),loadClass方法找不到类后最后会调用findClass(name),这是自定义ClassLoader需要重写的主要方法,可以仿照如下方式重写:

    public class MyClassLoader extends ClassLoader {

        private static final String BASE_DIR = "data/c87/";

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            String fileName = name.replaceAll("\\.", "/");
            fileName = BASE_DIR + fileName + ".class";  //得到字节码文件完整路径
            try {
                /**
                 * 首先将.class文件转化为二进制字节流
                 */
                FileInputStream is = new FileInputStream(fileName);
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int len = 0;
                try {
                    while ((len = is.read()) != -1) {
                        bos.write(len);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                byte[] bytes = bos.toByteArray();
                is.close();
                bos.close();

                //通过defineClass方法将二进制字节流转化为Class对象
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException ex) {
                throw new ClassNotFoundException("failed to load class " + name, ex);
            }
        }
    }

其中defineClass方法由本地native方法完成,不需要自己写。自定义ClassLoader后,有两大好处:

  • 加载时机更加灵活,我们可以按照自己的逻辑在任意时刻重新加载Class对象。
  • .class文件存储位置更加灵活,不光可以存储于类目录中,也可以存储于网络或数据库等位置。

 

三、热部署

自定义ClassLoader的一个重要应用场景:热部署。所谓热部署,就是在不重启应用的情况下,当类的定义即字节码文件被修改后,能够替换该Class创建的对象。

主要流程是:

  1. 创建自定义ClassLoader
  2. 加载指定目录下目标类的.class字节码文件
  3. 运用反射创建目标类实例
  4. 监听.class字节码文件变化
  5. .class字节码文件被修改后,重新加载并创建实例

首先,定义目标类:我们定义一个接口IHelloService,并定义其实现类HelloImpl,HelloImpl即我们的目标类,编译后得到字节码文件HelloImpl.class

    public interface IHelloService {
        public void sayHello();
    }

    public class HelloImpl implements IHelloService {
        public void sayHello() {
            System.out.println("hello");
        }
    }

然后,仿照上面自定义ClassLoader,加载HelloImpl.class字节码文件,并运用反射生成HelloImpl实例:

    //单例,获取HelloImpl实例
    public static IHelloService getHelloService() {
        if (helloService != null) {
            return helloService;
        }
        synchronized (HotDeployDemo.class) {
            if (helloService == null) {
                helloService = createHelloService();
            }
            return helloService;
        }
    }

    private static IHelloService createHelloService() {
        try {
            MyClassLoader cl = new MyClassLoader();  //自定义ClassLoader
            Class<?> cls = cl.loadClass(CLASS_NAME);  //最终调用findClass和defineClass方法加载我们编译好的HelloImpl.class字节码文件,生成HelloImpl类
            if (cls != null) {
                return (IHelloService) cls.newInstance();  //反射创建HelloImpl实例
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

调用HelloImpl实例:

    public static void client() {
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    while (true) {
                        IHelloService helloService = getHelloService();
                        helloService.sayHello();
                        Thread.sleep(1000);  //为方便验证,每隔1s调用一次
                    }
                } catch (InterruptedException e) {
                }
            }
        };
        t.start();
    }

此时,输出如下:

hello
hello
hello

监听.class字节码文件变化,如果监听到HelloImpl.class文件发生变化,则重新创建HelloImpl实例:

    //这里以线程轮寻方式监听.class文件修改日期变化
    public static void monitor() {
        Thread t = new Thread() {
            private long lastModified = new File(FILE_NAME).lastModified();

            @Override
            public void run() {
                try {
                    while (true) {
                        Thread.sleep(100);
                        long now = new File(FILE_NAME).lastModified();
                        if (now != lastModified) {
                            lastModified = now;
                            //如果监听到HelloImpl.class文件发生变化,则重新创建HelloImpl实例
                            reloadHelloService();
                        }
                    }
                } catch (InterruptedException e) {
                }
            }
        };
        t.start();
    }

    private static void reloadHelloService() {
        helloService = createHelloService();
    }

我们修改HelloImpl源码,并将重新编译的class字节码文件覆盖原文件:

   public class HelloImpl implements IHelloService {
        public void sayHello() {
            System.out.println("hello,world");  //修改原文件逻辑
        }
    }

此时,不用重启应用,发现输出已经发生了变化:

hello
hello
hello
hello,world
hello,world

至此,我们已经完成了一次热部署。

 

posted @ 2019-07-11 15:24  西贝雪  阅读(420)  评论(0)    收藏  举报