spring boot jar的啟動(dòng)原理解析
1.前言
近來(lái)有空對(duì)公司的open api平臺(tái)進(jìn)行了些優(yōu)化,然后在打出jar包的時(shí)候,突然想到以前都是對(duì)spring boot使用很熟練,但是從來(lái)都不知道spring boot打出的jar的啟動(dòng)原理,然后這回將jar解開(kāi)了看了下,與想象中確實(shí)大不一樣,以下就是對(duì)解壓出來(lái)的jar的完整分析。
2.jar的結(jié)構(gòu)
spring boot的應(yīng)用程序就不貼出來(lái)了,一個(gè)較簡(jiǎn)單的demo打出的結(jié)構(gòu)都是類(lèi)似,另外我采用的spring boot的版本為1.4.1.RELEASE網(wǎng)上有另外一篇文章對(duì)spring boot jar啟動(dòng)的分析,那個(gè)應(yīng)該是1.4以下的,啟動(dòng)方式與當(dāng)前版本也有著許多的不同。
在mvn clean install后,我們?cè)诓榭磘arget目錄中時(shí),會(huì)發(fā)現(xiàn)兩個(gè)jar包,如下:
xxxx.jar xxx.jar.original
這個(gè)則是歸功于spring boot插件的機(jī)制,將一個(gè)普通的jar打成了一個(gè)可以執(zhí)行的jar包,而xxx.jar.original則是maven打出的jar包,這些可以參考spring官網(wǎng)的文章來(lái)了解,如下:
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#executable-jar
以下是spring boot應(yīng)用打出的jar的部分目錄結(jié)構(gòu),大部分省略了,僅僅展示出其中重要的部分。
. ├── BOOT-INF │ ├── classes │ │ ├── application-dev.properties │ │ ├── application-prod.properties │ │ ├── application.properties │ │ ├── com │ │ │ └── weibangong │ │ │ └── open │ │ │ └── openapi │ │ │ ├── SpringBootWebApplication.class │ │ │ ├── config │ │ │ │ ├── ProxyServletConfiguration.class │ │ │ │ └── SwaggerConfig.class │ │ │ ├── oauth2 │ │ │ │ ├── controller │ │ │ │ │ ├── AccessTokenController.class │ │ ├── logback-spring.xml │ │ └── static │ │ ├── css │ │ │ └── guru.css │ │ ├── images │ │ │ ├── FBcover1200x628.png │ │ │ └── NewBannerBOOTS_2.png │ └── lib │ ├── accessors-smart-1.1.jar ├── META-INF │ ├── MANIFEST.MF │ └── maven │ └── com.weibangong.open │ └── open-server-openapi │ ├── pom.properties │ └── pom.xml └── org └── springframework └── boot └── loader ├── ExecutableArchiveLauncher$1.class ├── ExecutableArchiveLauncher.class ├── JarLauncher.class ├── LaunchedURLClassLoader$1.class ├── LaunchedURLClassLoader.class ├── Launcher.class ├── archive │ ├── Archive$Entry.class │ ├── Archive$EntryFilter.class │ ├── Archive.class │ ├── ExplodedArchive$1.class │ ├── ExplodedArchive$FileEntry.class │ ├── ExplodedArchive$FileEntryIterator$EntryComparator.class ├── ExplodedArchive$FileEntryIterator.class
這個(gè)jar除了我們寫(xiě)的應(yīng)用程序打出的class以外還有一個(gè)單獨(dú)的org包,應(yīng)該是spring boot應(yīng)用在打包的使用spring boot插件將這個(gè)package打進(jìn)來(lái),也就是增強(qiáng)了mvn生命周期中的package階段,而正是這個(gè)包在啟動(dòng)過(guò)程中起到了關(guān)鍵的作用,另外中jar中將應(yīng)用所需的各種依賴都打進(jìn)來(lái),并且打入了spring boot額外的package,這種可以all-in-one的jar也被稱之為fat.jar,下文我們將一直以fat.jar來(lái)代替打出的jar的名字。
3.MANIFEST.MF文件
這個(gè)時(shí)候我們?cè)倮^續(xù)看META-INF中的MANIFEST.MF文件,如下:
Manifest-Version: 1.0 Implementation-Title: open :: server :: openapi Implementation-Version: 1.0-SNAPSHOT Archiver-Version: Plexus Archiver Built-By: xiaxuan Implementation-Vendor-Id: com.weibangong.open Spring-Boot-Version: 1.4.1.RELEASE Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.PropertiesLauncher Start-Class: com.weibangong.open.openapi.SpringBootWebApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Created-By: Apache Maven 3.3.9 Build-Jdk: 1.8.0_20 Implementation-URL: http://maven.apache.org/open-server-openapi
這里指定的main-class是單獨(dú)打入的包中的一個(gè)類(lèi)文件而不是我們的啟動(dòng)程序,然后MANIFEST.MF文件有一個(gè)單獨(dú)的start-class指定的是我們的應(yīng)用的啟動(dòng)程序。
4.啟動(dòng)分析
首先我們找到類(lèi)org.springframework.boot.loader.PropertiesLauncher,其中main方法為:
public static void main(String[] args) throws Exception { PropertiesLauncher launcher = new PropertiesLauncher(); args = launcher.getArgs(args); launcher.launch(args); }
查看launch方法,這個(gè)方法在父類(lèi)Launcher中,找到父類(lèi)方法launch方法,如下:
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); this.createMainMethodRunner(mainClass, args, classLoader).run(); } protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); }
launch方法最終調(diào)用了createMainMethodRunner方法,后者實(shí)例化了MainMethodRunner對(duì)象并運(yùn)行了run方法,我們轉(zhuǎn)到MainMethodRunner源碼中,如下:
package org.springframework.boot.loader; import java.lang.reflect.Method; public class MainMethodRunner { private final String mainClassName; private final String[] args; public MainMethodRunner(String mainClass, String[] args) { this.mainClassName = mainClass; this.args = args == null?null:(String[])args.clone(); } public void run() throws Exception { Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName); Method mainMethod = mainClass.getDeclaredMethod("main", new Class[]{String[].class}); mainMethod.invoke((Object)null, new Object[]{this.args}); } }
查看run方法,就很怎么將spring boot的jar怎么運(yùn)行起來(lái)的了,由此分析基本也就結(jié)束了。
5、main程序的啟動(dòng)流程
講完了jar的啟動(dòng)流程,現(xiàn)在來(lái)講下spring boot應(yīng)用中,main程序的啟動(dòng)與加載流程,首先我們看一個(gè)spring boot應(yīng)用的main方法。
package cn.com.devh; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; /** * Created by xiaxuan on 17/8/25. */ @SpringBootApplication @EnableFeignClients @EnableEurekaClient public class A1ServiceApplication { public static void main(String[] args) { SpringApplication.run(A1ServiceApplication.class, args); } }
轉(zhuǎn)到SpringApplication中的run方法,如下:
/** * Static helper that can be used to run a {@link SpringApplication} from the * specified source using default settings. * @param source the source to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */ public static ConfigurableApplicationContext run(Object source, String... args) { return run(new Object[] { source }, args); } /** * Static helper that can be used to run a {@link SpringApplication} from the * specified sources using default settings and user supplied arguments. * @param sources the sources to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */ public static ConfigurableApplicationContext run(Object[] sources, String[] args) { return new SpringApplication(sources).run(args); }
這里的SpringApplication的實(shí)例化是關(guān)鍵,我們轉(zhuǎn)到SpringApplication的構(gòu)造函數(shù)。
/** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified sources (see {@link SpringApplication class-level} * documentation for details. The instance can be customized before calling * {@link #run(String...)}. * @param sources the bean sources * @see #run(Object, String[]) * @see #SpringApplication(ResourceLoader, Object...) */ public SpringApplication(Object... sources) { initialize(sources); } private void initialize(Object[] sources) { if (sources != null && sources.length > 0) { this.sources.addAll(Arrays.asList(sources)); } this.webEnvironment = deduceWebEnvironment(); setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }
這里的initialize方法中的deduceWebEnvironment()確定了當(dāng)前是以web應(yīng)用啟動(dòng)還是以普通的jar啟動(dòng),如下:
private boolean deduceWebEnvironment() { for (String className : WEB_ENVIRONMENT_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return false; } } return true; }
其中的WEB_ENVIRONMENT_CLASSES為:
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" };
只要其中任何一個(gè)不存在,即當(dāng)前應(yīng)用以普通jar的形式啟動(dòng)。
然后setInitializers方法初始化了所有的ApplicationContextInitializer,
/** * Sets the {@link ApplicationContextInitializer} that will be applied to the Spring * {@link ApplicationContext}. * @param initializers the initializers to set */ public void setInitializers( Collection<? extends ApplicationContextInitializer<?>> initializers) { this.initializers = new ArrayList<ApplicationContextInitializer<?>>(); this.initializers.addAll(initializers); } setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))**
這一步初始化所有Listener。
我們?cè)倩氐街暗腟pringApplication(sources).run(args);處,進(jìn)入run方法,代碼如下:
/** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}. * @param args the application arguments (usually passed from a Java main method) * @return a running {@link ApplicationContext} */ public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners.started(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); context = createAndRefreshContext(listeners, applicationArguments); afterRefresh(context, applicationArguments); listeners.finished(context, null); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } return context; } catch (Throwable ex) { handleRunFailure(context, listeners, ex); throw new IllegalStateException(ex); } }
這一步進(jìn)行上下文的創(chuàng)建createAndRefreshContext(listeners, applicationArguments),
private ConfigurableApplicationContext createAndRefreshContext( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { ConfigurableApplicationContext context; // Create and configure the environment ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); listeners.environmentPrepared(environment); if (isWebEnvironment(environment) && !this.webEnvironment) { environment = convertToStandardEnvironment(environment); } if (this.bannerMode != Banner.Mode.OFF) { printBanner(environment); } // Create, load, refresh and run the ApplicationContext context = createApplicationContext(); context.setEnvironment(environment); postProcessApplicationContext(context); applyInitializers(context); listeners.contextPrepared(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans context.getBeanFactory().registerSingleton("springApplicationArguments", applicationArguments); // Load the sources Set<Object> sources = getSources(); Assert.notEmpty(sources, "Sources must not be empty"); load(context, sources.toArray(new Object[sources.size()])); listeners.contextLoaded(context); // Refresh the context refresh(context); if (this.registerShutdownHook) { try { context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } } return context; } // Create and configure the environment ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs());
這一步進(jìn)行了環(huán)境的配置與加載。
if (this.bannerMode != Banner.Mode.OFF) { printBanner(environment); }
這一步進(jìn)行了打印spring boot logo,需要更改的話,在資源文件中加入banner.txt,banner.txt改為自己需要的圖案即可。
// Create, load, refresh and run the ApplicationContext context = createApplicationContext(); return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass)
創(chuàng)建上下文,這一步中真正包含了是創(chuàng)建什么容器,并進(jìn)行了響應(yīng)class的實(shí)例化,其中包括了EmbeddedServletContainerFactory的創(chuàng)建,是選擇jetty還是tomcat,內(nèi)容繁多,留待下一次再講。
if (this.registerShutdownHook) { try { context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } }
這一步就是當(dāng)前上下文進(jìn)行注冊(cè),當(dāng)收到kill指令的時(shí)候進(jìn)行容器的銷(xiāo)毀等工作了。
基本到此,啟動(dòng)的分析就結(jié)束了,但是還有一些細(xì)節(jié)講述起來(lái)十分耗時(shí),這個(gè)留待后續(xù)的博文中再來(lái)講述,今天就到這里。
6.總結(jié)
綜上spring boot jar的啟動(dòng)流程基本就是下面幾個(gè)步驟:
1、我們正常進(jìn)行maven打包時(shí),spring boot插件擴(kuò)展maven生命周期,將spring boot相關(guān)package打入到j(luò)ar中,這個(gè)jar中包含了應(yīng)用所打出的jar以外還有spring boot啟動(dòng)程序相關(guān)的類(lèi)文件。
2、我以前看過(guò)稍微低一些版本的spring boot的jar的啟動(dòng)流程,當(dāng)時(shí)我記得是當(dāng)前線程又起了一個(gè)新線程來(lái)運(yùn)行main程序,而現(xiàn)在的已經(jīng)改成了直接使用反射來(lái)啟動(dòng)main程序。
總結(jié)
以上所述是小編給大家介紹的spring boot jar的啟動(dòng)原理解析,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
詳解idea搭建springboot+mybatis框架的教程
這篇文章主要介紹了詳解idea搭建springboot+mybatis框架的教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11JAVA生產(chǎn)者消費(fèi)者(線程同步)代碼學(xué)習(xí)示例
這篇文章主要介紹了JAVA線程同步的代碼學(xué)習(xí)示例,大家參考使用吧2013-11-11IDEA新建Springboot項(xiàng)目(圖文教程)
下面小編就為大家?guī)?lái)一篇IDEA新建Springboot項(xiàng)目(圖文教程)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07SpringBoot利用自定義注解實(shí)現(xiàn)多數(shù)據(jù)源
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何利用自定義注解實(shí)現(xiàn)多數(shù)據(jù)源效果,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以了解一下2022-10-10SpringBoot3整合MyBatis出現(xiàn)異常:Property?'sqlSessionFactory&a
這篇文章主要介紹了SpringBoot3整合MyBatis報(bào)錯(cuò):Property?‘sqlSessionFactory‘?or?‘sqlSessionTemplate‘?are?required,其實(shí)不是個(gè)大問(wèn)題,只是自己編碼時(shí)遇到了,然后總結(jié)總結(jié)分享一下,如果有遇到類(lèi)似問(wèn)題的,可以參考一下2022-11-11