咱們知道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。