面試官:為什麼 SpringBoot 的 jar 可以直接運行?

2021-02-20 石杉的架構筆記

公眾號後臺回復「面試」,獲取精品學習資料

掃描下方海報了解專欄詳情

本文來源:

http://fangjian0423.github.io/2017/05/31/springboot-executable-jar/

《Java工程師面試突擊(第3季)》重磅升級,由原來的70講增至160講,內容擴充一倍多,升級部分內容請參見文末


SpringBoot提供了一個插件spring-boot-maven-plugin用於把程序打包成一個可執行的jar包。在pom文件裡加入這個插件即可:

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

打包完生成的executable-jar-1.0-SNAPSHOT.jar內部的結構如下:

├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── spring.study
│ └── executable-jar
│ ├── pom.properties
│ └── pom.xml
├── lib
│ ├── aopalliance-1.0.jar
│ ├── classmate-1.1.0.jar
│ ├── spring-boot-1.3.5.RELEASE.jar
│ ├── spring-boot-autoconfigure-1.3.5.RELEASE.jar
│ ├── ...
├── org
│ └── springframework
│ └── boot
│ └── loader
│ ├── ExecutableArchiveLauncher$1.class
│ ├── ...
└── spring
└── study
└── executablejar
└── ExecutableJarApplication.class

然後可以直接執行jar包就能啟動程序了:

java -jar executable-jar-1.0-SNAPSHOT.jar

打包出來fat jar內部有4種文件類型:

META-INF文件夾:程序入口,其中MANIFEST.MF用於描述jar包的信息lib目錄:放置第三方依賴的jar包,比如springboot的一些jar包

MANIFEST.MF文件的內容:

Manifest-Version: 1.0
Implementation-Title: executable-jar
Implementation-Version: 1.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: Format
Start-Class: spring.study.executablejar.ExecutableJarApplication
Implementation-Vendor-Id: spring.study
Spring-Boot-Version: 1.3.5.RELEASE
Created-By: Apache Maven 3.2.3
Build-Jdk: 1.8.0_20
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher

我們看到,它的Main-Class是org.springframework.boot.loader.JarLauncher,當我們使用java -jar執行jar包的時候會調用JarLauncher的main方法,而不是我們編寫的SpringApplication。

那麼JarLauncher這個類是的作用是什麼的?

它是SpringBoot內部提供的工具Spring Boot Loader提供的一個用於執行Application類的工具類(fat jar內部有spring loader相關的代碼就是因為這裡用到了)。相當於Spring Boot Loader提供了一套標準用於執行SpringBoot打包出來的jar

Spring Boot Loader抽象的一些類

抽象類Launcher:各種Launcher的基礎抽象類,用於啟動應用程式;跟Archive配合使用;目前有3種實現,分別是JarLauncher、WarLauncher以及PropertiesLauncher

Archive:歸檔文件的基礎抽象類。JarFileArchive就是jar包文件的抽象。它提供了一些方法比如getUrl會返回這個Archive對應的URL;getManifest方法會獲得Manifest數據等。ExplodedArchive是文件目錄的抽象

JarFile:對jar包的封裝,每個JarFileArchive都會對應一個JarFile。JarFile被構造的時候會解析內部結構,去獲取jar包裡的各個文件或文件夾,這些文件或文件夾會被封裝到Entry中,也存儲在JarFileArchive中。如果Entry是個jar,會解析成JarFileArchive。

比如一個JarFileArchive對應的URL為:

jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/

它對應的JarFile為:

/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar

這個JarFile有很多Entry,比如:

META-INF/
META-INF/MANIFEST.MF
spring/
spring/study/
....
spring/study/executablejar/ExecutableJarApplication.class
lib/spring-boot-starter-1.3.5.RELEASE.jar
lib/spring-boot-1.3.5.RELEASE.jar
...

JarFileArchive內部的一些依賴jar對應的URL(SpringBoot使用org.springframework.boot.loader.jar.Handler處理器來處理這些URL):

jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/

jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

我們看到如果有jar包中包含jar,或者jar包中包含jar包裡面的class文件,那麼會使 用 !/ 分隔開,這種方式只有org.springframework.boot.loader.jar.Handler能處 理,它是SpringBoot內部擴展出來的一種URL協議。

JarLauncher的執行過程

JarLauncher的main方法:

public static void main(String[] args) {
    // 構造JarLauncher,然後調用它的launch方法。參數是控制臺傳遞的
    new JarLauncher().launch(args);
}  

JarLauncher被構造的時候會調用父類ExecutableArchiveLauncher的構造方法。

ExecutableArchiveLauncher的構造方法內部會去構造Archive,這裡構造了JarFileArchive。構造JarFileArchive的過程中還會構造很多東西,比如JarFile,Entry …

JarLauncher的launch方法:
protected void launch(String[] args) {
  try {
    // 在系統屬性中設置註冊了自定義的URL處理器:org.springframework.boot.loader.jar.Handler。如果URL中沒有指定處理器,會去系統屬性中查詢
    JarFile.registerUrlProtocolHandler();
    // getClassPathArchives方法在會去找lib目錄下對應的第三方依賴JarFileArchive,同時也會項目自身的JarFileArchive
    // 根據getClassPathArchives得到的JarFileArchive集合去創建類加載器ClassLoader。這裡會構造一個LaunchedURLClassLoader類加載器,這個類加載器繼承URLClassLoader,並使用這些JarFileArchive集合的URL構造成URLClassPath
    // LaunchedURLClassLoader類加載器的父類加載器是當前執行類JarLauncher的類加載器
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // getMainClass方法會去項目自身的Archive中的Manifest中找出key為Start-Class的類
    // 調用重載方法launch
    launch(args, getMainClass(), classLoader);
  }
  catch (Exception ex) {
    ex.printStackTrace();
    System.exit(1);
  }
}

// Archive的getMainClass方法
// 這裡會找出spring.study.executablejar.ExecutableJarApplication這個類
public String getMainClass() throws Exception {
  Manifest manifest = getManifest();
  String mainClass = null;
  if (manifest != null) {
    mainClass = manifest.getMainAttributes().getValue("Start-Class");
  }
  if (mainClass == null) {
    throw new IllegalStateException(
        "No 'Start-Class' manifest entry specified in " + this);
  }
  return mainClass;
}

// launch重載方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
    throws Exception {
      // 創建一個MainMethodRunner,並把args和Start-Class傳遞給它
  Runnable runner = createMainMethodRunner(mainClass, args, classLoader);
      // 構造新線程
  Thread runnerThread = new Thread(runner);
      // 線程設置類加載器以及名字,然後啟動
  runnerThread.setContextClassLoader(classLoader);
  runnerThread.setName(Thread.currentThread().getName());
  runnerThread.start();
}

MainMethodRunner的run方法:

@Override
public void run() {
  try {
    // 根據Start-Class進行實例化
    Class<?> mainClass = Thread.currentThread().getContextClassLoader()
        .loadClass(this.mainClassName);
    // 找出main方法
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    // 如果main方法不存在,拋出異常
    if (mainMethod == null) {
      throw new IllegalStateException(
          this.mainClassName + " does not have a main method");
    }
    // 調用
    mainMethod.invoke(null, new Object[] { this.args });
  }
  catch (Exception ex) {
    UncaughtExceptionHandler handler = Thread.currentThread()
        .getUncaughtExceptionHandler();
    if (handler != null) {
      handler.uncaughtException(Thread.currentThread(), ex);
    }
    throw new RuntimeException(ex);
  }
}

Start-Class的main方法調用之後,內部會構造Spring容器,啟動內置Servlet容器等過程。這些過程我們都已經分析過了。

關於自定義的類加載器LaunchedURLClassLoader

LaunchedURLClassLoader重寫了loadClass方法,也就是說它修改了默認的類加載方式(先看該類是否已加載這部分不變,後面真正去加載類的規則改變了,不再是直接從父類加載器中去加載)。LaunchedURLClassLoader定義了自己的類加載規則:

private Class<?> doLoadClass(String name) throws ClassNotFoundException {

  // 1) Try the root class loader
  try {
    if (this.rootClassLoader != null) {
      return this.rootClassLoader.loadClass(name);
    }
  }
  catch (Exception ex) {
    // Ignore and continue
  }

  // 2) Try to find locally
  try {
    findPackage(name);
    Class<?> cls = findClass(name);
    return cls;
  }
  catch (Exception ex) {
    // Ignore and continue
  }

  // 3) Use standard loading
  return super.loadClass(name, false);
}

加載規則:

如果根類加載器存在,調用它的加載方法。這裡是根類加載是ExtClassLoader調用LaunchedURLClassLoader自身的findClass方法,也就是URLClassLoader的findClass方法調用父類的loadClass方法,也就是執行默認的類加載順序(從BootstrapClassLoader開始從下往下尋找)

LaunchedURLClassLoader自身的findClass方法:

protected Class<?> findClass(final String name)
     throws ClassNotFoundException
{
    try {
        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    // 把類名解析成路徑並加上.class後綴
                    String path = name.replace('.', '/').concat(".class");
                    // 基於之前得到的第三方jar包依賴以及自己的jar包得到URL數組,進行遍歷找出對應類名的資源
                    // 比如path是org/springframework/boot/loader/JarLauncher.class,它在jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/中被找出
                    // 那麼找出的資源對應的URL為jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class
                    Resource res = ucp.getResource(path, false);
                    if (res != null) { // 找到了資源
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else { // 找不到資源的話直接拋出ClassNotFoundException異常
                        throw new ClassNotFoundException(name);
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
}

下面是LaunchedURLClassLoader的一個測試:

// 註冊org.springframework.boot.loader.jar.Handler URL協議處理器
JarFile.registerUrlProtocolHandler();
// 構造LaunchedURLClassLoader類加載器,這裡使用了2個URL,分別對應jar包中依賴包spring-boot-loader和spring-boot,使用 "!/" 分開,需要org.springframework.boot.loader.jar.Handler處理器處理
LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
        new URL[] {
                new URL("jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/")
                , new URL("jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-1.3.5.RELEASE.jar!/")
        },
        LaunchedURLClassLoaderTest.class.getClassLoader());

// 加載類
// 這2個類都會在第二步本地查找中被找出(URLClassLoader的findClass方法)
classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
classLoader.loadClass("org.springframework.boot.SpringApplication");
// 在第三步使用默認的加載順序在ApplicationClassLoader中被找出
classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");

Spring Boot Loader的作用

SpringBoot在可執行jar包中定義了自己的一套規則,比如第三方依賴jar包在/lib目錄下,jar包的URL路徑使用自定義的規則並且這個規則需要使用org.springframework.boot.loader.jar.Handler處理器處理。它的Main-Class使用JarLauncher,如果是war包,使用WarLauncher執行。這些Launcher內部都會另起一個線程啟動自定義的SpringApplication類。

這些特性通過spring-boot-maven-plugin插件打包完成。

END

《Java工程師面試突擊第三季》加餐部分大綱:(註:1-66講的大綱請掃描文末二維碼,在課程詳情頁獲取)

詳細的課程內容,大家可以掃描下方二維碼了解:

相關焦點

  • 面試官問:為什麼SpringBoot的 jar 可以直接運行?
    └── spring └── study └── executablejar └── ExecutableJarApplication.class然後可以直接執行jar包就能啟動程序了:java
  • 女面試官:「一口吃掉牛尾巴」是什麼字?高材生機智回答,直接被錄取
    1、下水道的井蓋為什麼是圓的?2、比細菌還小的是什麼東西?這是一家服裝公司正在面試顧問,當時是一位美女面試官,她看上去特別的溫和,可是對於面試她還是非常的嚴格,在面試的職員的時候,她首先是讓各位求職者自我介紹,隨後進行了筆試,經過了這兩輪的考核後,就只剩下了6位,再這6位當中,面試官再次提出了一個問題,又淘汰了3位,最後也就這3位進行了最後的面試。
  • 面試官:成語字典有幾個成語?小姑娘機智回答,被錄取
    我們下邊直接進入主題。故事的主人公是一個來自遼寧大連的小姑娘,小念。小念是一個地地道道的東北女孩,學習成績優異,所以,當初直接考上了清華大學。後來進入大學的時候,一開始小念還天天的認真讀書,但是住在一起的同學,天天帶著小念出去玩,大家也都知道,人是一個容易受帶動的高級動物,所以,後期小念的成績也是,有點讓人失望了。不過還好,畢業之後的小念仿佛突然察覺到了什麼似的。
  • 職業移民面試官刁難?律師:英語要過關
    有華人表示,美國移民官要求背出社安號,還詢問年收入金額,移民官的態度與口氣也十分不友善。對此,移民律師認為,現在刁難案件增多,意在考驗申請人的英語程度,問題也會針對申請人,當時辦理綠卡途徑的方式而有所不同,尤其對透過職業移民申請綠卡或入籍的面試為最嚴。
  • 面試官:我兒子是你兒子的爸爸,我是你的誰?美女精闢回答
    無論是在什麼公司什麼部門擔任著什麼工作職務前都要經歷最重要的一個環節,就是要和面試官談話,達到雙向標準,一個是公司方考慮著要不要應聘和面試方要不要受聘。在面試的時候總是會出現一些一個又一個讓人費勁腦汁的問題。
  • 面試官:猴子摘桃,打一成語?女大學生回答不出被淘汰
    ,往往面試官出的一些題,讓他們大多都感到很奇葩,很多時候都回答不上來,而最近一位女大學生小美參加了一家公司的面試,結果,面試官出了一道題:猴子摘桃,打一成語?  這時,走來一位面試官,直接領著三位來到了公司的會議室,看了三位的個人簡歷以後,於是,出了幾道理論知識,剛好女大學生小美就是理科專業畢業的,所以,很快就把問題給回答出來了,面試官看三位回答的都很好。
  • Google面試官:不給我留提問時間,怎麼給你 hire?
    據此也可以看出狗家對算法的重視,現在算法面試題做出來是遠遠不夠的,對做題時間的把控和其他方面的考察同樣重要。如果面試表現不好,FB一般不會直接掛你,很多情況是給你加面機會或者down level。FB 的Coding環節,面試官一般會準備兩道題。這時候如果你只是完美地做出一道題,基本上這輪就跪了。相反,你快速解決兩道題,即便有些小瑕疵,說不定也能過。
  • 面試官:你可以用純 CSS 判斷滑鼠進入的方向嗎?
    •作者:陳大魚頭•github: KRISACHAN[1]前言在之前某一個前端技術群裡,有一個群友說他面試的時候遇到了一個問題,就是面試官讓他用純 CSS 來實現一個根據滑鼠移動位置覺得物體移動方向的 DEMO。
  • 女面試官:我沒穿衣服被你看到,會怎麼做?畢業大學生高情商回答
    職場面試奇葩題層出不窮,被面試官奇葩問題卡住過的面試者會說面試官無理取鬧,問一些跟工作沒有半毛關係的問題,但作為一名優秀的面試官,
  • 女面試官:「饕餮」這兩個字怎麼念?美女機智回答,被錄取
    閱讀本文前,請您先點擊上面的藍色字體,再點擊「關注」,這樣您就可以繼續免費收到文章了。
  • 女面試官:三個金叫「鑫」,三個鬼叫什麼?小夥機智回答,被錄取
    ,面試官也算是絞盡腦汁,想盡一切辦法了。嚴嵩,一個剛從985大學畢業的應屆畢業生,憑著他985的學歷,本可以在當地找一個相對來說不錯的工作,但是年輕氣盛的他不甘心就這樣混下去。於是他選擇了一個行業裡的頂尖公司的面試。但是,當嚴嵩去應聘的時候,他看見好多人已經落選了,大家臉上都帶著一些不悅,憤憤不平,以及些許的不甘心。
  • 美女面試官:世界上筆畫最多的字是什麼?研究生回復「龘」被淘汰
    張智霖也是和收到一家大型公司的面試邀請,經過一番準備,張智霖也是去到了這家公司。去到後,面試官同時面試了好幾個面試者,問了一個這樣的問題,讓幾個人都是有點不知道該怎麼回答。美女面試官問道:「世界上筆畫最多的字是什麼?」
  • 面試官:請問,鉛筆姓什麼?回「鉛」的都錯了,女大學生機智回答
    許多面試官的問題也是一樣,一道道智力題迎頭痛擊,讓許多求職者做好的準備付諸流水。這些題真的有那麼難嗎?也許我們把自己放輕鬆,試試跳脫常規思維,也許問題並沒有我們想像那麼複雜。下面跟隨我們一起去看看他們的面試現場!朋友小玲最近剛大學畢業,在學校的時候,小玲是一個各方面都非優異的學生,而且經常抽出空閒時間做一些兼職工作,所以工作經歷還算豐富。
  • 效果拔群:日本網友視頻面試緊張,竟把面試官「魔改」成遊戲攻略對象!
    同樣的如果要找工作,大多數情況下也是靠視頻面試的方式。視頻面試就是跟對方公司約好時間,打開電腦跟通訊軟體,開始面試、敘述學習經歷,了解對方公司跟工作情況等等。對於應聘者來說,視頻面試是一個全面展示自己的好機會。除了過人的能力以外,種種細節也都是視頻面試取勝的關鍵。
  • 女面試官:一瓶水,怎樣能先喝到底部的水?美女聰明回答!
    當代社會競爭激烈,就業問題是當今社會永恆不變的熱點,企業和人才都是雙向選擇,面試就顯得尤為重要,可不能出現:酒香也怕巷子深的問題,所以短短幾十分鐘的面試
  • 面試官:請問,鉛筆姓什麼?回「鉛」的都淘汰,小姑娘巧妙作答!
    中了面試官的套路。很多面試官喜歡一些心思細膩,思維活躍的求職者們。對於這些公司來說,學歷,專業知識只是他們的門檻而已,頭腦靈活,遇事能夠應變自如的人,才是公司們想要的人才,要知道專業知識是可以培養的,然而思維邏輯方式,臨場應變能力這些可是無法培養的!餘力今年25歲,是一名典型的「90後」。從工作了一年的原公司離職後,他參加了某公司的面試,應聘銷售助理崗位。
  • 【福利】實拍AV女優面試官的一天,害羞...
    小堀芳一是AV女優面試官,有三十年的工作經驗,看過上萬個想拍AV的裸女胴體。如果沒有他,就不會有我們耳熟能詳的小澤圓和松島楓。
  • 面試官不專業? 將公民證書國籍「臺灣」改「中國」?
    近日華府地區有華裔民眾表示,在公民面試結束確認「入籍美國公民證書」(Certificate of U.S. Naturalization)資料時,國籍「臺灣」(Taiwan)被面試官改為「中國」(China),且面試官堅持稱,公民申請國籍沒有「臺灣」的選項,引起不少華裔的困惑和不滿,並批評面試官不專業。
  • 面試官: 李字去掉子, 是什麼字? 答「木」被落選, 男孩機智被錄取
    主要也是為了讓公司挑選到更好更合適的人才,只要你了解到面試官們想要什麼答案,那就能勝人一步。我就有這麼一位朋友阿傑,他在面試時被問到了這樣一個問題:李字去掉子,是什麼字?結果和他一起面試回答「木」的其他人全部遭到了淘汰,這也讓他反思面試官為啥要問這個問題,最後回答讓他通過了錄取。
  • 面試官:什麼字是全世界通用的?博士答英文,卻當場被淘汰
    很快,他就收到了面試邀約。他準備了一晚上,就去面試地點了。面試官在簡單地了解了一些面試者的基礎資料後,問了一個奇怪的問題:什麼字是全世界通用的?這麼嚴肅的場所,面試官怎麼會問這樣的問題呢?但是很快,大家神色就恢復自然了。郭富城思量了一番,就先舉手回答了:"現在全世界通用的字當然就是英文了。雖然據調查,使用英文的人其實還是沒有中文多,但是英文依舊是全世界通用的語言。"郭富城覺得自己回答的已經足夠完善了,但是面試官給他的表情卻讓他覺得他沒戲了。接下來,回答問題的是一個大學畢業的小夥子。