我们知道java语言是一次编译,多平台运行。这得益于Java在设计的时候,把编译和运行是独立的两个流程。编译负责把源代码编译成 JVM 可识别的字节码,运行时加载字节码,并解释成机器指令运行。
因为是源代码编译成字节码,所以 JVM 平台除了java语言外,还有groovy,scala等。 因为是加载字节码运行,所以有apm,自定义classloader,动态语言等技术。构成了丰富的Java 世界。
编译期主要的目的是把 java 源代码编译为 符合 jvm 规范的的字节码。在运行期,由 jvm 加载字节码并执行,程序就运行起来了。
其实java语言和 jvm 是没有绑定关系。只要符合jvm规范的字节码都可以执行,但是字节码不一定由Java语言编译而来。正因如此,jvm 平台涌现出了groovy,scala,kotlin等众多语言。
如果你感兴趣,也可以把把你喜欢的语言搬到 jvm 上运行。
在 initialization 阶段之前,只有 loading 段可以通过自定义 Classloader 添加自定义逻辑,其他阶段都是由 JVM 完成的。这就是本文想要表达的重点,Classloader 究竟能做什么呢。
在了解 Classloader 究竟能做什么之前,必须要先了解一下双亲委派模型。众所周知,java 是单继承的,classloader 也继承了这种设计思想。
这里针对 JDK 8 版本介绍,JDK9 之后引入了模块功能,classloader 继承关系有所变化。
站在 JVM 的角度,只有两种加载器,一种是Bootstrap classloader,由C++或者java实现。另一种是其他 classloader。都是用java语言编写,继承自 java.lang.ClassLoader 抽象类。
上面简单介绍的是背景知识,下面是重头戏。在了解了javac 编译流程,类的生命周期,classloader 双亲委派之后,能用它来做什么呢。
在了解“类的生命周期”之后,知道 ClassLoader 只有在 loading 阶段可以自定义字节码,其他阶段都是由 JVM 实现的。下面我看看几个应用场景,直观的感受一下。
Java SPI (Service Provider Interface) 是动态加载服务的机制。可以按照规则实现自己的SPI,使用 ServiceLoader 加载服务。
Java SPI 的组件:
实现一个 SPI 并且使用 ServiceLoader 加载服务。
public interface MessageServiceProvider { void sendMessage(String message); } 复制代码
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); } } 复制代码
util.spi.EmailServiceProvider util.spi.PushNotificationServiceProvider 复制代码
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 复制代码
下面是项目文件结构:
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 服务的呢?
看下load方法做了什么。
mysql 驱动也由驱动接口,通过 SPI 的方式加载的。
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文件 。
实现一个加密class文件,并使用自定义 ClassLoader 加载的 demo。
使用 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; } 复制代码
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 复制代码
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); } } } 复制代码
通过反射调用 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 复制代码
ClassLoader 是一个重要的工具,但是平时很少需要自定义一个 ClassLoader 。通过自定义 ClassLoader 加载字节码还是令人兴奋的。
从类的生命周期理解 ClassLoader,更清楚它能做什么。很多时候需要结合字节码技术,更能发挥他的威力。很多框架也是这么做的,比如 APM。