spring boot jar的启动原理分析

Posted 叶长风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring boot jar的启动原理分析相关的知识,希望对你有一定的参考价值。

spring boot jar的启动原理分析


  • 1、前言

  • 2、分模块后的结构

  • 3、MANIFEST.MF文件

  • 4、运行

  • 5、main程序的启动流程

  • 6、总结

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包,如下:

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的部分目录结构,大部分省略了,仅仅展示出其中重要的部分。

.
├── 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文件,如下:

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方法为:

public static void main(String[] args) throws Exception 
        PropertiesLauncher launcher = new PropertiesLauncher();
        args = launcher.getArgs(args);
        launcher.launch(args);

查看launch方法,这个方法在父类Launcher中,找到父类方法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方法最终调用了createMainMethodRunner方法,后者实例化了MainMethodRunner对象并运行了run方法,我们转到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怎么运行起来的了,由此分析基本也就结束了。

5、main程序的启动流程


讲完了jar的启动流程,现在来讲下spring boot应用中,main程序的启动与加载流程,首先我们看一个spring boot应用的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);
    

转到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的实例化是关键,我们转到SpringApplication的构造函数。


    /**
     * 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启动,如下:

    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" ;

只要其中任何一个不存在,即当前应用以普通jar的形式启动。

然后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。

我们再回到之前的SpringApplication(sources).run(args);处,进入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);
        
    

这一步进行上下文的创建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());

这一步进行了环境的配置与加载。

        if (this.bannerMode != Banner.Mode.OFF) 
            printBanner(environment);
        

这一步进行了打印spring boot logo,需要更改的话,在资源文件中加入banner.txt,banner.txt改为自己需要的图案即可。

// Create, load, refresh and run the ApplicationContext
        context = createApplicationContext();
return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass)

创建上下文,这一步中真正包含了是创建什么容器,并进行了响应class的实例化,其中包括了EmbeddedServletContainerFactory的创建,是选择jetty还是tomcat,内容繁多,留待下一次再讲。

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程序。

  • 下一节我们讲述@EnableEurekaClient的服务注册过程。

以上是关于spring boot jar的启动原理分析的主要内容,如果未能解决你的问题,请参考以下文章

spring boot应用启动原理分析

Spring Boot:Spring Boot启动原理分析

Spring Boot:Spring Boot启动原理分析

Spring Boot:Spring Boot启动原理分析

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!

我说 Spring Boot 不能用 jar 包启动?leader 过来就是一 jo。。。