jar包天天见,可是你知道它的运行机制吗
轉載自??jar包天天見,可是你知道它的運行機制嗎
今天介紹兩個大家每天都在用但是卻很少去了解它的知識點:spi 和 jar 運行機制,廢話不多說,開始正題。
spi
spi 是 Java 提供的一套用來被第三方實現或者擴展的 API ,它可以用來啟用框架擴展和替換組件。spi 機制是這樣的:讀取 META-INF/services/ 目錄下的元信息,然后 ServiceLoader 根據信息加載對應的類,你可以在自己的代碼中使用這個被加載的類。要使用 Java SPI,需要遵循如下約定:
-
當服務提供者提供了接口的一種具體實現后,在 jar 包的 META-INF/services 目錄下創建一個以 “接口全限定名” 命名的文件,內容為實現類的全限定名;
-
接口實現類所在的 jar 包放在主程序的 classpath 中;
-
主程序通過 java.util.ServiceLoder 動態裝載實現模塊,它通過掃描 META-INF/services 目錄下的配置文件找到實現類的全限定名,把類加載到 JVM ;
-
SPI 的實現類必須攜帶一個不帶參數的構造方法;
現在我們來簡單的使用一下吧。
?
spi 使用示例
建一個 maven 項目,定義一個接口 ( com.test.SpiTest ),并實現該接口( com.test.SpiTestImpl);然后在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的文件 ( com.test.SpiTest),內容是要應用的實現類( com.test.SpiTestImpl)。
public interface SpiTest {void test();}public class SpiTestImpl implements SpiTest {@Overridepublic void test() {System.out.println("test");}}然后在我們的應用程序中使用 ServiceLoader來加載配置文件中指定的實現。
public static void main(String[] args) {ServiceLoader<SpiTest> load = ServiceLoader.load(SpiTest.class);SpiTest next = load.iterator().next();next.test();}這便是 spi 的使用方式了,簡約而不簡單。
?
spi 技術的應用
那這一項技術有哪些方面的應用呢?最直接的 jdbc 中我們需要指定數據庫驅動的全限定名,這便是 spi 技術。還有不少框架比如 dubbo ,都會預留 spi 擴展點比如:dubbo spi
為什么要這么做呢?在 Spring 框架中我們注入一個 bean 很容易,通過注解或者 xml 配置即可,然后在其他的地方就能使用這個 bean 。在非 Spring 框架下,我們想要有同樣的效果就可以考慮 spi 技術了。
寫過 SpringBoot 的 starter 的都知道,需要在 src/main/resources/ 下建立 /META-INF/spring.factories 文件。這其實也是一種spi技術的變形。
?
jar 機制
通常項目中我們打 jar 包都是通過 maven 來進行的,導致很多人忽略了這個東西的存在,就像很多人不知道 jdb.exe 是啥玩意一樣。下面我們不借助任何工具來打一個 jar 包并對 jar 文件結構進行解析。
命令行打 jar 包
首先我們建立一個普通的 java 項目,新建幾個 class 類,然后在根目錄下新建 META-INF/MAINFEST.MF這個文件包含了 jar 的元信息,當我們執行 java -jar 的時候首先會讀取該文件的信息做相關的處理。我們來看看這個文件中可以配置哪些信息 :
-
Manifest-Version:用來定義 manifest 文件的版本,例如:Manifest-Version: 1.0
-
Main-Class:定義 jar 文件的入口類,該類必須是一個可執行的類,一旦定義了該屬性即可通過 java -jar x.jar 來運行該 jar 文件。
-
Class-Path:指定該 jar 包所依賴的外部 jar 包,以當前 jar 包所在的位置為相對路徑,無法指定 jar 包內部的 jar 包
-
簽名相關屬性,包括?Name,?Digest-Algorithms,?SHA-Digest?等
定義好元信息之后我們就可以打 jar 包了,以下是打包的一些常用命令
-
默認打包
生成的test.jar中就含test目錄和jar自動生成的META-INF目錄(內含MAINFEST.MF清單文件)
jar -cvf test.jar test
-
查看包內容
jar -tvf test.jar
-
解壓jar包
jar -xvf test.jar
-
提取jar包部分內容
jar -xvf test.jar test\test.class
-
追加內容到jar包
追加 MAINFEST.MF 清單文件以外的文件,會追加整個目錄結構
jar -uvf test.jar other\ss.class
-
追加清單文件
會追加整個目錄結構( test.jar 會包含 META-INF 目錄)
jar -uMvf test.jar META-INF\MAINFEST.MF
-
創建自定義MAINFEST.MF的jar包
jar -cMvf test.jar test META-INF
通過 -m 選項配置自定義 MAINFEST.MF 文件時,自定義MAINFEST.MF 文件必須在位于工作目錄下才可以
jar -cmvf MAINFEST.MF test.jar test
jar 運行的過程
jar 運行過程和類加載機制有關,而類加載機制又和我們自定義的類加載器有關,現在我們先來了解一下雙親委派模式。
java 中類加載器分為三個:
-
BootstrapClassLoader 負責加載?${JAVA_HOME}/jre/lib?部分 jar 包
-
ExtClassLoader 加載?${JAVA_HOME}/jre/lib/ext?下面的 jar 包
-
AppClassLoader 加載用戶自定義 -classpath 或者 Jar 包的 Class-Path 定義的第三方包
類的生命周期為:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個階段。
當我們執行 java -jar 的時候 jar 文件以二進制流的形式被讀取到內存,但不會加載到 jvm 中,類會在一個合適的時機加載到虛擬機中。類加載的時機:
-
遇到 new、getstatic、putstatic 或 invokestatic 這四條字節碼指令時,如果類沒有進行過初始化,則需要先對其進行初始化。這四條指令的最常見的 Java 代碼場景是使用 new 關鍵字實例化對象的時候,讀取或設置一個類的靜態字段調用一個類的靜態方法的時候。
-
使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
-
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
-
當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類。
當觸發類加載的時候,類加載器也不是直接加載這個類。首先交給 AppClassLoader ,它會查看自己有沒有加載過這個類,如果有直接拿出來,無須再次加載,如果沒有就將加載任務傳遞給 ExtClassLoader ,而 ExtClassLoader 也會先檢查自己有沒有加載過,沒有又會將任務傳遞給 BootstrapClassLoader ,最后 BootstrapClassLoader 會檢查自己有沒有加載過這個類,如果沒有就會去自己要尋找的區域去尋找這個類,如果找不到又將任務傳遞給 ExtClassLoader ,以此類推最后才是 AppClassLoader 加載我們的類。這樣做是確保類只會被加載一次。通常我們的類加載器只識別 classpath (這里的 classpath 指項目根路徑,也就是 jar 包內的位置)下 .class 文件。jar 中其他的文件包括 jar 包被當做了資源文件,而不會去讀取里面的 .class 文件。但實際上我們可以通過自定義類加載器來實現一些特別的操作
?
Tomcat 的類加載器
Tomcat 的類加載機制是違反了雙親委托原則的,對于一些未加載的非基礎類(Object,String等),各個 web 應用自己的類加載器(WebAppClassLoader) 會優先加載,加載不到時再交給 commonClassLoader 走雙親委托。
tomcat 的類加載器:
-
Common 類加載器:負責加載?/common?目錄的類庫,這兒存放的類庫可被 tomcat 以及所有的應用使用。
-
Catalina 類加載器:負責加載?/server?目錄的類庫,只能被?tomcat?使用。
-
Shared?類加載器:負載加載?/shared?目錄的類庫,可被所有的?web?應用使用,但?tomcat?不可使用。
-
WebApp?類加載器:負載加載單個?Web?應用下?classes?目錄以及?lib?目錄的類庫,只能當前應用使用。
-
Jsp?類加載器:負責加載?Jsp?,每一個?Jsp?文件都對應一個?Jsp?加載器。
我們將一堆 jar 包放到 tomcat 的項目文件夾下, tomcat 運行的時候能加載到這些 jar 包的 class 就是因為這些類加載器對讀取到的二進制數據進行處理解析從中拿到了需要的類
SpringBoot 的 jar 包
當我們將一個 SpringBoot 項目打好包之后,不妨解壓看看里面的結構是什么樣子的的
run.jar
|——org
| |——springframework
| |——boot
| |——loader
| |——JarLauncher.class
| |——Launcher.class
|——META-INF
| |——MANIFEST.MF
|——BOOT-INF
| |——class
| |——Main.class
| |——Begin.class
| |——lib
| |——commons.jar
| |——plugin.jar
| |——resource
| |——a.jpg
?
| |——b.jpg
classpath 可加載的類只有 JarLauncher.class, Launcher.class, Main.class, Begin.class。在 BOOT-INF/lib 和 BOOT-INF/class 里面的文件不屬于 classloader 搜素對象直接訪問的話會報 NoClassDefDoundErr 異常。Jar 包里面的資源以 Stream 的形式存在(他們本就處于 Jar 包之中),java 程序時可以訪問到的。當 springboot 運行 main 方法時在 main 中會運行 org.springframework.boot.loader.JarLauncher 和 Launcher.class 這兩個個加載器(你是否還及得前文提到過得 spi 技術),這個加載器去加載受 stream 中的 jar 包中的 class。這樣就實現了加載 jar 包中的 jar 這個功能否則正常的類加載器是無法加載 jar 包中的 jar 的 class 的,只會根據 MAINFEST.MF 來加載 jar 外部的 jar 來讀取里面的 class。
如何自定義類加載器
public class MyClassLoader extends ClassLoader{private String classpath;public MyClassLoader(String classpath) {this.classpath = classpath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 該方法是根據一個name加載一個類,我們可以使用一個流來讀取path中的文件然后從文件中解析出class來}} 調用 defineClass() 方法加載類 public static void main(String []args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException{//自定義類加載器的加載路徑MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");//包名+類名Class c=myClassLoader.loadClass("com.test.Test")if(c!=null){// 做點啥}}?
總結
本文從比較基礎的層面解讀了我們頻繁使用卻大部分人不是很了解的兩個知識點—— spi 和 jar 機制。希望大家看完這篇文章后能對 SpringBoot 中的一些“黑魔法”有更深入的了解,而不是停留在表面。
總結
以上是生活随笔為你收集整理的jar包天天见,可是你知道它的运行机制吗的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 张少华主演的电视剧 张少华演过哪些电视剧
- 下一篇: Google 出的 Guava 是个什么