美文网首页
Classloader, 你究竟能干啥

Classloader, 你究竟能干啥

作者: 但莫 | 来源:发表于2020-07-11 18:11 被阅读0次

    我们知道java语言是一次编译,多平台运行。这得益于Java在设计的时候,把编译和运行是独立的两个流程。编译负责把源代码编译成 JVM 可识别的字节码,运行时加载字节码,并解释成机器指令运行。

    因为是源代码编译成字节码,所以 JVM 平台除了java语言外,还有groovy,scala等。
    因为是加载字节码运行,所以有apm,自定义classloader,动态语言等技术。构成了丰富的Java 世界。

    javac 编译流程

    javac 编译流程
    1. parse:读取.java源文件,做词法分析(LEXER)和语法分析(PARSER)
    2. enter:生成符号表
    3. process:处理注解
    4. attr:检查语义合法性、常量折叠
    5. flow:数据流分析
    6. desugar:去除语法糖
    7. generate:生成字节码

    编译期主要的目的是把 java 源代码编译为 符合 jvm 规范的的字节码。在运行期,由 jvm 加载字节码并执行,程序就运行起来了。

    其实java语言和 jvm 是没有绑定关系。只要符合jvm规范的字节码都可以执行,但是字节码不一定由Java语言编译而来。正因如此,jvm 平台涌现出了groovy,scala,kotlin等众多语言。

    如果你感兴趣,也可以把把你喜欢的语言搬到 jvm 上运行。

    类的生命周期

    类的声明周期
    1. loading:加载。是第一个阶段,主要是加载字节码,静态存储结构转化为方法区数据结构,生成class对象。这里没有限制字节码的来源,可以是文件、zip,网络、jsp,甚至是加密文件。这个阶段可以使用自定义 classloader 实现自定义行为,这就给字节码带来了很多可能的玩法。
    2. verification:验证。确保字节码符合 jvm 规范。
    3. preparation:准备。是正式为类中定义的变量设置初始值。
    4. resolution:解析。将常量池内的符号引用替换为直接引用的过程。
    5. initialization: 初始化。这里将程序的主导权交给了应用程序,会执行·<clinit>()和构造函数。
    6. using:使用。使用初始化后的类,这里就到了应用逻辑的范畴。
    7. unloading:卸载。需要满足该类所有实例已经被GC,加载该类的ClassLoader已经被GC,该类的java.lang.Class对象已经没有被引用。在tomcat jsp 热加载的场景会用到,每个jsp都是单独的 classloader,当jsp由变动时,会卸载旧的classloader,创建新的classloader加载jsp,这样就实现了热加载。

    在 initialization 阶段之前,只有 loading 段可以通过自定义 Classloader 添加自定义逻辑,其他阶段都是由 JVM 完成的。这就是本文想要表达的重点,Classloader 究竟能做什么呢。

    双亲委派

    在了解 Classloader 究竟能做什么之前,必须要先了解一下双亲委派模型。众所周知,java 是单继承的,classloader 也继承了这种设计思想。

    这里针对 JDK 8 版本介绍,JDK9 之后引入了模块功能,classloader 继承关系有所变化。

    双亲委派

    站在 JVM 的角度,只有两种加载器,一种是Bootstrap classloader,由C++或者java实现。另一种是其他 classloader。都是用java语言编写,继承自 java.lang.ClassLoader 抽象类。

    jdk 8 classloader 继承关系
    1. Application Classloader。负责加载用户路径下的类,如果没有自定义类加载器,这个就是默认的类加载器。
    2. Extension Classloader。负责加载<JAVA_HOME>\lib\ext,或java.ext.dirs系统变量所
      指定的路径中所有的类库。
    3. BootStrap Classloader。负责加载<JAVA_HOME>\lib,-Xbootclasspath参数指定的类。应用获取不到这个 Classloader ,以null代替。

    ClassLoader 应用案例

    上面简单介绍的是背景知识,下面是重头戏。在了解了javac 编译流程,类的生命周期,classloader 双亲委派之后,能用它来做什么呢。

    在了解“类的生命周期”之后,知道 ClassLoader 只有在 loading 阶段课可以可以自定义,其他阶段都是由 JVM 实现的。下面我看看几个应用场景,直观的感受一下。

    Java SPI 中的应用

    Java SPI (Service Provider Interface) 是动态加载服务的机制。可以按照规则实现自己的SPI,使用 ServiceLoader 加载服务。

    Java SPI 的组件:

    1. 服务接口: 一个接口或者抽象类定义服务功能。
    2. 服务提供方: 服务接口的实现,提供具体的服务。
    3. 配置文件:需要在 META-INF/services 目录下放置一个服务接口名相同的文件,每一行是一个实现类的全类名。
    4. ServiceLoader:Java SPI 的主类,用来通过服务接口加载服务实现,有很多工具方法,可实现重新加载服务。

    Java SPI Example

    实现一个 SPI 并且使用 ServiceLoader 加载服务。

    1. 定义服务接口
    public interface MessageServiceProvider {
        void sendMessage(String message);
    }
    
    1. 定义服务接口
      实现 email 和 推送消息连个实现。
    public class EmailServiceProvider implements MessageServiceProvider {
        public void sendMessage(String message) {
            System.out.println("Sending Email with Message = "+message);
        }
    }
    public class PushNotificationServiceProvider implements MessageServiceProvider {
        public void sendMessage(String message) {
            System.out.println("Sending Push Notification with Message = "+message);
        }
    }
    
    1. 编写服务配置
      在 META-INF/services 创建 util.spi.MessageServiceProvider 文件,内容是服务类全路径
    util.spi.EmailServiceProvider
    util.spi.PushNotificationServiceProvider
    
    1. ServiceLoader 加载服务
      最后,通过 ServiceLoader 加载服务并测试。
    public class ServiceLoaderTest {
      public static void main(String[] args) {
        ServiceLoader<MessageServiceProvider> serviceLoader = ServiceLoader
            .load(MessageServiceProvider.class);
        for (MessageServiceProvider service : serviceLoader) {
          service.sendMessage("Hello");
        }
    }    
    

    输出如下:

    Sending Email with Message = Hello
    Sending Push Notification with Message = Hello
    

    下面是项目文件结构:

    项目结构

    Java SPI class loader 的思考

    ServiceLoader 类在 rt.jar 包中,应该是由 Bootstrap Classloader 加载,而 EmailServiceProvider 是我定义的类,应该是由 Application Classloader 加载。先验证一下这个想法。

    ServiceLoader<MessageServiceProvider> serviceLoader = ServiceLoader.load(MessageServiceProvider.class);
    System.out.println(ServiceLoader.class.getClassLoader());
    
    for (MessageServiceProvider service : serviceLoader) {
    System.out.println(service.getClass().getClassLoader());
    }
    

    结果如下:

    // ServiceLoader 由 Bootstrap Classloader 加载,获取不到classLoader
    null 
    // 由 Application Classloader 加载
    jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
    

    按照classloader的继承关系,Bootstrap Classloader 是不能加载应用类的,那ServiceLoader是如何引用到 SPI 服务的呢?

    java.util.ServiceLoader#load(java.lang.Class<S>)

    看下load方法做了什么。

    1. ①,③,是同一个 ClassLoader ,是main线程的 contextClassLoader,而main线程的 contextClassLoader 是jvm设置的。有了这个线程,可以推测 ServiceLoader 是通过 contextClassLoader 加载服务的。
    2. ②是要加载的服务。
    image
    1. 从调用栈可以看到 ServiceLoader 的迭代器是通过懒加载的方式加载服务。
    2. ① 是 Application Classloader,从线程上下文中获取的。
    3. ② 使用线程 contextClassLoader 加载的服务实现,绕开了双亲委派。

    jdbc driver 也是SPI服务

    mysql 驱动包中也由驱动服务接口的实现配置。


    image

    DriverManager 在加载的时候会调用 loadInitialDrivers 方法加载驱动服务

    // DriverManager.loadInitialDrivers()
    private static void loadInitialDrivers() {
           AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
    
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                }
            }
        }
    }    
    // com.mysql.cj.jdbc.Driver
    // 把自己注册到 DriverManager 中
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    

    因为服务是懒加载的,所以会遍历迭代器,在Mysql 驱动类中,会把自己注册到 DriverManager 中,这样就 DriverManager 中就管理了所有的驱动程序。

    自定义文件名

    有些时候可能需要防止正常的访问,可以通过自定义 ClassLoader ,在loading的时候进行处理

    比如 lombok,使用 ShadowClassLoader 加载SCL.lombok文件 。


    image

    加密 class 文件

    实现一个加密class文件,并使用自定义 ClassLoader 加载的 demo。

    1. 加密 class 文件

    使用 xor 的方式加密,因为两次 xor 等于原值,是一种比较简单的方式,安全级别更高的话可以通过JNI或者公私钥的方式。

    /**
    * 解密/解密 class文件
    */
    public static byte[] decodeClassBytes(byte[] bytes) {
        byte[] decodedBytes = new byte[bytes.length];
        for (int i = 0; i < bytes.length; i++) {
          decodedBytes[i] = (byte) (bytes[i] ^ 0xFF);
        }
        return decodedBytes;
    }
    
    1. 编写加密类
      类的逻辑比较简单,构造的时候打印一句话。编译后的class会通过上一步的方法加密,重命名为.class_文件用来区分。
    public class MyClass {
      public MyClass(){
        System.out.println("My class");
      }
    }
    

    加密后的文件是不能通过正常方式解析的,可以用javap命令验证一下

    D:\workspace\mygit\jdk-learn\jdk8\src\main\resources>javap -v  lang.classloader.encrypt.Myclass
    错误: 读取lang.classloader.encrypt.Myclass的常量池时出错: unexpected tag at #1: 245
    
    1. 编写自定义 ClassLoader
      首先定义一个引导类,引导类由自定义 ClassLoader加载。之后引导类创建类时会使用 自定义 ClassLoader 加载。这个流程和 Tomcat 自定义classLoader 是一样的。
    public class MyCustomClassLoader extends ClassLoader {
    
      // 加密的 class
      private Collection<String> encryptClass = new HashSet<>();
      // 忽略的类,未加密的类
      private Collection<String> skipClass = new HashSet<>();
    
      public void init() {
        skipClass.add("lang.classloader.encrypt.EncryptApp");
        encryptClass.add("lang.classloader.encrypt.MyClass");
      }
    
      @Override
      public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 由父类加载的类
        if (name.startsWith("java.")
            && !encryptClass.contains(name)
            && !skipClass.contains(name)) {
          return super.loadClass(name);
        } 
        // 未加密的类
        else if (skipClass.contains(name)) {
          try {
            String classPath = name.replace('.', '/') + ".class";
            //返回读取指定资源的输入流
            URL resource = getClass().getClassLoader().getResource(classPath);
            InputStream is = resource != null ? resource.openStream() : null;
            if (is == null) {
              return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
    
            //将一个byte数组转换为Class类的实例
            return defineClass(name, b, 0, b.length);
          } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
          }
        }
        // 加密的类
        return findClass(name);
      }
    
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 加载类文件内容
        byte[] bytes = getClassFileBytesInDir(name);
        // 解密
        byte[] decodedBytes = decodeClassBytes(bytes);
        // 初始化类,由 jvm 实现
        return defineClass(name, decodedBytes, 0, bytes.length);
      }
    
      // 读取加密class文件
      private static byte[] getClassFileBytesInDir(String className) throws ClassNotFoundException {
        try {
          return FileUtils.readFileToByteArray(
              new File(className.replace(".", "//") + ".class_"));
        } catch (IOException e) {
          throw new ClassNotFoundException(className, e);
        }
      }
    }
    
    1. 测试程序
      测试时,先创建自定义类加载器,然后用自定义类加载器去加载启动类,启动类会使用自定义类加载器去加载MyClass。

    通过反射调用 EncryptApp 方法的说明很重要,可以尝试直接类型转换看看抛出的异常。

    public class EncryptApp {
      public void printClassLoader() {
        System.out.println("EncryptApp:" + this.getClass().getClassLoader());
        System.out.println("MyClass.class.getClassLoader() = " + MyClass.class.getClassLoader());
        new MyClass();
      }
    }
    
      public static void main(String[] args)
          throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        MyCustomClassLoader myCustomClassLoader = new MyCustomClassLoader();
        myCustomClassLoader.init();
    
        Class<?> startupClass = myCustomClassLoader.loadClass("lang.classloader.encrypt.EncryptApp");
        
        // 重要:必须通过反射的方式获取方法,
        // 因为当前线程的classloader,和加载 EncryptApp 的不一样,
        // 所以不能类型转换,必须用object
        Object encryptApp = startupClass.getConstructor().newInstance();
        String methodName = "printClassLoader";
        Method method = encryptApp.getClass().getMethod(methodName);
        method.invoke(encryptApp);
      }
    

    结果如下:

    // EncryptApp 是有 MyCustomClassLoader 加载
    EncryptApp:lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
    // EncryptApp 启动类加载 MyClass 也是使用 MyCustomClassLoader
    MyClass.class.getClassLoader() = lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
    My class
    
    image

    总结

    ClassLoader 是一个重要的工具,但是平时很少需要自定义一个 ClassLoader 。通过自定义 ClassLoader 加载字节码还是令人兴奋的。

    从类的生命周期理解 ClassLoader,更清楚它能做什么。很多时候需要结合字节码技术,更能发挥他的威力。很多框架也是这么做的,比如 APM。

    参考资料

    • 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
    • 深入理解 jvm 字节码

    相关文章

      网友评论

          本文标题:Classloader, 你究竟能干啥

          本文链接:https://www.haomeiwen.com/subject/enxecktx.html