• 计算机程序的思维逻辑 (87)


    上节,我们探讨了动态代理,在前几节中,我们多次提到了类加载器ClassLoader,本节就来详细讨论Java中的类加载机制与ClassLoader。

    类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。与之前介绍的反射注解、和动态代理一样,在大部分的应用编程中,我们不太需要自己实现ClassLoader。

    不过,理解类加载的机制和过程,有助于我们更好的理解之前介绍的内容,更好的理解Java。在反射一节,我们介绍过Class的静态方法Class.forName,理解类加载器有助于我们更好的理解该方法。

    ClassLoader一般是系统提供的,不需要自己实现,不过,通过创建自定义的ClassLoader,可以实现一些强大灵活的功能,比如:

    • 热部署,在不重启Java程序的情况下,动态替换类的实现,比如Java Web开发中的JSP技术就利用自定义的ClassLoader实现修改JSP代码即生效,OSGI (Open Service Gateway Initiative)框架使用自定义ClassLoader实现动态更新。
    • 应用的模块化和相互隔离,不同的ClassLoader可以加载相同的类但互相隔离、互不影响。Web应用服务器如Tomcat利用这一点在一个程序中管理多个Web应用程序,每个Web应用使用自己的ClassLoader,这些Web应用互不干扰。OSGI利用这一点实现了一个动态模块化架构,每个模块有自己的ClassLoader,不同模块可以互不干扰。
    • 从不同地方灵活加载,系统默认的ClassLoader一般从本地的.class文件或jar文件中加载字节码文件,通过自定义的ClassLoader,我们可以从共享的Web服务器、数据库、缓存服务器等其他地方加载字节码文件。

    理解自定义ClassLoader有助于我们理解这些系统程序和框架,如Tomat, JSP, OSGI,在业务需要的时候,也可以借助自定义ClassLoader实现动态灵活的功能。

    下面,我们首先来进一步理解Java加载类的过程,理解类ClassLoader和Class.forName,介绍一个简单的应用,然后我们探讨如何实现自定义ClassLoader,演示如何利用它实现热部署。

    类加载的基本机制和过程

    运行Java程序,就是执行java这个命令,指定包含main方法的完整类名,以及一个classpath,即类路径。类路径可以有多个,对于直接的class文件,路径是class文件的根目录,对于jar包,路径是jar包的完整名称(包括路径和jar包名)。

    Java运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找,如果是class文件的根目录,则直接查看是否有对应的子目录及文件,如果是jar文件,则首先在内存中解压文件,然后再查看是否有对应的类。

    负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是Class对象。类加载器不是只有一个,一般程序运行时,都会有三个:

    1. 启动类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,不是Java语言实现的,一般是C++实现的,它负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar,我们日常用的Java类库比如String, ArrayList等都位于该包内。
    2. 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Launcher$ExtClassLoader,它负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext目录中的jar包。
    3. 应用程序类加载器(Application ClassLoader):这个加载器的实现类是sun.misc.Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。

    这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader的父亲是Extension ClassLoader,Extension的父亲是Bootstrap ClassLoader,注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子ClassLoader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:

    1. 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次。
    2. 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
    3. 在父ClassLoader没有加载成功的前提下,自己尝试加载类。

    这个过程一般被称为"双亲委派"模型,即优先让父ClassLoader去加载。为什么要先让父ClassLoader去加载呢?这样,可以避免Java类库被覆盖的问题,比如用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。

    需要了解的是,"双亲委派"虽然是一般模型,但也有一些例外,比如:

    • 自定义的加载顺序:尽管不被建议,自定义的ClassLoader可以不遵从"双亲委派"这个约定,不过,即使不遵从,以"java"开头的类也不能被自定义类加载器加载,这是由Java的安全机制保证的,以避免混乱。
    • 网状加载顺序:在OSGI框架中,类加载器之间的关系是一个网,每个OSGI模块有一个类加载器,不同模块之间可能有依赖关系,在一个模块加载一个类时,可能是从自己模块加载,也可能是委派给其他模块的类加载器加载。
    • 父加载器委派给子加载器加载:典型的例子有JNDI服务(Java Naming and Directory Interface),它是Java企业级应用中的一项服务,具体我们就不介绍了。

    一个程序运行时,会创建一个Application ClassLoader,在程序中用到ClassLoader的地方,如果没有指定,一般用的都是这个ClassLoader,所以,这个ClassLoader也被称为系统类加载器(System ClassLoader)。

    下面,我们来具体看下表示类加载器的类 - ClassLoader。

    理解ClassLoader

    基本用法

    类ClassLoader是一个抽象类,Application ClassLoader和Extension ClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader和sun.misc.Launcher$ExtClassLoader,Bootstrap ClassLoader不是由Java实现的,没有对应的类。

    每个Class对象都有一个方法,可以获取实际加载它的ClassLoader,方法是: 

    public ClassLoader getClassLoader()

    ClassLoader有一个方法,可以获取它的父ClassLoader:

    public final ClassLoader getParent()

    如果ClassLoader是Bootstrap ClassLoader,返回值为null。

    比如:

    复制代码
    public class ClassLoaderDemo {
        public static void main(String[] args) {
            ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
            while (cl != null) {
                System.out.println(cl.getClass().getName());
                cl = cl.getParent();
            }
            
            System.out.println(String.class.getClassLoader());
        }
    }
    复制代码

    输出为:

    sun.misc.Launcher$AppClassLoader
    sun.misc.Launcher$ExtClassLoader
    null

    ClassLoader有一个静态方法,可以获取默认的系统类加载器:

    public static ClassLoader getSystemClassLoader()

    ClassLoader中有一个主要方法,用于加载类:

    public Class<?> loadClass(String name) throws ClassNotFoundException

    比如:

    复制代码
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    try {
        Class<?> cls = cl.loadClass("java.util.ArrayList");
        ClassLoader actualLoader = cls.getClassLoader();
        System.out.println(actualLoader);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }    
    复制代码

    需要说明的是,由于委派机制,Class的getClassLoader()方法返回的不一定是调用loadClass的ClassLoader,比如,上面代码中,java.util.ArrayList实际由BootStrap ClassLoader加载,所以返回值就是null。

    ClassLoader vs Class.forName

    反射一节,我们介绍过Class的两个静态方法forName:

    public static Class<?> forName(String className)
    public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

    第一个方法使用系统类加载器加载,第二个指定ClassLoader,参数initialize表示,加载后,是否执行类的初始化代码(如static语句块),没有指定默认为true。

    ClassLoader的loadClass方法与forName方法都可以加载类,它们有什么不同呢?基本是一样的,不过,有一个不同,ClassLoader的loadClass不会执行类的初始化代码,看个例子:

    复制代码
    public class CLInitDemo {
        public static class Hello {
            static {
                System.out.println("hello");
            }
        };
    
        public static void main(String[] args) {
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            String className = CLInitDemo.class.getName() + "$Hello";
            try {
                Class<?> cls = cl.loadClass(className);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    复制代码

    使用ClassLoader加载静态内部类Hello,Hello有一个static语句块,输出"hello",运行该程序,类被加载了,但没有任何输出,即static语句块没有被执行。如果将loadClass的语句换为:

    Class<?> cls = Class.forName(className);

    则static语句块会被执行,屏幕将输出"hello"。

    实现代码

    我们来看下ClassLoader的loadClass代码,以进一步理解其行为:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    它调用了另一个loadClass方法,其主要代码为(省略了一些代码,加了注释,以便于理解):

    复制代码
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查类是否已经被加载了
            Class c = findLoadedClass(name);
            if (c == null) {
                //没被加载,先委派父ClassLoader或BootStrap ClassLoader去加载
                try {
                    if (parent != null) {
                        //委派父ClassLoader,resolve参数固定为false
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //没找到,捕获异常,以便尝试自己加载                
                }
                if (c == null) {
                    // 自己去加载,findClass才是当前ClassLoader的真正加载方法
                    c = findClass(name);
                }
            }
            if (resolve) {
                // 链接,执行static语句块
                resolveClass(c);
            }
            return c;
        }
    }
    复制代码

    参数resolve类似Class.forName中的参数initialize,可以看出,其默认值为false,即使通过自定义ClassLoader重写loadClass,设置resolve为true,它调用父ClassLoader的时候,传递的也是固定的false。

    findClass是一个protected方法,类ClassLoader的默认实现就是抛出ClassNotFoundException,子类应该重写该方法,实现自己的加载逻辑,后文我们会看个具体例子。

    类加载应用 - 可配置的策略

    可以通过ClassLoader的loadClass或Class.forName自己加载类,但什么情况需要自己加载类呢?

    很多应用使用面向接口的编程,接口具体的实现类可能有很多,适用于不同的场合,具体使用哪个实现类在配置文件中配置,通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,这是一种策略模式,我们看个简单的示例。

    定义一个服务接口IService:

    public interface IService {
        public void action();
    }

    客户端通过该接口访问其方法,怎么获得IService实例呢?查看配置文件,根据配置的实现类,自己加载,使用反射创建实例对象,示例代码为:

    复制代码
    public class ConfigurableStrategyDemo {
        public static IService createService() {
            try {
                Properties prop = new Properties();
                String fileName = "data/c87/config.properties";
                prop.load(new FileInputStream(fileName));
                String className = prop.getProperty("service");
                Class<?> cls = Class.forName(className);
                return (IService) cls.newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        public static void main(String[] args) {
            IService service = createService();
            service.action();
        }
    }
    复制代码

    config.properties的内容示例为:

    service=shuo.laoma.dynamic.c87.ServiceB

    代码比较简单,就不赘述了。

    自定义ClassLoader

    基本用法

    Java类加载机制的强大之处在于,我们可以创建自定义的ClassLoader,自定义ClassLoader是Tomcat实现应用隔离、支持JSP,OSGI实现动态模块化的基础。 

    怎么自定义呢?一般而言,继承类ClassLoader,重写findClass就可以了。怎么实现findClass呢?使用自己的逻辑寻找class文件字节码的字节形式,找到后,使用如下方法转换为Class对象:

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)

    name表示类名,b是存放字节码数据的字节数组,有效数据从off开始,长度为len。

    看个例子:

    复制代码
    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 {
                byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException ex) {
                throw new ClassNotFoundException("failed to load class " + name, ex);
            }
        }
    }
    复制代码

    MyClassLoader从BASE_DIR下的路径中加载类,它使用了我们在57节介绍的BinaryFileUtils读取文件,转换为byte数组。MyClassLoader没有指定父ClassLoader,默认是系统类加载器,即ClassLoader.getSystemClassLoader()的返回值,不过,ClassLoader有一个可重写的构造方法,可以指定父ClassLoader:

    protected ClassLoader(ClassLoader parent) 

    用途

    MyClassLoader有什么用呢?将BASE_DIR加到classpath中不就行了,确实可以,这里主要是演示基本用法,实际中,可以从Web服务器、数据库或缓存服务器获取bytes数组,这就不是系统类加载器能做到的了。

    不过,不把BASE_DIR放到classpath中,而是使用MyClassLoader加载,确实有一个很大的好处,可以创建多个MyClassLoader,对同一个类,每个MyClassLoader都可以加载一次,得到同一个类的不同Class对象,比如:

    复制代码
    MyClassLoader cl1 = new MyClassLoader();
    String className = "shuo.laoma.dynamic.c87.HelloService";
    Class<?> class1 = cl1.loadClass(className);
    
    MyClassLoader cl2 = new MyClassLoader();
    Class<?> class2 = cl2.loadClass(className);
    
    if (class1 != class2) {
        System.out.println("different classes");
    }
    复制代码

    cl1和cl2是两个不同的ClassLoader,class1和class2对应的类名一样,但它们是不同的对象。

    这到底有什么用呢?

    • 可以实现隔离,一个复杂的程序,内部可能按模块组织,不同模块可能使用同一个类,但使用的是不同版本,如果使用同一个类加载器,它们是无法共存的,不同模块使用不同的类加载器就可以实现隔离,Tomcat使用它隔离不同的Web应用,OSGI使用它隔离不同模块。
    • 可以实现热部署,使用同一个ClassLoader,类只会被加载一次,加载后,即使class文件已经变了,再次加载,得到的也还是原来的Class对象,而使用MyClassLoader,则可以先创建一个新的ClassLoader,再用它加载Class,得到的Class对象就是新的,从而实现动态更新。

    下面,我们来具体看热部署的示例。 

    自定义ClassLoader的应用 - 热部署

    所谓热部署,就是在不重启应用的情况下,当类的定义,即字节码文件修改后,能够替换该Class创建的对象,怎么做到这一点呢?我们利用MyClassLoader,看个简单的示例。

    我们使用面向接口的编程,定义一个接口IHelloService:

    public interface IHelloService {
        public void sayHello();
    }

    实现类是shuo.laoma.dynamic.c87.HelloImpl,class文件放到MyClassLoader的加载目录中。

    演示类是HotDeployDemo,它定义了以下静态变量:

    private static final String CLASS_NAME = "shuo.laoma.dynamic.c87.HelloImpl";
    private static final String FILE_NAME = "data/c87/"
                +CLASS_NAME.replaceAll("\.", "/")+".class";
    private static volatile IHelloService helloService;

    CLASS_NAME表示实现类名称,FILE_NAME是具体的class文件路径,helloService是IHelloService实例。

    当CLASS_NAME代表的类字节码改变后,我们希望重新创建helloService,反映最新的代码,怎么做呢?先看用户端获取IHelloService的方法:

    复制代码
    public static IHelloService getHelloService() {
        if (helloService != null) {
            return helloService;
        }
        synchronized (HotDeployDemo.class) {
            if (helloService == null) {
                helloService = createHelloService();
            }
            return helloService;
        }
    }
    复制代码

    这是一个单例模式,createHelloService()的代码为:

    复制代码
    private static IHelloService createHelloService() {
        try {
            MyClassLoader cl = new MyClassLoader();
            Class<?> cls = cl.loadClass(CLASS_NAME);
            if (cls != null) {
                return (IHelloService) cls.newInstance();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    复制代码

    它使用MyClassLoader加载类,并利用反射创建实例,它假定实现类有一个public无参构造方法。

    在调用IHelloService的方法时,客户端总是先通过getHelloService获取实例对象,我们模拟一个客户端线程,它不停的获取IHelloService对象,并调用其方法,然后睡眠1秒钟,其代码为:

    复制代码
    public static void client() {
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    while (true) {
                        IHelloService helloService = getHelloService();
                        helloService.sayHello();
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                }
            }
        };
        t.start();
    }
    复制代码

    怎么知道类的class文件发生了变化,并重新创建helloService对象呢?我们使用一个单独的线程模拟这一过程,代码为:

    复制代码
    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;
                            reloadHelloService();
                        }
                    }
                } catch (InterruptedException e) {
                }
            }
        };
        t.start();
    }
    复制代码

    我们使用文件的最后修改时间来跟踪文件是否发生了变化,当文件修改后,调用reloadHelloService()来重新加载,其代码为:

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

    就是利用MyClassLoader重新创建HelloService,创建后,赋值给helloService,这样,下次getHelloService()获取到的就是最新的了。

    在主程序中启动client和monitor线程,代码为:

    public static void main(String[] args) {
        monitor();
        client();
    }

    在运行过程中,替换HelloImpl.class,可以看到行为会变化,为便于演示,我们在data/c87/shuo/laoma/dynamic/c87/目录下准备了两个不同的实现类HelloImpl_origin.class和HelloImpl_revised.class,在运行过程中替换,会看到输出不一样,如下图所示:

    使用cp命令修改HelloImpl.class,如果其内容与HelloImpl_origin.class一样,输出为"hello",如果与HelloImpl_revised.class一样,输出为"hello revised"。

    完整的代码和数据在github上,文末有链接。

    小结

    本节探讨了Java中的类加载机制,包括Java加载类的基本过程,类ClassLoader的用法,以及如何创建自定义的ClassLoader,探讨了两个简单应用示例,一个通过动态加载实现可配置的策略,另一个通过自定义ClassLoader实现热部署。

    84节到本节,我们探讨了Java中的多个动态特性,包括反射注解动态代理和类加载器,作为应用程序员,大部分用的都比较少,用的较多的就是使用框架和库提供的各种注解了,但这些特性大量应用于各种系统程序、框架、和库中,理解这些特性有助于我们更好的理解它们,也可以在需要的时候自己实现动态、通用、灵活的功能。

    注解一节,我们提到,注解是一种声明式编程风格,它提高了Java语言的表达能力,日常编程中一种常见的需求是文本处理,在计算机科学中,有一种技术大大提高了文本处理的表达能力,那就是正则表达式,大部分编程语言都有对它的支持,它有什么强大功能呢?

    (与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.dynamic.c87下)

  • 相关阅读:
    通过 SingleFlight 模式学习 Go 并发编程
    进程内优雅管理多个服务
    20192406梁健 202120222 《网络与系统攻防技术》实验四实验报告
    20192406梁健 202120222 《网络与系统攻防技术》实验五实验报告
    oracle存储过程迁移到PostgreSQL之问题总结
    frida 获取app里所有调用 java.lang.String的值
    firad 绕过ssl
    python frida 安装
    mitmproxy 夜神系统级证书
    MultiEntity AspectBased Sentiment Analysis with Context, Entity and Aspect Memory论文翻译
  • 原文地址:https://www.cnblogs.com/ivy-xu/p/6925177.html
Copyright © 2020-2023  润新知