1.前言
近來有空對公司的open api平臺進行了些優化,然后在打出jar包的時候,突然想到以前都是對spring boot使用很熟練,但是從來都不知道spring boot打出的jar的啟動原理,然后這回將jar解開了看了下,與想象中確實大不一樣,以下就是對解壓出來的jar的完整分析。
2.jar的結構
spring boot的應用程序就不貼出來了,一個較簡單的demo打出的結構都是類似,另外我采用的spring boot的版本為1.4.1.RELEASE網上有另外一篇文章對spring boot jar啟動的分析,那個應該是1.4以下的,啟動方式與當前版本也有著許多的不同。
在mvn clean install后,我們在查看target目錄中時,會發現兩個jar包,如下:
1
2
|
xxxx.jar xxx.jar.original |
這個則是歸功于spring boot插件的機制,將一個普通的jar打成了一個可以執行的jar包,而xxx.jar.original則是maven打出的jar包,這些可以參考spring官網的文章來了解,如下:
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#executable-jar
以下是spring boot應用打出的jar的部分目錄結構,大部分省略了,僅僅展示出其中重要的部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
. ├── 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 |
這個jar除了我們寫的應用程序打出的class以外還有一個單獨的org包,應該是spring boot應用在打包的使用spring boot插件將這個package打進來,也就是增強了mvn生命周期中的package階段,而正是這個包在啟動過程中起到了關鍵的作用,另外中jar中將應用所需的各種依賴都打進來,并且打入了spring boot額外的package,這種可以all-in-one的jar也被稱之為fat.jar,下文我們將一直以fat.jar來代替打出的jar的名字。
3.MANIFEST.MF文件
這個時候我們再繼續看META-INF中的MANIFEST.MF文件,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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是單獨打入的包中的一個類文件而不是我們的啟動程序,然后MANIFEST.MF文件有一個單獨的start-class指定的是我們的應用的啟動程序。
4.啟動分析
首先我們找到類org.springframework.boot.loader.PropertiesLauncher,其中main方法為:
1
2
3
4
5
|
public static void main(String[] args) throws Exception { PropertiesLauncher launcher = new PropertiesLauncher(); args = launcher.getArgs(args); launcher.launch(args); } |
查看launch方法,這個方法在父類Launcher中,找到父類方法launch方法,如下:
1
2
3
4
5
6
7
|
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方法最終調用了createMainMethodRunner方法,后者實例化了MainMethodRunner對象并運行了run方法,我們轉到MainMethodRunner源碼中,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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怎么運行起來的了,由此分析基本也就結束了。
5、main程序的啟動流程
講完了jar的啟動流程,現在來講下spring boot應用中,main程序的啟動與加載流程,首先我們看一個spring boot應用的main方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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); } } |
轉到SpringApplication中的run方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/** * 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的實例化是關鍵,我們轉到SpringApplication的構造函數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/** * 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()確定了當前是以web應用啟動還是以普通的jar啟動,如下:
1
2
3
4
5
6
7
8
|
private boolean deduceWebEnvironment() { for (String className : WEB_ENVIRONMENT_CLASSES) { if (!ClassUtils.isPresent(className, null )) { return false ; } } return true ; } |
其中的WEB_ENVIRONMENT_CLASSES為:
1
2
|
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet" , "org.springframework.web.context.ConfigurableWebApplicationContext" }; |
只要其中任何一個不存在,即當前應用以普通jar的形式啟動。
然后setInitializers方法初始化了所有的ApplicationContextInitializer,
1
2
3
4
5
6
7
8
9
10
11
|
/** * 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。
我們再回到之前的SpringApplication(sources).run(args);處,進入run方法,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
/** * 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); } } |
這一步進行上下文的創建createAndRefreshContext(listeners, applicationArguments),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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()); |
這一步進行了環境的配置與加載。
1
2
3
|
if ( this .bannerMode != Banner.Mode.OFF) { printBanner(environment); } |
這一步進行了打印spring boot logo,需要更改的話,在資源文件中加入banner.txt,banner.txt改為自己需要的圖案即可。
1
2
3
|
// Create, load, refresh and run the ApplicationContext context = createApplicationContext(); return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass) |
創建上下文,這一步中真正包含了是創建什么容器,并進行了響應class的實例化,其中包括了EmbeddedServletContainerFactory的創建,是選擇jetty還是tomcat,內容繁多,留待下一次再講。
1
2
3
4
5
6
7
8
|
if ( this .registerShutdownHook) { try { context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } } |
這一步就是當前上下文進行注冊,當收到kill指令的時候進行容器的銷毀等工作了。
基本到此,啟動的分析就結束了,但是還有一些細節講述起來十分耗時,這個留待后續的博文中再來講述,今天就到這里。
6.總結
綜上spring boot jar的啟動流程基本就是下面幾個步驟:
1、我們正常進行maven打包時,spring boot插件擴展maven生命周期,將spring boot相關package打入到jar中,這個jar中包含了應用所打出的jar以外還有spring boot啟動程序相關的類文件。
2、我以前看過稍微低一些版本的spring boot的jar的啟動流程,當時我記得是當前線程又起了一個新線程來運行main程序,而現在的已經改成了直接使用反射來啟動main程序。
總結
以上所述是小編給大家介紹的spring boot jar的啟動原理解析,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對服務器之家網站的支持!
原文鏈接:http://blog.csdn.net/u012734441/article/details/78325300