Classloader, 你究竟能干啥
我们知道java语言是一次编译,多平台运行。这得益于Java在设计的时候,把编译和运行是独立的两个流程。编译负责把源代码编译成 JVM 可识别的字节码,运行时加载字节码,并解释成机器指令运行。
因为是源代码编译成字节码,所以 JVM 平台除了java语言外,还有groovy,scala等。
因为是加载字节码运行,所以有apm,自定义classloader,动态语言等技术。构成了丰富的Java 世界。
javac 编译流程
javac 编译流程- parse:读取.java源文件,做词法分析(LEXER)和语法分析(PARSER)
- enter:生成符号表
- process:处理注解
- attr:检查语义合法性、常量折叠
- flow:数据流分析
- desugar:去除语法糖
- generate:生成字节码
编译期主要的目的是把 java 源代码编译为 符合 jvm 规范的的字节码。在运行期,由 jvm 加载字节码并执行,程序就运行起来了。
其实java语言和 jvm 是没有绑定关系。只要符合jvm规范的字节码都可以执行,但是字节码不一定由Java语言编译而来。正因如此,jvm 平台涌现出了groovy,scala,kotlin等众多语言。
如果你感兴趣,也可以把把你喜欢的语言搬到 jvm 上运行。
类的生命周期
类的声明周期- loading:加载。是第一个阶段,主要是加载字节码,静态存储结构转化为方法区数据结构,生成class对象。这里没有限制字节码的来源,可以是文件、zip,网络、jsp,甚至是加密文件。这个阶段可以使用自定义 classloader 实现自定义行为,这就给字节码带来了很多可能的玩法。
- verification:验证。确保字节码符合 jvm 规范。
- preparation:准备。是正式为类中定义的变量设置初始值。
- resolution:解析。将常量池内的符号引用替换为直接引用的过程。
- initialization: 初始化。这里将程序的主导权交给了应用程序,会执行·<clinit>()和构造函数。
- using:使用。使用初始化后的类,这里就到了应用逻辑的范畴。
- 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 继承关系- Application Classloader。负责加载用户路径下的类,如果没有自定义类加载器,这个就是默认的类加载器。
- Extension Classloader。负责加载<JAVA_HOME>\lib\ext,或java.ext.dirs系统变量所
指定的路径中所有的类库。 - 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 的组件:
- 服务接口: 一个接口或者抽象类定义服务功能。
- 服务提供方: 服务接口的实现,提供具体的服务。
- 配置文件:需要在 META-INF/services 目录下放置一个服务接口名相同的文件,每一行是一个实现类的全类名。
- ServiceLoader:Java SPI 的主类,用来通过服务接口加载服务实现,有很多工具方法,可实现重新加载服务。
Java SPI Example
实现一个 SPI 并且使用 ServiceLoader 加载服务。
- 定义服务接口
public interface MessageServiceProvider {
void sendMessage(String message);
}
- 定义服务接口
实现 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);
}
}
- 编写服务配置
在 META-INF/services 创建 util.spi.MessageServiceProvider 文件,内容是服务类全路径
util.spi.EmailServiceProvider
util.spi.PushNotificationServiceProvider
- 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方法做了什么。
- ①,③,是同一个 ClassLoader ,是main线程的 contextClassLoader,而main线程的 contextClassLoader 是jvm设置的。有了这个线程,可以推测 ServiceLoader 是通过 contextClassLoader 加载服务的。
- ②是要加载的服务。
- 从调用栈可以看到 ServiceLoader 的迭代器是通过懒加载的方式加载服务。
- ① 是 Application Classloader,从线程上下文中获取的。
- ② 使用线程 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。
- 加密 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;
}
- 编写加密类
类的逻辑比较简单,构造的时候打印一句话。编译后的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
- 编写自定义 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);
}
}
}
- 测试程序
测试时,先创建自定义类加载器,然后用自定义类加载器去加载启动类,启动类会使用自定义类加载器去加载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 字节码