Classloader, 你究竟能干啥

2020-07-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。

参考资料

上一篇下一篇

猜你喜欢

热点阅读