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

2021-03-02 Java基基

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

抽象類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的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重寫了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");

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

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

相關焦點

  • 面試官:為什麼 SpringBoot 的 jar 可以直接運行?
    本文來源:http://fangjian0423.github.io/2017/05/31/springboot-executable-jar/《Java工程師面試突擊(第3季)》重磅升級,由原來的└── spring └── study └── executablejar └── ExecutableJarApplication.class然後可以直接執行jar包就能啟動程序了:java -jar
  • 女面試官:「一口吃掉牛尾巴」是什麼字?高材生機智回答,直接被錄取
    1、下水道的井蓋為什麼是圓的?2、比細菌還小的是什麼東西?這是一家服裝公司正在面試顧問,當時是一位美女面試官,她看上去特別的溫和,可是對於面試她還是非常的嚴格,在面試的職員的時候,她首先是讓各位求職者自我介紹,隨後進行了筆試,經過了這兩輪的考核後,就只剩下了6位,再這6位當中,面試官再次提出了一個問題,又淘汰了3位,最後也就這3位進行了最後的面試。
  • 面試官:成語字典有幾個成語?小姑娘機智回答,被錄取
    我們下邊直接進入主題。故事的主人公是一個來自遼寧大連的小姑娘,小念。小念是一個地地道道的東北女孩,學習成績優異,所以,當初直接考上了清華大學。後來進入大學的時候,一開始小念還天天的認真讀書,但是住在一起的同學,天天帶著小念出去玩,大家也都知道,人是一個容易受帶動的高級動物,所以,後期小念的成績也是,有點讓人失望了。不過還好,畢業之後的小念仿佛突然察覺到了什麼似的。
  • 職業移民面試官刁難?律師:英語要過關
    有華人表示,美國移民官要求背出社安號,還詢問年收入金額,移民官的態度與口氣也十分不友善。對此,移民律師認為,現在刁難案件增多,意在考驗申請人的英語程度,問題也會針對申請人,當時辦理綠卡途徑的方式而有所不同,尤其對透過職業移民申請綠卡或入籍的面試為最嚴。
  • 面試官:我兒子是你兒子的爸爸,我是你的誰?美女精闢回答
    無論是在什麼公司什麼部門擔任著什麼工作職務前都要經歷最重要的一個環節,就是要和面試官談話,達到雙向標準,一個是公司方考慮著要不要應聘和面試方要不要受聘。在面試的時候總是會出現一些一個又一個讓人費勁腦汁的問題。
  • 女面試官:我沒穿衣服被你看到,會怎麼做?畢業大學生高情商回答
    職場面試奇葩題層出不窮,被面試官奇葩問題卡住過的面試者會說面試官無理取鬧,問一些跟工作沒有半毛關係的問題,但作為一名優秀的面試官,
  • 女面試官:三個金叫「鑫」,三個鬼叫什麼?小夥機智回答,被錄取
    ,面試官也算是絞盡腦汁,想盡一切辦法了。嚴嵩,一個剛從985大學畢業的應屆畢業生,憑著他985的學歷,本可以在當地找一個相對來說不錯的工作,但是年輕氣盛的他不甘心就這樣混下去。於是他選擇了一個行業裡的頂尖公司的面試。但是,當嚴嵩去應聘的時候,他看見好多人已經落選了,大家臉上都帶著一些不悅,憤憤不平,以及些許的不甘心。
  • 女面試官:一瓶水,怎樣能先喝到底部的水?美女聰明回答!
    因為是在學校表現很好,學習也是很優秀,所以她不擔心自己的工作問題,當然幸運女神也眷戀著她,昨天收到一家大型公司的面試邀請。去到後,面試官問了一個這樣的問題,讓幾個人都是有點的懵了。女面試官:一瓶水,怎樣能先喝到底部的水?
  • 面試官:你可以用純 CSS 判斷滑鼠進入的方向嗎?
    •作者:陳大魚頭•github: KRISACHAN[1]前言在之前某一個前端技術群裡,有一個群友說他面試的時候遇到了一個問題,就是面試官讓他用純 CSS 來實現一個根據滑鼠移動位置覺得物體移動方向的 DEMO。
  • 面試官:請問,鉛筆姓什麼?回「鉛」的都淘汰,小姑娘巧妙作答!
    在面試的時候,面試官總會問一些奇葩的問題,有些問題看似很簡單,但其實很容易出錯,因為求職者們覺得這道題很簡單,便不夠細心,往往就忽略了很多細節,
  • 女面試官:「饕餮」這兩個字怎麼念?美女機智回答,被錄取
    閱讀本文前,請您先點擊上面的藍色字體,再點擊「關注」,這樣您就可以繼續免費收到文章了。
  • 面試官: 李字去掉子, 是什麼字? 答「木」被落選, 男孩機智被錄取
    如今我們去面試時,會碰到各類與眾不同,奇奇怪怪的題目,不少的公司不像以前面試時只問一些專業性問題,現在還會增加奇怪的題目。
  • 美女面試官:世界上筆畫最多的字是什麼?研究生回復「龘」被淘汰
    張智霖也是和收到一家大型公司的面試邀請,經過一番準備,張智霖也是去到了這家公司。去到後,面試官同時面試了好幾個面試者,問了一個這樣的問題,讓幾個人都是有點不知道該怎麼回答。美女面試官問道:「世界上筆畫最多的字是什麼?」
  • 【福利】實拍AV女優面試官的一天,害羞...
    小堀芳一是AV女優面試官,有三十年的工作經驗,看過上萬個想拍AV的裸女胴體。如果沒有他,就不會有我們耳熟能詳的小澤圓和松島楓。
  • 面試官:什麼字是全世界通用的?博士答英文,卻當場被淘汰
    很快,他就收到了面試邀約。他準備了一晚上,就去面試地點了。面試官在簡單地了解了一些面試者的基礎資料後,問了一個奇怪的問題:什麼字是全世界通用的?這麼嚴肅的場所,面試官怎麼會問這樣的問題呢?但是很快,大家神色就恢復自然了。郭富城思量了一番,就先舉手回答了:"現在全世界通用的字當然就是英文了。雖然據調查,使用英文的人其實還是沒有中文多,但是英文依舊是全世界通用的語言。"郭富城覺得自己回答的已經足夠完善了,但是面試官給他的表情卻讓他覺得他沒戲了。接下來,回答問題的是一個大學畢業的小夥子。
  • 綠卡面試 問題刁鑽令人驚訝!上學吃飯都得招
    華人鄭小姐之前因沒抽到H1-B工作簽證,在艾爾蒙地找了一個學校掛靠,結果學校被抄,她在30天轉學找到新學校,以為此事已過去好幾年,認為和綠卡面試無關,就沒做準備,沒想到綠卡面試時移民官問的問題讓她傻眼。她指出,在長達一個小時的面試裡,移民官詢問的問題都圍繞著她唸書的狀況,且非常仔細,比如「行政辦公室在大樓的那一邊?」、「什麼時候上課?」
  • 暗訪日本Aレ女尤面試官:只能看,不許摸
    「可以把胸部再夾緊一點嗎?」小堀問。「是這樣嗎?」小城交叉雙手。小堀乾巴巴的指頭按下快門,傻瓜相機咔嚓一聲,又一位女士全裸入鏡。儀式完畢,記者問:「你這職業,太招人妒忌了吧?」「只能看,不許碰,有什麼好?如果可以碰,或許不錯。」
  • 面試官:猴子摘桃,打一成語?女大學生回答不出被淘汰
    ,往往面試官出的一些題,讓他們大多都感到很奇葩,很多時候都回答不上來,而最近一位女大學生小美參加了一家公司的面試,結果,面試官出了一道題:猴子摘桃,打一成語?  這時,走來一位面試官,直接領著三位來到了公司的會議室,看了三位的個人簡歷以後,於是,出了幾道理論知識,剛好女大學生小美就是理科專業畢業的,所以,很快就把問題給回答出來了,面試官看三位回答的都很好。
  • Google面試官:不給我留提問時間,怎麼給你 hire?
    據此也可以看出狗家對算法的重視,現在算法面試題做出來是遠遠不夠的,對做題時間的把控和其他方面的考察同樣重要。如果面試表現不好,FB一般不會直接掛你,很多情況是給你加面機會或者down level。FB 的Coding環節,面試官一般會準備兩道題。這時候如果你只是完美地做出一道題,基本上這輪就跪了。相反,你快速解決兩道題,即便有些小瑕疵,說不定也能過。
  • 女面試官:我兒子叫你兒子爸爸,那我是你的誰?小夥神回復被錄取
    如今面試已經不像以前那樣,只問一些簡單的基礎知識來考察求職者的能力,面試官還會出一些看上去「沒頭腦」,實則考驗求職者應變能力的問題。