咱们知道java语言是一次编译,多平台运行。这得益于Java在设计的时候,把编译和运行是独立的两个流程。编译负责把源代码编译成 JVM 可识别的字节码,运行时加载字节码,并解释成机器指令运行。java
由于是源代码编译成字节码,因此 JVM 平台除了java语言外,还有groovy,scala等。 由于是加载字节码运行,因此有apm,自定义classloader,动态语言等技术。构成了丰富的Java 世界。mysql
编译期主要的目的是把 java 源代码编译为 符合 jvm 规范的的字节码。在运行期,由 jvm 加载字节码并执行,程序就运行起来了。git
其实java语言和 jvm 是没有绑定关系。只要符合jvm规范的字节码均可以执行,可是字节码不必定由Java语言编译而来。正因如此,jvm 平台涌现出了groovy,scala,kotlin等众多语言。sql
若是你感兴趣,也能够把把你喜欢的语言搬到 jvm 上运行。数组
在 initialization 阶段以前,只有 loading 段能够经过自定义 Classloader 添加自定义逻辑,其余阶段都是由 JVM 完成的。这就是本文想要表达的重点,Classloader 究竟能作什么呢。tomcat
在了解 Classloader 究竟能作什么以前,必需要先了解一下双亲委派模型。众所周知,java 是单继承的,classloader 也继承了这种设计思想。安全
这里针对 JDK 8 版本介绍,JDK9 以后引入了模块功能,classloader 继承关系有所变化。bash
站在 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 服务的呢?
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。