文章目錄
1. 爲什麼要進行插件化開發?
1、解決依賴衝突
設想如下場景,不同Hadoop廠商例如HDP和CDH的中使用了hadoop-yarn-client
的不同版本,如果項目同時依賴HDP和CDH,都通過maven進行依賴引入。此時maven會根據最短路徑原則
和優先聲明
原則進行hadoop-yarn-client
版本的選擇,但此時HDP和CDH依賴都是通過AppClassLoader進行加載,其對應類加載器命名空間中只能存在一個hadoop-yarn-client
版本,導致HDP和CDH必然只能使用同一個版本的hadoop-yarn-client
,這就產生了依賴衝突問題。因此,我們的需求是同時引入兩個版本的hadoop-yarn-client
,根據項目需要,動態地加載不同版本的hadoop-yarn-client
。此時,插件化開發中的類加載器命名空間技術便應運而生,它將不同版本但全限定名相同的兩個類,加載到不同的命名空間進行隔離。我們只需要根據項目需要,動態引用不同命名空間的類即可。
2、動態加載
當我們生產上的項目不能輕易停止或重啓時,我們又想臨時接入Fusioninsight集羣,此時項目就要動態地加載Fusioninsight所使用的又一個版本的hadoop-yarn-client
。SPI和文件監聽技術粉墨登場,SPI技術有點類似IOC的思想,它將類加載的控制權轉移到程序之外,通過讀取配置文件中關於實現類的描述,來加載接口的具體實現類。文件監聽技術則可以動態地監聽JAR包的新增與更新,在發生變化時,再次將更新或新增後的插件加載到JVM中。
綜上所述,插件化開發可以實現避免依賴衝突、運⾏時通過構建參數動態加載插件等功能。本章將進行插件化中一些關鍵技術的解釋,並進行一些小實驗假以佐證。
2.類加載器命名空間
假設類加載器A有一個父類加載器B,B有一個父類加載器C,用A去加載class,根據雙親委派原則,最終用C去加載了class且加載成功,則A和B叫做class的初始類加載器,C叫做class的定義類加載器。換句話說,A委託B,B又委託C最終加載了class,則最終加載class的類加載器C叫做class的定義類加載器,A和B叫做class的初始類加載器。
每個類加載器都維護了一個列表,如果一個class將該類加載器標記爲定義類加載器或者初始類加載器,則該class會存入該類加載器的列表中,列表即命名空間。
class會在被其標記爲初始類加載器和定義類加載器的類加載器命名空間中共享,用上面的例子講,class x經過A、B、C三個類加載器進行雙親委派,並最終由C進行加載,則class x會在三者的命名空間中共享,即雖然class x是由C進行加載,但是A和B命名空間中的其他class都可以引用到class x。
3. SPI
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來加載和替換第三方插件。在面向對象的設計裏,一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了開閉原則,Java SPI就是爲某個接口尋找服務實現的機制,Java Spi的核心思想就是解耦。
3.1 示例
我們以一個例子展開,進行SPI源碼的講解:
- 支付接口
package com.imooc.spi;
import java.math.BigDecimal;
public interface PayService {
void pay(BigDecimal price);
}
- 第三方支付實現類
package com.imooc.spi;
import java.math.BigDecimal;
// 支付寶支付
public class AlipayService implements PayService{
public void pay(BigDecimal price) {
System.out.println("使用支付寶支付");
}
}
package com.imooc.spi;
import java.math.BigDecimal;
// 微信支付
public class WechatPayService implements PayService{
public void pay(BigDecimal price) {
System.out.println("使用微信支付");
}
}
- resources目錄下創建目錄META-INF/services/com.imooc.spi.PayService文件,文件內容爲實現類的全限定名
com.imooc.spi.AlipayService
com.imooc.spi.WechatPayService
- 創建測試類
package com.imooc.spi;
import com.util.ServiceLoader;
import java.math.BigDecimal;
public class PayTests {
public static void main(String[] args) {
ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class);
for (PayService payService : payServices) {
payService.pay(new BigDecimal(1));
}
// 通過迭代器進行遍歷
/*
Iterator<SPIService> spiServiceIterator = serviceLoaders.iterator();
while (spiServiceIterator != null && spiServiceIterator.hasNext()) {
SPIService spiService = spiServiceIterator.next();
System.out.println(spiService.getClass().getName() + " : " + spiService.pay());
}
*/
}
}
//結果:
//使用支付寶支付
//使用微信支付
3.2 源碼解析
- load()
public final class ServiceLoader<S> implements Iterable<S> {
// SPI文件路徑的前綴
private static final String PREFIX = "META-INF/services/";
// 需要加載的服務的類或接口
private Class<S> service;
// 用於定位、加載和實例化提供程序的類加載器
private ClassLoader loader;
// 創建ServiceLoader時獲取的訪問控制上下文
private final AccessControlContext acc;
// 按實例化順序緩存Provider
private LinkedHashMap<String, S> providers = new LinkedHashMap();
// 懶加載迭代器
private LazyIterator lookupIterator;
// 1. 獲取上下文類加載器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 2. 調用構造方法
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
// 3. 校驗參數和ClassLoad
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
//4. 清理緩存容器,實例懶加載迭代器
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
}
可以看到,load方法僅僅是給ServiceLoader的成員屬性service和loader進行了賦值,其中service是接口的class對象,loader是線程上下文類加載器默認爲ApplicationClassLoader,最終調用了reload方法將service和loader賦值給了內部類ServiceLoader.LazyIterator的成員屬性。
- hasNext() & next()
衆所周知,當對Iterable類使用foreach循環時,其實是在調用其內部迭代器Iterator的hasNext方法和next方法,而ServiceLoader中Iterator類型的匿名內部類中的hasNext方法和next方法又調用了內部類ServiceLoader.LazyIterator中的hasNext方法和next方法,因此ServiceLoader.LazyIterator類的hasNext和next方法會在foreach循環中進行調用。
public final class ServiceLoader<S> implements Iterable<S> {
// SPI文件路徑的前綴
private static final String PREFIX = "META-INF/services/";
// 需要加載的服務的類或接口
private Class<S> service;
// 用於定位、加載和實例化提供程序的類加載器
private ClassLoader loader;
// 創建ServiceLoader時獲取的訪問控制上下文
private final AccessControlContext acc;
// 按實例化順序緩存Provider
private LinkedHashMap<String, S> providers = new LinkedHashMap();
// 懶加載迭代器
private LazyIterator lookupIterator;
// 內部迭代器
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String, S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
// Private inner class implementing fully-lazy provider lookup
private class LazyIterator implements Iterator<S> {
// 需要加載的服務的類或接口
Class<S> service;
// 用於定位、加載和實例化提供程序的類加載器
ClassLoader loader;
// 枚舉類型的資源路徑
Enumeration<URL> configs = null;
// 迭代器
Iterator<String> pending = null;
// 配置文件中下一行className
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
//到classpath下尋找以接口全限定名命名的文件,並返回其中的實現類的全限定名
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 獲取實現類全限定名
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加載實現類,如果已存在於JVM則直接返回
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
// 實例化並進行類轉換
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() {
return hasNextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() {
return nextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}
可以看到,hasNextService()方法負責到配置文件中查找當前實現類的全限定名;nextService()方法則負責調用Class.forName方法進行當前實現類的class的返回。
4. class緩存查找機制
在分析過SPI的源碼之後,我們發現,最終插件會通過Class.forName(cn, false, loader)
進行加載。但衆所周知,如果Class.forName
發現JVM中已經存在該class,則會直接返回class對象,而不必重複加載。另,如果JVM中不存在該class,則會通過loader類加載器進行加載,而根據《插件化開發基礎篇-ClassLoader》中的ClassLoader源碼分析可知,loader類加載器中的loadclass
方法中,會調用findLoadedClass
方法進行緩存的查找,如果JVM中已經存在該class則直接返回,否則再次進行加載。
要想徹底理順插件化開發的機制,我們必須搞清楚Class.forName
和findLoadedClass
兩個方法查找JVM中class的機制,究竟是查找類加載器命名空間下是否存在該class,還是跨命名空間進行JVM全局查找,亦或是其他?我們通過兩個實驗來尋找答案。
4.1 findLoadedClass緩存查找
首先,自定義一個類加載器MyClassLoader
import java.io.*;
public class MyClassLoader extends ClassLoader {
/**
* 重寫findClass方法
* @param name 是我們這個類的全路徑
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = this.getClass().getResourceAsStream(fileName);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (IOException e){
throw new ClassNotFoundException(name);
}
}
}
接下來,進行如下實驗:
/**
* 注意事項:使用如上自定義類加載,且把實驗所用到的RemoteADSServiceImpl.class文件放到resources目錄下
*/
public class Test {
public static void main(String[] args) throws Exception {
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
//系統類加載器,AppClassLoader
ClassLoader cl1 = ClassLoader.getSystemClassLoader();
//自定義來加載器,沒有自己的構造器,自動調用super(),因而父加載器爲AppClassLoader
ClassLoader cl2 = new MyClassLoader();
cl1.loadClass("com.custom.RemoteADSServiceImpl");
Object test1 = m.invoke(cl1, "com.custom.RemoteADSServiceImpl");
System.out.println(test1 != null);//true
cl2.loadClass("com.custom.RemoteADSServiceImpl");
Object test2 = m.invoke(cl2, "com.custom.RemoteADSServiceImpl");
System.out.println(test2 != null);//false,原因:雖然調用了cl2的加載方法,但是根據雙親委派原則,實際加載使用的是cl1
cl2.loadClass("com.custom.RemoteADSServiceImpl");
Object test3 = m.invoke(cl1, "com.custom.RemoteADSServiceImpl");
System.out.println(test3 != null);//true,原因:同test2
}
}
結論:findLoadedClass判斷class是否存在,與類加載器命名空間無關,它只能返回以當前類加載器作爲定義類加載器的class,以當前類加載器作爲初始加載器的class則不能被當做緩存進行返回
4.2 Class.forName緩存查找
/**
* 說明:實驗通過在ClassLoader類中的loadclass方法上打斷點,判斷是否進行類加載
* 走loadclass即沒有命中緩存,使用參數中的classLoader中的loadclass方法重新加載
* 不走loadclass,即命中了緩存
*/
public class Test {
public static void main(String[] args) throws Exception {
java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
//系統類加載器,AppClassLoader
ClassLoader cl1 = ClassLoader.getSystemClassLoader();
//自定義類加載器,沒有自己的構造器,自動調用super(),因而父加載器爲AppClassLoader
ClassLoader cl2 = new MyClassLoader();
Class x = Class.forName("com.custom.RemoteADSServiceImpl", false, cl1);//走loadclass
Class y = Class.forName("com.custom.RemoteADSServiceImpl", false, cl1);//不走loadclass
Class x = Class.forName("com.custom.RemoteADSServiceImpl", false, cl1);//走loadclass
Class y = Class.forName("com.custom.RemoteADSServiceImpl", false, cl2);//走loadclass
// 原因:cl2是cl1的子類加載器,第一步直接用cl1進行加載,cl2並沒有被標記爲該class的初始加載器,即RemoteADSServiceImpl.class不在cl2的命名空間
Class x = Class.forName("com.custom.RemoteADSServiceImpl", false, cl2);//走loadclass
Class y = Class.forName("com.custom.RemoteADSServiceImpl", false, cl1);//不走loadclass
// 原因:cl2是cl1的子類,第一步其實在用cl1進行加載,cl1被RemoteADSServiceImpl.class標記爲定義加載器,cl2被RemoteADSServiceImpl.class標記爲初始加載器,因此RemoteADSServiceImpl.class在cl1和cl2的命名空間中共享
Class x = Class.forName("com.custom.RemoteADSServiceImpl", false, cl2);//走loadclass
Class y = Class.forName("com.custom.RemoteADSServiceImpl", false, cl2);//不走loadclass
// 原因:cl2是cl1的子類,第一步雖然是在用cl1進行加載,但是cl2被RemoteADSServiceImpl.class標記爲初始加載器,因此RemoteADSServiceImpl.class在cl1和cl2的命名空間中共享
}
}
結論:Class.forName判斷class是否存在,與類加載器命名空間相關,如果class將當前類加載器標記爲定義類加載器或者初始類加載器,則能直接返回
5. 文件監聽機制
5.1 示例
public class Test {
public static void main(String[] args) throws Exception {
File directory = new File("/Users/djg/Downloads");
// 輪詢間隔 5 秒
long interval = TimeUnit.SECONDS.toMillis(5);
// step1:創建observer
FileAlterationObserver observer = new FileAlterationObserver(directory);
// step2:設置listener
observer.addListener(new MyFileListener());
// step3:創建monitor
FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
// step4:啓動monitor
monitor.start();
}
}
final class MyFileListener extends FileAlterationListenerAdaptor {
@Override
public void onDirectoryCreate(File file) {
System.out.println(file.getName() + " director created.");
}
@Override
public void onDirectoryChange(File file) {
System.out.println(file.getName() + " director changed.");
}
@Override
public void onDirectoryDelete(File file) {
System.out.println(file.getName() + " director deleted.");
}
@Override
public void onFileCreate(File file) {
System.out.println(file.getName() + " created.");
}
@Override
public void onFileChange(File file) {
System.out.println(file.getName() + " changed.");
}
@Override
public void onFileDelete(File file) {
System.out.println(file.getName() + " deleted.");
}
}
5.2 源碼解析
- 啓動Monitor輪詢線程
public final class FileAlterationMonitor implements Runnable {
private final long interval;
private final List<FileAlterationObserver> observers = new CopyOnWriteArrayList<FileAlterationObserver>();
private Thread thread = null;
private ThreadFactory threadFactory;
private volatile boolean running = false;
// 啓動方法
public synchronized void start() throws Exception {
if (running) {
throw new IllegalStateException("Monitor is already running");
}
for (FileAlterationObserver observer : observers) {
observer.initialize();
}
running = true;
// 起一個文件監控線程
if (threadFactory != null) {
thread = threadFactory.newThread(this);
} else {
thread = new Thread(this);
}
thread.start();
}
public void run() {
while (running) {
for (FileAlterationObserver observer : observers) {
// 每次輪詢,執行一次文件監控邏輯
observer.checkAndNotify();
}
if (!running) {
break;
}
// 每隔interval時長輪詢一次
try {
Thread.sleep(interval);
} catch (final InterruptedException ignored) {
}
}
}
}
- 文件增刪改判斷
public class FileAlterationObserver implements Serializable {
private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>();
private final FileEntry rootEntry;
private final FileFilter fileFilter;
private final Comparator<File> comparator;
public void checkAndNotify() {
// 每次輪詢都會執行,FileAlterationListener#onStart一般不需要去複寫
for (FileAlterationListener listener : listeners) {
listener.onStart(this);
}
File rootFile = rootEntry.getFile();
if (rootFile.exists()) {
checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
} else if (rootEntry.isExists()) {
checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
} else {
// Didn't exist and still doesn't
}
// 每次輪詢都會執行,FileAlterationListener#onStop一般不需要去複寫
for (FileAlterationListener listener : listeners) {
listener.onStop(this);
}
}
// Compare two file lists for files which have been created, modified or deleted
// previous表示上一次輪詢時根目錄下的所有文件
// files表示當前根目錄下所有文件
private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) {
int c = 0;
FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
for (FileEntry entry : previous) {
while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
current[c] = createFileEntry(parent, files[c]);
doCreate(current[c]);
c++;
}
if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
// 當文件沒有發生增減,判斷文件是否被修改
doMatch(entry, files[c]);
checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
current[c] = entry;
c++;
} else {
checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
doDelete(entry);
}
}
for (; c < files.length; c++) {
current[c] = createFileEntry(parent, files[c]);
doCreate(current[c]);
}
parent.setChildren(current);
}
// 判斷文件是否發生修改
private void doMatch(FileEntry entry, File file) {
if (entry.refresh(file)) {
for (FileAlterationListener listener : listeners) {
if (entry.isDirectory()) {
listener.onDirectoryChange(file);
} else {
listener.onFileChange(file);
}
}
}
}
}
- 文件修改具體判斷邏輯
public class FileEntry implements Serializable {
// 將當前文件各項屬性和緩存進行對比,FileEntry對象是文件緩存,file參數是當前文件
// 取一些關鍵屬性進行對比,如字節長度,最近修改時間,文件名等等
public boolean refresh(File file) {
// cache original values
boolean origExists = exists;
long origLastModified = lastModified;
boolean origDirectory = directory;
long origLength = length;
// refresh the values
name = file.getName();
exists = file.exists();
directory = exists ? file.isDirectory() : false;
lastModified = exists ? file.lastModified() : 0;
length = exists && !directory ? file.length() : 0;
// Return if there are changes
return exists != origExists ||
lastModified != origLastModified ||
directory != origDirectory ||
length != origLength;
}
}
聊到這裏,你已經掌握了插件化開發的大部分技術細節了,要想更多地學習插件化在實際場景用的應用,請關注下一篇文章《插件化開發應用篇》
參考:
https://www.cnblogs.com/ygj0930/p/6628429.html
https://blog.csdn.net/sureyonder/article/details/5564181
https://www.jianshu.com/p/0adea1b03d52
https://stackoverflow.com/questions/39284030/java-classloader-difference-between-defining-loader-initiating-loader