類載入
類載入器進階
系統載入位元組碼檔案主要有三步:裝載 -> 連線 -> 初始化。
類載入時機
-
類載入時機
簡單理解:位元組碼檔案什麼時候會被載入到記憶體中?
有以下的幾種情況:
- 建立類的例項(物件)
- 呼叫類的類方法
- 訪問類或者介面的類變數,或者為該類變數賦值
- 使用反射方式來強制建立某個類或介面對應的java.lang.Class物件
- 初始化某個類的子類
- 直接使用java.exe命令來執行某個主類
總結而言:用到了就載入,不用不載入
類載入過程
裝載 loading
過一個類的全限定名來獲取定義此類的二進位制位元組流,將這個位元組流所代表的靜態儲存結構轉化為執行時資料結構。
裝載完畢記憶體中生成代表這個類的java.lang.Class
物件
- 確定了將來物件的大小和是否需要補齊
連線 linking
驗證 verify
確保Class檔案位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身安全
確保載入類的資訊符合JVM規範,例如:以0xcafebabe開頭,就沒有安全問題(在notepad++安裝HEX-Editor檢視驗證)。
準備 prepare
正式為靜態變數在方法區中開闢儲存空間並設定預設值。這裡“通常情況”是設定靜態變數的預設值,比如我們定義了public static int value = 11,那麼value變數在準備階段設定的初始值就是0,而不是11(初始化階段才會顯示賦值)。
特殊情況:比如給value變數加上了fianl關鍵字public static final int value = 11,那麼準備階段value的值就被賦值為11。
解析 resolve
將虛擬機器常量池內的符號引用(常量名)替換為直接引用(地址)的過程(在IDEA安裝Jclasslib外掛檢視)。
將類的二進位制資料流中的符號引用替換為直接引用
例如成員變數位置定義了String型別的name,在載入本類的時候String類是否載入這是虛擬機器不知道的,此時的String其實是用符號替代的;在解析階段會將臨時的符號變為String的引用(如0x0002)並找到String類。
- JVM針對類或介面、欄位、方法等內容進行解析,方法資訊會形成虛方法表 vtable
初始化(initialization)
執行類構造器 <clinit>()
方法的過程,也就是把編譯時期自動收集類中所有靜態變數的賦值動作和靜態程式碼塊中的賦值語句合併。
<clinit> ()
方法對於類或介面來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成clinit()方法。
類載入器
類載入器的作用
Java程式被編譯器編譯之後成為位元組碼檔案(.class檔案),當程式第一次需要使用某個類時,虛擬機器便會將對應的位元組碼檔案進行載入,從而建立出對應的Class物件。而這個將位元組碼檔案載入到虛擬機器記憶體的過程,這個就是由類載入器(ClassLoader)來完成的。
類載入器的分類
虛擬機器內部提供了三種類載入器(JDK1.8):
- Bootstrap class loader: 啟動類載入器,虛擬機器的內建類載入器,通常表示為null ,並且沒有父null;底層C++實現,隨著虛擬機器啟動。
- Extension class loader:平臺類載入器,負責載入JDK中一些特殊的模組。
- System class loader:系統類載入器,負責載入使用者類路徑上所指定的類庫(預設從類的根路徑下載入)
類載入器的繼承關係(這裡的繼承關係不是Extends,而是邏輯上的繼承)
-
System的父載入器為Extension
-
Extension的父載入器為Bootstrap
-
程式碼演示
//系統/應用類載入器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //平臺/擴充套件類載入器 ClassLoader platformClassLoader = systemClassLoader.getParent(); //啟動類載入器 ClassLoader bootstrapClassLoader = platformClassLoader.getParent(); System.out.println(systemClassLoader); //jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b System.out.println(platformClassLoader); //jdk.internal.loader.ClassLoaders$PlatformClassLoader@378bf509 System.out.println(bootstrapClassLoader); //null
- 引導類載入器 BootStrapClassLoader
這個類載入器使用C/C++語言實現的,巢狀在JVM內部,透過Java程式碼無法獲得。
用來載入Java核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類
並不繼承java.lang.ClassLoader,沒有父載入器
處於安全考慮,Bootstrap啟動類載入器值載入包名為java、javax、sun開頭的類
擴充套件類載入器和應用程式載入器,並指定他們的父類載入器
使用-XX:+TraceClassLoading引數檢視類載入情況
- 擴充套件類載入器 Extension class loader
由java語言編寫,繼承於ClassLoader類,sun.misc.Launcher$ExtClassLoader
父類載入器為啟動類載入器
從java.ext.dirs系統屬性所指定的目錄中載入類庫,或從JDK的安裝目錄的jre/lib/ext子目錄下載入類庫,如果使用者建立jra放在此目錄下,也會由擴充套件類載入器載入
- 系統類載入器 System class loader
java語言編寫,繼承於ClassLoader類,sun.smisc.Launcher$AppClassLoader
父載入器為擴充套件類載入器
負責載入環境變數classpath或系統屬性java.class.path指定路徑下的類(載入自定義類)
透過ClassLoader的getSystemClassLoader()方法獲取獲取到該類載入器
使用者類載入器 User class loader
在Java的日常引用程式開發中,類載入器幾乎由上述三種類加器相互配合執行的。在必要的時候,開發人員可以自定義類載入器,來制定類的載入方法
體現java語言強大生命力和魅力的關鍵因素之一,便是java開發者可以自定義類載入器來實現類庫的動態載入,可以是本地的jar,也可以是網路上的資源
透過類載入器可以實現非常絕妙的外掛機制。類載入器為應用程式提供類一種動態增加新功能的機制。
自定義類載入器能夠實現應用隔離和位元組碼加密等功能。
自定義類載入透過需要繼承於ClassLoader,其父類載入器為系統載入器。
- 示例:載入不同的類時,採用的載入類
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // 輸出:null --> 啟動類載入器
ClassLoader classLoader2 = DNSNameService.class.getClassLoader();
System.out.println(classLoader2); // 輸出:sun.misc.Launcher$ExtClassLoader@2503dbd3
ClassLoader classLoader3 = Test01.class.getClassLoader();
System.out.println(classLoader3); // 輸出:sun.misc.Launcher$AppClassLoader@18b4aac2
- 注意:手動呼叫classLoad.loadClass()方法不會進行初始化 initiallization
雙親委派模型
如果一個類載入器收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類的載入器去執行,如果父類載入器還存在其父類載入器,則進一步向上委託,依次遞迴,請求最終將到達頂層的啟動類載入器,如果父類載入器可以完成類載入任務,就成功返回,倘若父類載入器無法完成此載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模式
載入範圍:
- 啟動類載入器
jre -> lib -> rt.jar
- 擴充套件類載入器
jre -> lib -> ext -> *.jar
- 應用類載入器 載入
classpath
中的jar包/檔案:系統類載入器預設從類的根路徑下(也就是src路徑下載入),這樣也可以載入根路徑下的某些資原始檔。
雙親委派機制的好處,避免類的俯衝載入,確保類的全域性唯一性,同時保護程式安全,防止核心API被隨意篡改。
例如:如果自己寫了一個java.lang.String類就會因為雙親委派機制不能被載入,不會破壞原生的String類的載入。
ClassLoader 中的兩個方法
- 方法介紹
方法名 | 說明 |
---|---|
public static ClassLoader getSystemClassLoader() | 獲取系統類載入器 |
public InputStream getResourceAsStream(String name) | 載入某一個資原始檔 |
從類的根路徑下載入某一個資原始檔,類的根路徑是src資料夾
-
示例程式碼
public class ClassLoaderDemo2 { public static void main(String[] args) throws IOException { //static ClassLoader getSystemClassLoader() 獲取系統類載入器 //InputStream getResourceAsStream(String name) 載入某一個資原始檔 //獲取系統類載入器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //利用載入器去載入一個指定的檔案 //引數:檔案的路徑(放在src的根目錄下,預設去那裡載入) //返回值:位元組流。 InputStream is = systemClassLoader.getResourceAsStream("prop.properties"); Properties prop = new Properties(); prop.load(is); System.out.println(prop); is.close(); } }
獲取當前的工作路徑
System.out.println(System.getProperty("user.dir"));
可以根據這個路徑獲取某些檔案
XML
作為配置檔案:用來儲存程式在執行時需要的一些引數
比如IDEA:儲存背景圖片、字型資訊、字號資訊、主題資訊
常見的配置檔案:
- .txt
如果要儲存IDEA 的配置資訊:
\idea\background.png
微軟雅黑
23
Windows 10 Light
但是隻看配置檔案,不知道每個值表達的意思,所以會有properties檔案:
- .properties
background=\idea\background.png
fontfamily=微軟雅黑
fontsize=23
theme=Windows 10 Light
如果要配置的資訊比較複雜,properties就比較麻煩了
例如拼圖遊戲的資訊,如果有多個使用者的配置:
user=zhangsan,lisi
進度=50%,70%
遊戲圖片=Animal1,Animal2
遊戲背景色=白色,白色
這樣讀取時非常不方便,找每個使用者都要對,
切割,只有在配置資訊比較簡單時可以使用properties
- .xml
優點:易於閱讀,可以配置成組出現的資料
缺點:解析比較複雜
XML概述
-
全球資訊網聯盟(W3C)
全球資訊網聯盟(W3C)建立於1994年,又稱W3C理事會。1994年10月在麻省理工學院電腦科學實驗室成立。
建立者: Tim Berners-Lee (蒂姆·伯納斯·李)。
是Web技術領域最具權威和影響力的國際中立性技術標準機構。
到目前為止,W3C已釋出了200多項影響深遠的Web技術標準及實施指南,-
如廣為業界採用的超文字標記語言HTML(標準通用標記語言下的一個應用)
-
可擴充套件標記語言XML(標準通用標記語言下的一個子集)
-
以及幫助殘障人士有效獲得Web資訊的無障礙指南(WCAG)等
-
單元測試
測試階段分為:單元測試、整合測試、系統測試、驗收測試
測試方法可以分為:白盒測試、黑盒測試、灰盒測試
對應的測試階段:
Junit
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
單元測試類名為XxxTest,測試方法為public void testXxx(){}
常見註解
@ParamterizedTest //引數化測試,測試方法可以指定入參,不需要再寫@Test
@ValueSource //為測試方法的入參提供引數
@ParameterizedTest
@ValueSource(strings = {"412302201909137037","412302201909137017","412302201909137027"})
@DisplayName("獲取性別測試")
public void getGender2Test(String id) {
String gender = userService.getGender(id);
System.out.println(gender);
}
會按照引數中陣列的資料依次呼叫測試方法並傳遞入參
但是這種方式只能為一個入參的方法傳遞引數,如果有多個入參就需要使用@CsvSource:
@ParameterizedTest
@CsvSource({"admin,123456","zhangsan,123","lisi,123"})
public void test01(String username, String password){
}
會將引數以 , 分割並傳遞給方法的入參
斷言
在上文中,測試方法只能呼叫得到結果後輸出到控制檯,測試方法應該提供的功能是計算方法的返回值和預期值是否相等,Junit提供了斷言:
注意:入參最後的msg代表不相等時的提示資訊,可以不指定。
日誌體系
記錄程式執行過程中的所有資訊,並且可以永久儲存
- 可以將系統執行的資訊選擇性的記錄到指定的位置
- 隨時以開關的形式控制是否記錄日誌
日誌框架的體系:Commons Logging介面設計的不盡人意,後來設計了Simple Logging Facade for Java,slf4j是介面層,Ceki Gülcü團隊最先開發了log4j,並基於log4j最佳化改進出了logback,最後採用全新的disruptor框架重構log4j的底層並且取logback之精華重新搭建了log4j2框架。log4j2推翻了log4j和logback所有的底層實現,但是複用了log4j的幾大模組和介面
logback
logback.com
logback主要分為三個技術模組:
-
logback-core:其他兩個模組的基礎程式碼
-
logback-classic:完整實現了slf4j API的模組
-
logback-access:模組與Tomcat和Jetty等Servlet容器整合,以提供HTTP訪問日誌的功能
首先匯入jar包,然後將核心配置檔案logback.xml
複製到src目錄下
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE :表示當前的日誌資訊是可以輸出到控制檯的。
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流物件 預設 System.out 改為 System.err-->
<target>System.out</target>
<encoder>
<!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度
%msg:日誌訊息,%n是換行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</pattern>
</encoder>
</appender>
<!-- File是輸出的方向通向檔案的 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!--日誌輸出路徑-->
<file>C:/code/itheima-data.log</file>
<!--指定日誌檔案拆分和壓縮規則-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日誌檔案輸出的檔名,%i表示序號-->
<fileNamePattern>C:/code/itheima-data2-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
<!--最大檔案大小,超過這個大小會觸發滾動到新檔案,預設為10MB-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
</appender>
<!--
level:用來設定列印級別,大小寫無關:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
, 預設debug
<root>可以包含零個或多個<appender-ref>元素,標識這個輸出位置將會被本日誌級別控制。
-->
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>
</configuration>
然後就可以在程式碼中獲取日誌的物件了:
public static final Logger LOGGER = LoggerFactory.getLogger("類物件");
//登入操作
Scanner sc = new Scanner(System.in);
System.out.print("請輸入使用者名稱:");
String username = sc.nextLine();
System.out.print("請輸入密碼:");
String password = sc.nextLine();
if ("zhangsan".equals(username) && "123".equals(password)){
System.out.println("登入成功");
LOGGER.info("使用者於此時登入成功,使用者名稱為 " + username + " 密碼為 " + password);
}else {
System.out.println("登入失敗");
LOGGER.info("使用者於此時登入失敗,使用者名稱為 " + username + " 密碼為 " + password);
}
此時儲存的日誌檔案a.txt:
2023-04-16 13:58:11.891 [main] INFO LogDemo.LogTest01 - 使用者於此時登入失敗,使用者名稱為 zhangsna 密碼為 123
2023-04-16 13:58:37.325 [main] INFO LogDemo.LogTest01 - 使用者於此時登入成功,使用者名稱為 zhangsan 密碼為 123
配置檔案詳解
Logback日誌系統的特性都是透過核心配置檔案logback.xml控制的
logback日誌輸出位置、格式設定:
-
透過
<appender>
標籤可以設定輸出位置和日誌資訊的詳細格式 -
通常可以設定2個日誌輸出位置:控制檯和系統檔案
輸出到控制檯的配置標誌:
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流物件 預設 System.out 改為 System.err-->
<target>System.out</target>
<encoder>
<!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度
%c 當前類名
%msg:日誌訊息,%n是換行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</pattern>
</encoder>
</appender>
輸出到系統檔案的配置標誌:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
class中指定的就是做完整操作的類
<!-- File是輸出的方向通向檔案的 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!--日誌輸出路徑-->
<file>C:/code/itheima-data.log</file>
<!--指定日誌檔案拆分和壓縮規則-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--透過指定壓縮檔名稱,來確定分割檔案方式-->
<fileNamePattern>C:/code/itheima-data2-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
<!--檔案拆分大小-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
</appender>
日誌級別:級別程度依次是: TRACE < DEBUG < INFO < WARN < ERROR;預設級別是debug(忽略大小寫)
-
作用:用於控制系統中哪些日誌級別是可以輸出的,只輸出級別不低於設定級別的日誌資訊
-
ALL和OFF的作用分別是開啟全部日誌資訊,關閉全部日誌資訊
在<root level = "INFO">
標籤的level屬性中設定日誌級別
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>
使用了Lombok之後,使用@Slf4j就可以直接使用log物件
SPI
簡介
SPI機制:Service Provider Interface,JDK內建的一種服務提供發現機制,使得程式的擴充套件(切換實現)可以輕鬆實現,以實現介面和實現類之間的解耦。
為什麼需要SPI
基於OCP和依賴倒置原則,模組之間的通訊一般基於介面進行,通常情況下呼叫者模組並不知道被呼叫者模組內部的具體實現。
為了實現模組裝配時不必在程式中指明(分層解耦),這就需要一種服務發現機制,Java SPI就是提供了這樣的機制:為某個介面尋找服務實現的機制,類似於IoC的思想,將裝配的控制權放在程式之外。
重新理解SPI
SPI是專門給服務提供者或擴充套件框架功能的開發者使用的一個介面,SPI將服務介面和具體的服務分離開來,將服務呼叫方和服務實現者解耦,修改或替換服務的實現並不需要修改呼叫方。這也是Java提供的輕量級外掛化機制。
SPI的簡單案例
搜尋介面的定義:
public interface ResourceSearchService {
/**
* 根據資源型別批次獲取資源資料
* @param resourceTypeAndIdDTO
* @return
*/ List<ResourceVO> getResourceDetail(ResourceTypeAndIdDTO resourceTypeAndIdDTO);
}
影片搜尋實現
public class VideoResourceServiceImpl implements ResourceSearchService {
@Override
public List<ResourceVO> getResourceDetail(ResourceTypeAndIdDTO resourceTypeAndIdDTO) {
System.out.println("Search Video");
return null;
}
}
文獻搜尋實現
public class PaperResourceServiceImpl implements ResourceSearchService {
@Override
public List<ResourceVO> getResourceDetail(ResourceTypeAndIdDTO resourceTypeAndIdDTO) {
System.out.println("Search Paper");
return null;
}
}
在根目錄下META-INF/service中定義.txt檔案,檔案類名為介面類全名,內容為實現類全名
//com.euneir.spi.core.ResourceSearchService.txt
com.euneir.spi.core.impl.PaperResourceServiceImpl
com.euneir.spi.core.impl.VideoResourceServiceImpl
測試類:
public class TestCase {
public static void main(String[] args) {
ServiceLoader<ResourceSearchService> service = ServiceLoader.load(ResourceSearchService.class);
Iterator<ResourceSearchService> iterator = service.iterator();
while (iterator.hasNext()) {
ResourceSearchService searchService = iterator.next();
searchService.getResourceDetail(new ResourceTypeAndIdDTO());
}
}
}
//Search Video
//Search Paper
ServiceLoader
ServiceLoader是JDK提供的工具類,ServiceLoader.load(Class service)建立ServiceLoader例項,在classpath中尋找所有META-INF/services目錄下是否存在以介面全限定名命名的檔案,如果存在就讀取檔案內容,獲取實現類的全限定名,透過Class.forName進行類載入
載入類後,並不會立刻進行例項化,在懶載入迭代器LazyClassPathLookupIterator遍歷的時候才會反射建立每個實現類的例項。
ServiceLoader類中有內部類:private final class LazyClassPathLookupIterator:
private final class LazyClassPathLookupIterator<T>
implements Iterator<Provider<T>>{
static final String PREFIX = "META-INF/services/";
}
所以檔案要配置在META-INF/services/下。
ServiceLoader內部的是LazyClassPathLookupIterator,也就是懶載入,只有遍歷器遍歷的時候才會初始化配置檔案
迭代器中讀取META-INF/services/下配置檔案的核心程式碼:
private Class<?> nextProviderClass() {
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// The platform classloader doesn't have a class path,
// but the boot loader might. if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) { //掃描jar包下的配置檔案
if (!configs.hasMoreElements()) {
return null;
}
pending = parse(configs.nextElement());
}
String cn = pending.next();
try {
return Class.forName(cn, false, loader); //Class.forName()
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
SPI的應用
JDBC4.0之前,註冊資料庫驅動的時候,通常會使用Class.forName進行類載入,然後再獲取Connection;但是JDBC4.0之後不需要使用Class.forName載入驅動,可以直接獲取Connection,就是使用了Java的SPI擴充套件機制來實現。
- 在MySQL中的實現:META-INF/services/java.sql.Driver.txt com.mysql.cj.jdbc.Driver
- 在postgresql中的實現:META-INF/services/java.sql.Driver.txt org.postgresql.Driver
MySQL載入過程的分析:
透過DriverManager呼叫getConnection方法時,會呼叫ensureDriversInitialized()方法,該方法核心程式碼:
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();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
if (drivers != null && !drivers.isEmpty()) {
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
用於載入各個資料庫的驅動。
- 在Commons-Logging中的實現:Commons-Logging自帶了日誌實現類,但是功能比較簡單,更多的是將Commons-Logging作為門面,底層實現依賴其他框架。Commons-Logging能夠選擇使用Log4j還是Logging,但是Commons-Logging並不依賴Log4j或Logging的API
抽象類LogFactory載入具體實現的步驟如下:
- SPI服務發現機制發現org.apache.commons.logging.LogFactory的實現
- 檢視classpath根目錄下的commons-logging.properties的org.apache.commons.logging.LogFactory屬性是否指定factory實現
- 如果沒有實現,就使用factory的預設實現org.apache.commons.logging.impl.LogFactoryImpl。如果有實現就構建實現類物件。
藉助SPI就可以構建一個松耦合的日誌系統。
SPI和API的區別
從效果上來看基本相同:
但是從結構上看略有區別:
API依賴的介面位於實現者的包中,概念上更接近於實現方,組織上存在於實現者的包中,實現和介面同時存在在實現者的包中
SPI依賴的介面在呼叫方的包中,概念上更接近於呼叫方,組織上位於呼叫者的包中,實現邏輯的單獨的包中,實現可插拔。
SPI的優點
- 松耦合:無需在編譯時將實現類硬編碼在Java程式碼中
- 擴充套件性
SPI的缺點
- 不能按需載入,需要遍歷所有實現並且例項化,然後在迴圈中才能找到我們需要的實現。某些類例項化可能是非常耗時的。
- 獲取某個實現類的方式不夠靈活。只能透過Iterator的形式獲取,不能根據某個引數獲取(Spring的BeanFactory更高階)
- 多個執行緒併發時使用ServiceLoader類的例項是不安全的。
Spring的SPI機制在Java原生的SPI機制上進行了改造和擴充套件:
- 支援多個實現類:Spring的SPI允許為同一介面定義多個實現類,而Java的原生SPI機制只支援單個實現類,在應用程式中使用Spring的SPI機制更加靈活和可擴充套件
- 支援動態替換:Spring的SPI支援動態替換服務提供者,可以透過修改配置檔案或其他方式來切換服務提供者,而Java原生的SPI機制只能在啟動時載入一次服務提供者,並且無法在執行時動態替換。
- 提供更多擴充套件點:Spring的SPI提供了很多擴充套件點,例如BeanPostProcessor、BeanFactoryPostProcessor,可以在服務提供者的初始化和建立過程中進行自定義操作。
實現ServiceLoader
主要流程:
- 透過URL工具類從jar包的META-INF/services目錄下找到對應的檔案,讀取這個檔案的檔名找到對應的SPI介面。
- 讀取檔案內容,得到類全名,判斷是否和SPI介面同一型別,如果是就反射構造相應的例項物件。
- 將構造出來的例項物件新增到Providers列表