做者: "朱明"java
在以前分析 divide 插件的負載均衡策略時, 有看到過一行代碼:mysql
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
複製代碼
當時很簡單的略過了它的實現, 它的做用很容易分析, 調用一個看似工具類的方法, 傳入多個節點組成的集羣, 返回一個節點. 這是一個負載均衡器.git
可是細節卻很是多, 最重要的一點是使用 SPI 來選擇具體的實現類. 看看這個方法的代碼:github
public class LoadBalanceUtils {
public static DivideUpstream selector(final List<DivideUpstream> upstreamList, final String algorithm, final String ip) {
// 調用自定義的 SPI 獲得一個子類
LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm);
return loadBalance.select(upstreamList, ip);
}
}
複製代碼
後面的是調用具體子類的 select()
方法, 根據子類的不一樣實現, 最終會表現出各類形式. 目前的子類實現有:sql
關鍵就在於 ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm);
這行.數據庫
在研究它以前, 咱們先不妨研究下 Java 提供的 SPI 機制.apache
<<高可用可伸縮微服務架構>> 第3章 Apache Dubbo 框架的原理與實現 中有這樣的一句定義.緩存
SPI 全稱爲 Service Provider Interface, 是 JDK 內置的一種服務提供發現功能, 一種動態替換髮現的機制. 舉個例子, 要想在運行時動態地給一個接口添加實現, 只須要添加一個實現便可.markdown
書中也有個很是形象的腦圖, 展現了 SPI 的使用:架構
也就是說在咱們代碼中的實現裏, 無需去寫入一個 Factory 工廠, 用 MAP 去包裝一些子類, 最終返回的類型是父接口. 只須要定義好資源文件, 讓父接口與它的子類在文件中寫明, 便可經過設置好的方式拿到全部定義的子類對象:
ServiceLoader<Interface> loaders = ServiceLoader.load(Interface.class)
for(Interface interface : loaders){
System.out.println(interface.toString());
}
複製代碼
這種方式相比與普通的工廠模式, 確定是更符合開閉原則, 新加入一個子類不用去修改工廠方法, 而是編輯資源文件.
按照 SPI 的規範, 我建了一個 demo, 看看具體的實現效果
Animal 中定義一個 run()
方法, 而子類實現它.
public interface Animal {
void run();
}
public class Dog implements Animal {
@Override
public void run() {
System.out.println("狗在跑");
}
}
public class Horse implements Animal {
@Override
public void run() {
System.out.println("馬在跑");
}
}
複製代碼
使用 SPI 的加載類, 獲得子類的執行結果:
private static void test() {
final ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
for (Animal animal : load) {
System.out.println(animal);
animal.run();
}
}
複製代碼
在調用後咱們獲得以前在資源文件中寫入的實現類, 併成功調取它們各自的 run()
方法.
到這裏我產生一個疑問, 是否每次調用 ServiceLoader.load(Animal.class)
返回的都是同一個對象? 若是是我猜想它是在啓動時加載到緩存了, 若是不是, 可能就是在底層用了反射, 每次調用都有必定消耗. 咱們看看下面的實驗:
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
test();
System.out.println("----------");
}
}
private static void test() {
final ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
for (Animal animal : load) {
System.out.println(animal);
animal.run();
}
}
複製代碼
兩次調用出現的對象卻不同, 不禁讓我替其性能揪心一下, 因此咱們先分析下它的代碼, 看看到底怎麼實現.
找到 java.util,ServiceLoaders
這個類, 入眼最醒目的就是以前咱們按照規範放置資源文件的目錄
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
}
複製代碼
在 debug PREFIX
屬性的被調用處時, 發現 ServiceLoader.load
實際是使用懶加載的方式, 並無在調用它的時候, 找尋到實際返回類, 而是在遍歷時查找.
它的懶加載具體實如今以下代碼:
public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取當前的類加載器 (咱們本身的一般是弟中弟 AppClassLoader )
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
// 調用構造器初始化對象 (說明每次調用都使用新的 ServiceLoader 對象)
return new ServiceLoader<>(service, loader);
}
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();
}
public void reload() {
providers.clear();
// 建立懶加載迭代器, 傳入關鍵的接口 Class 以及加載器
lookupIterator = new LazyIterator(service, loader);
}
}
複製代碼
調用 ServiceLoader.load
後關鍵事情都沒幹, 僅僅是將接口 class 和加載器傳給 LazyIterator 這個迭代器的實現類.
看到這能夠猜想, 真正迭代調用返回的對象時, 確定須要迭代器完成實現類的搜索和初始化, 而傳參是 Class 信息和加載器, 實現類的初始化也明顯會是反射了.
看下 LazyIterator 的實現方式, 先從其最開始會被調用到的 hasNext()
開始:
private class LazyIterator implements Iterator<S> {
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
// ...
}
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
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;
}
}
複製代碼
hasNext()
的調用能夠獲取到咱們資源中的類名, 寫入到實例屬性 nextName
中, 並返回 true
, 讓迭代器能夠進行 next()
的調用
public S next() {
if (acc == null) {
return nextService();
} else {
// ...
}
}
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 反射獲得 Class 對象
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());
// 將初始化的對象放入hash緩存 (關鍵步驟)
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
複製代碼
看到這裏咱們明白了, 在初始化後會將對象放入緩存中, key 就是接口 class 二次調用不會再有反射消耗.
那麼以前咱們在測試時的方式爲何會產生不一樣對象實例呢? 緣由就是每次調用 ServiceLoader.load()
都會產生新的 ServiceLoader
對象. 咱們將測試方法改進下:
public static void main(String[] args) {
// 複用 ServiceLoaders
final ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
for (int i = 0; i < 2; i++) {
test(load);
System.out.println("----------");
}
}
private static void test(ServiceLoader<Animal> load) {
for (Animal animal : load) {
System.out.println(animal);
animal.run();
}
}
複製代碼
Java SPI 中咱們還有不少的細節沒有描述到, 但主流程就是這些. 咱們以前的兩個疑問點, 如何實現以及性能狀況也能夠獲得解答:
寫到這我有個很是疑惑的地方, 以前我以爲它和工廠方法很相似但比它有優點, 由於添加子類後僅需用改動資源文件不用變更工廠類.
但我嘗試用 Java SPI 去真正實現時, 發現並不能達到這個效果, 一個重要的緣由是, 資源文件中的各個實現類沒有區分度, 我沒法去篩選出某一個我須要的緩存在 ServiceLoaders
中的實現類.
那麼它的使用場景在哪呢?
通過查閱資料得知, 在 JDBC 中最關鍵的可插拔式驅動設計就是由 SPI 實現.
各個數據庫鏈接包中關於 JDBC 方式實現, 都須要實現其 Driver 接口, 這塊其實用的就是 SPI 的方式, 咱們看看 mysql-connector-java.jar
那麼 JDK 中的 JDBC 相關類, 是如何實現這塊的? 關鍵類就是 DriverManager
public class DriverManager {
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
// ...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 這裏就是 SPI 的實現, 迭代時實際會 Class.forName() 初始化實現類
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// ...
}
}
複製代碼
若是代碼中調用到 DriverManager 的靜態方法, 即會觸發上面這些代碼, 而這些代碼的做用即是將 SPI 資源文件中 Driver 實現類所有初始化, 那麼初始化實現類後又有什麼做用呢? 接着看看 com.mysql.jdbc.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
// 調用 DriverManager 的註冊方法, 將此 Driver 實現類註冊到 JDBC 的 Driver 管理器中
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}
複製代碼
DriverManager 的註冊方法實現很簡單, 即將入參放入靜態變量做爲全局緩存
public class DriverManager {
// 緩存 Driver 實現類
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {
if(driver != null) {
// 註冊到變量中
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
}
}
}
複製代碼
正常使用時, 咱們會直接用 DriverManager.getConnection(url, user, passwd)
獲取到鏈接, 但這裏就有疑問了, 咱們在 DriverManager 中註冊了多個 Driver, 爲何這裏能肯定一個惟一 Driver 呢?
先找到 DriverManager 的 getConnection()
方法:
public static Connection getConnection(String url, String user, String password) throws SQLException {
// ...
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException {
// ...
for(DriverInfo aDriver : registeredDrivers) {
// isDriverAllowed() 僅是經過 Class.forName() 初始化, 沒有甄別做用
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
// 最關鍵的點在這行, 篩選工做其實在實現類自身的 connect() 方法中, 會根據傳入的 url 篩選
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
} catch (SQLException ex) {
}
} else {
}
}
// ...
}
複製代碼
看看最重要的 Mysql 的 Driver 中如何實現篩選 (Driver 繼承自 NonRegisteringDriver)
public class NonRegisteringDriver implements java.sql.Driver {
private static final String URL_PREFIX = "jdbc:mysql://";
private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
public java.sql.Connection connect(String url, Properties info) throws SQLException {
// ...
// parseURL() 會匹配 url 是否符合其所在 Driver 的鏈接方式
// 這裏就是採用"約定大於配置"的思想, 經過匹配路徑頭作篩選
if ((props = parseURL(url, info)) == null) {
return null;
}
// ...
}
public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException {
// ...
// 若是 url 不匹配此 Driver 的路徑則返回null, 最外層會繼續嘗試下個 Driver
if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)
&& !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
return null;
}
// ...
}
}
複製代碼
看到這裏我想你已經瞭解 MySQL & JDBC 中關於 SPI 的實現方式了, 概括幾點
java.sql.Driver
的實現類所有初始化null
. JDBC 的 DriverManager 接收到 null
會繼續下個 Driver 實現類的調用.寫完這些分析咱們再來看若是實現個簡單的 demo.
先分享個我之前寫的方式
static {
try {
// 反射, 該類加載時會在靜態塊中, 向 DriverManager 註冊 Driver
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try (
final Connection conn = DriverManager.getConnection(url, user, passwd);
final Statement stmt = conn.createStatement();
final ResultSet rs = stmt.executeQuery("select count(1) from test")
) {
while (rs.next()) {
int count = rs.getInt("count(1)");
System.out.println(count);
}
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
雖然這樣可使用, 但不以爲有多餘的代碼嗎? 看看我新寫的方式
public static void main(String[] args) throws ClassNotFoundException {
try (
final Connection conn = DriverManager.getConnection(url, user, passwd);
final Statement stmt = conn.createStatement();
final ResultSet rs = stmt.executeQuery("select count(1) from test")
) {
while (rs.next()) {
int count = rs.getInt("count(1)");
System.out.println(count);
}
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
僅僅須要這些簡單的代碼便可, DriverManager.getConnection()
被調用時 DriverManager 會自動加載 SPI 中的實現類, 不須要咱們再去 Class.forName()
手動調用 java.mysql.Driver
的初始化.
看到這裏我想你依然明白 SPI 最最重要的做用了. 無需顯式的寫出接口對應的實現類
那麼咱們還有個在 "Java SPI 思考" 中的問題也解開了. **如何區分出 SPI 中要使用的實現類呢? 讓實現類本身斷定便可, 外層調用僅需迭代全部. **
Java 中 SPI 的使用方式咱們已經掰開來了解透徹了, 而 Soul 中的 SPI 是本身設計的, 採用 Dubbo 中 SPI 的設計思想. 在 org.dromara.soul.spi.SPI
註釋類上能夠看到相關注釋.
/** * SPI Extend the processing. * All spi system reference the apache implementation of * https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/extension. */
複製代碼
在上兩個模塊中分析 Java SPI 使用時, 發現了些缺點:
那麼 Soul SPI 的實現, 是如何解決這兩個問題的? 關鍵就在接下來的兩個子模塊中
先來看 SPI 實現項目的全貌, 項目爲 soul-spi
:
其中最核心的類就是 ExtensionLoader, 能夠說是 Soul 版的 ServiceLoader, 它也定義了 SPI 資源文件的路徑位置
public final class ExtensionLoader<T> {
private static final String SOUL_DIRECTORY = "META-INF/soul/";
}
複製代碼
經過檢查它各個方法的調用處, 咱們找到入口方法 getExtensionLoader()
public final class ExtensionLoader<T> {
private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
// ...
// 根據加載類對象取出緩存中數據, 若是沒有則新建 ExtensionLoader 對象並放入緩存
ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
if (extensionLoader != null) {
return extensionLoader;
}
LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz));
return (ExtensionLoader<T>) LOADERS.get(clazz);
}
}
複製代碼
這個方法的做用其實就像是 ServiceLoader 的 load()
方法, 會返回一個 ServiceLoader 對象.
只是 Soul 中的實現改了種方式, 將 ExtensionLoader 對象緩存起來, 這樣 二次調用時傳入相同 Class 對象也會返回一樣的 ExtensionLoader, 避免了 ServiceLoader 使用時不理解其機制致使沒有用到它的緩存, 每次迭代都去反射初始化全部實現類
再來看看 ExtensionLoader 的 getJoin()
方法, 我將它理解爲 更優的 ServiceLoader 迭代器版實現. 它一樣是作了兩件 ServiceLoader 迭代時作過的事情:
初始化 SPI 中的實現類
將實現類緩存 -> 緩存爲 Key-Value 形式的 Map 集合
基於 K-V 緩存模式, 它還作了一件我最期待的改造:
O(1)
的直接匹配實現類方式ExtensionLoader 之因此能作到這種加強型搜索, 無需每次都迭代全部, 是依靠三種不一樣類型的緩存.
這三種緩存我將它分爲二層, 它們各有不一樣用途, 總覽以下:
// 一層緩存
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// 二層緩存之一
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// 二層緩存之一
private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
複製代碼
首先是第一層緩存, 它是咱們搜索接口的具體實現類時最早接觸到的, 若是命中它則直接能夠獲得實現類的對象
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
複製代碼
它的 key
其實就是 Soul SPI 資源文件中咱們配置的信息, 好比 Divide 插件的負載均衡實現類的資源文件
而它的 value
則是 Holder 對象, 其中存有實現類的對象. 調用 getJoin()
時傳入標識 (好比 random) 得到實現類對象.
public T getJoin(final String name) {
// ...
Holder<Object> objectHolder = cachedInstances.get(name);
Object value = objectHolder.getValue();
// ...
return (T) value;
}
複製代碼
cachedClasses
存放的是 標識(random) 與 類對象 的映射
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
複製代碼
cachedClasses
緩存的信息如何填充的呢? 是直接觸發到檢索 SPI 資源文件, 而後解析成 cachedClasses
緩存. 具體方法在 loadResources()
中
private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
Properties properties = new Properties();
// 解析資源文件
properties.load(inputStream);
properties.forEach((name, classPath) -> {
// 讀出 K-V 結構並組裝成 classes, 外層調用會包裝到 cachedClasses
loadClass(classes, name, classPath);
});
}
複製代碼
joinInstances
緩存存放的是 類對象與對象實例 的映射
private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
複製代碼
這一層緩存會藉助第二層緩存, 獲得對應標識(random) 的類對象, 並經過類對象初始化實例, 緩存到自身中. 對應實現方法爲 createExtension()
private T createExtension(final String name) {
Class<?> aClass = getExtensionClasses().get(name);
Object o = joinInstances.get(aClass);
if (o == null) {
joinInstances.putIfAbsent(aClass, aClass.newInstance());
}
return (T) o;
}
複製代碼
經過 ExtensionLoader 加載某個接口的實現類時, 緩存調用流程圖以下:
// name 理解爲標識, 用於甄別 SPI 文件中, 想要獲取的某個實現類
public T getJoin(final String name) {
// ...
// cachedInstances 緩存全部 Holder 對象. Holder 對象的 value 屬性存放具體實現類
// 我將 cachedInstances 理解爲第一層緩存, 命中則直接返回要找的類
Holder<Object> objectHolder = cachedInstances.get(name);
if (objectHolder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
objectHolder = cachedInstances.get(name);
}
Object value = objectHolder.getValue();
// 雙重鎖, 若是沒有命中則調用 createExtension()
if (value == null) {
synchronized (cachedInstances) {
value = objectHolder.getValue();
if (value == null) {
value = createExtension(name);
objectHolder.setValue(value);
}
}
}
return (T) value;
}
複製代碼
private T createExtension(final String name) {
// 關鍵代碼, 搜索標識對應的類對象
Class<?> aClass = getExtensionClasses().get(name);
if (aClass == null) {
throw new IllegalArgumentException("name is error");
}
// joinInstances 理解爲第二層緩存, K-V 存放類對象與其初始化對象
Object o = joinInstances.get(aClass);
if (o == null) {
try {
joinInstances.putIfAbsent(aClass, aClass.newInstance());
o = joinInstances.get(aClass);
} catch (InstantiationException | IllegalAccessException e) {
// ...
}
}
return (T) o;
}
複製代碼
public Map<String, Class<?>> getExtensionClasses() {
// cachedClasses 爲第三層緩存, 存放標識與類對象映射
Map<String, Class<?>> classes = cachedClasses.getValue();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.getValue();
if (classes == null) {
// 構造 classes 緩存, classes 的 K-V 結構爲 標識-類對象
classes = loadExtensionClass();
cachedClasses.setValue(classes);
}
}
}
return classes;
}
複製代碼
private Map<String, Class<?>> loadExtensionClass() {
// 拿到接口的 SPI 註解
SPI annotation = clazz.getAnnotation(SPI.class);
if (annotation != null) {
String value = annotation.value();
if (StringUtils.isNotBlank(value)) {
cachedDefaultName = value;
}
}
// 構造 classes 緩存, classes 的 K-V 結構爲 標識-類對象
Map<String, Class<?>> classes = new HashMap<>(16);
loadDirectory(classes);
return classes;
}
複製代碼
private void loadDirectory(final Map<String, Class<?>> classes) {
String fileName = SOUL_DIRECTORY + clazz.getName();
try {
ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
// 讀取 SPI 資源文件
Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
: ClassLoader.getSystemResources(fileName);
if (urls != null) {
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 構造 classes 緩存, classes 的 K-V 結構爲 標識-類對象
loadResources(classes, url);
}
}
}
}
複製代碼
private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
try (InputStream inputStream = url.openStream()) {
Properties properties = new Properties();
properties.load(inputStream);
// 解析資源文件爲 KV 結構
properties.forEach((k, v) -> {
String name = (String) k;
String classPath = (String) v;
if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
try {
// 加載路徑, 傳入 classes 緩存、標識、類路徑
loadClass(classes, name, classPath);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
});
}
}
複製代碼
private void loadClass(final Map<String, Class<?>> classes, final String name, final String classPath) throws ClassNotFoundException {
// 將資源文件中的類路徑反射成類對象
Class<?> subClass = Class.forName(classPath);
// 拿到實現類的 Join 註解
Join annotation = subClass.getAnnotation(Join.class);
Class<?> oldClass = classes.get(name);
if (oldClass == null) {
// 放入入參 classes 緩存中, K-V 形式爲 標識-類對象
classes.put(name, subClass);
}
}
複製代碼