[SpringBoot] SpringApplication.run 执行流程

摘要:
我不得不说SpringBoot太复杂了。我只是想研究SpringBoot最简单的HelloWorld程序是如何从主方法一步步开始的,但这是一个相当深的洞。HelloWorld首先,让我们看看SpringBoot的简单HelloWorld代码。有两个文件,HelloControll。java和Application.java。正在运行应用程序。java可以运行一个简单的RESTful Web服务器。“;13}1415}1617//Application.java18packagehello;1920importorg.springframework.boot.SpringBootApplication;21importorg.springfframework.boot.autoconfigure.SpringBootApplication;2223@SpringBootApplication24publicclassApplication{2526 public static void main{27SpringApplication.run;28}2930}当我打开浏览器,看到服务器正常向浏览器显示输出时,我不禁感叹SpringBoot真的太简单了。有必要深入了解SpringApplication。运行()方法查找。
作者:王奕然
链接:https://www.zhihu.com/question/21346206/answer/101789659
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

建议不要硬着头皮看spring代码,本身的代码800多m,就是不上班开始看也不知道什么时候看完。如果想学学ioc,控制反转这些建议看看jodd项目,比较简练,但是我仍然不建议过多的看这些框架的代码,因为这些代码要完成任务需要很多琐碎的类实现,比如读取某个包下面的所有类,解析class的头文件,反射各种信息,再加上封装,很有可能在读源码的过程中掉到各种细节里出不来,所以读这种源码要事无巨细,理解原理即可。
基本原理其实就是通过反射解析类及其类的各种信息,包括构造器、方法及其参数,属性。然后将其封装成bean定义信息类、constructor信息类、method信息类、property信息类,最终放在一个map里,也就是所谓的container,池等等,其实就是个map。。汗。。。。当你写好配置文件,启动项目后,框架会先按照你的配置文件找到那个要scan的包,然后解析包里面的所有类,找到所有含有@bean,@service等注解的类,利用反射解析它们,包括解析构造器,方法,属性等等,然后封装成各种信息类放到一个map里。每当你需要一个bean的时候,框架就会从container找是不是有这个类的定义啊?如果找到则通过构造器new出来(这就是控制反转,不用你new,框架帮你new),再在这个类找是不是有要注入的属性或者方法,比如标有@autowired的属性,如果有则还是到container找对应的解析类,new出对象,并通过之前解析出来的信息类找到setter方法,然后用该方法注入对象(这就是依赖注入)。如果其中有一个类container里没找到,则抛出异常,比如常见的spring无法找到该类定义,无法wire的异常。还有就是嵌套bean则用了一下递归,container会放到servletcontext里面,每次reQuest从servletcontext找这个container即可,不用多次解析类定义。如果bean的scope是singleton,则会重用这个bean不再重新创建,将这个bean放到一个map里,每次用都先从这个map里面找。如果scope是session,则该bean会放到session里面。仅此而已,没必要花更多精力。建议还是多看看底层的知识。

不得不说 SpringBoot 太复杂了,我本来只想研究一下 SpringBoot 最简单的 HelloWorld 程序是如何从 main 方法一步一步跑起来的,但是这却是一个相当深的坑。你可以试着沿着调用栈代码一层一层的深入进去,如果你不打断点,你根本不知道接下来程序会往哪里流动。这个不同于我研究过去的 Go 语言、Python 语言框架,它们通常都非常直接了当,设计上清晰易懂,代码写起来简单,里面的实现同样也很简单。但是 SpringBoot 不是,它的外表轻巧简单,但是它的里面就像一只巨大的怪兽,这只怪兽有千百只脚把自己缠绕在一起,把爱研究源码的读者绕的晕头转向。但是这 Java 编程的世界 SpringBoot 就是老大哥,你却不得不服。即使你的心中有千万头草泥马在奔跑,但是它就是天下第一。如果你是一个学院派的程序员,看到这种现象你会怀疑人生,你不得不接受一个规则 —— 受市场最欢迎的未必就是设计的最好的,里面夹杂着太多其它的非理性因素。

[SpringBoot] SpringApplication.run 执行流程第1张

经过了一番痛苦的折磨,我还是把 SpringBoot 的运行原理摸清楚了,这里分享给大家。

Hello World

首先我们看看 SpringBoot 简单的 Hello World 代码,就两个文件 HelloControll.java 和 Application.java,运行 Application.java 就可以跑起来一个简单的 RESTFul Web 服务器了。

[SpringBoot] SpringApplication.run 执行流程第2张

 

[SpringBoot] SpringApplication.run 执行流程第3张

 1 // HelloController.java
 2 package hello;
 3 
 4 import org.springframework.web.bind.annotation.RestController;
 5 import org.springframework.web.bind.annotation.RequestMapping;
 6 
 7 @RestController
 8 public class HelloController {
 9 
10     @RequestMapping("/")
11     public String index() {
12         return "Greetings from Spring Boot!";
13     }
14 
15 }
16 
17 // Application.java
18 package hello;
19 
20 import org.springframework.boot.SpringApplication;
21 import org.springframework.boot.autoconfigure.SpringBootApplication;
22 
23 @SpringBootApplication
24 public class Application {
25 
26     public static void main(String[] args) {
27         SpringApplication.run(Application.class, args);
28     }
29 
30 }

当我打开浏览器看到服务器正常地将输出呈现在浏览器的时候,我不禁大呼 —— SpringBoot 真他妈太简单了。

[SpringBoot] SpringApplication.run 执行流程第4张

但是问题来了,在 Application 的 main 方法里我压根没有任何地方引用 HelloController 类,那么它的代码又是如何被服务器调用起来的呢?这就需要深入到 SpringApplication.run() 方法中看个究竟了。不过即使不看代码,我们也很容易有这样的猜想,SpringBoot 肯定是在某个地方扫描了当前的 package,将带有 RestController 注解的类作为 MVC 层的 Controller 自动注册进了 Tomcat Server。

还有一个让人不爽的地方是 SpringBoot 启动太慢了,一个简单的 Hello World 启动居然还需要长达 5 秒,要是再复杂一些的项目这样龟漫的启动速度那真是不好想象了。

再抱怨一下,这个简单的 HelloWorld 虽然 pom 里只配置了一个 maven 依赖,但是传递下去,它一共依赖了 36 个 jar 包,其中以 spring 开头的 jar 包有 15 个。说这是依赖地狱真一点不为过。

[SpringBoot] SpringApplication.run 执行流程第5张

批评到这里就差不多了,下面就要正是进入主题了,看看 SpringBoot 的 main 方法到底是如何跑起来的。

SpringBoot 的堆栈

了解 SpringBoot 运行的最简单的方法就是看它的调用堆栈,下面这个启动调用堆栈还不是太深,我没什么可抱怨的。

[SpringBoot] SpringApplication.run 执行流程第6张

public class TomcatServer {

  @Override
  public void start() throws WebServerException {
  ...
  }

}

接下来再看看运行时堆栈,看看一个 HTTP 请求的调用栈有多深。不看不知道一看吓了一大跳!

[SpringBoot] SpringApplication.run 执行流程第7张

 

我通过将 IDE 窗口全屏化,并将其它的控制台窗口源码窗口统统最小化,总算勉强一个屏幕装下了整个调用堆栈。 

不过转念一想,这也不怪 SpringBoot,绝大多数都是 Tomcat 的调用堆栈,跟 SpringBoot 相关的只有不到 10 层。

探索 ClassLoader

SpringBoot 还有一个特色的地方在于打包时它使用了 FatJar 技术将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前项目的 class 被统一放到了 BOOT-INF/classes 目录中。

1 <build>
2     <plugins>
3         <plugin>
4             <groupId>org.springframework.boot</groupId>
5             <artifactId>spring-boot-maven-plugin</artifactId>
6         </plugin>
7     </plugins>
8 </build>

这不同于我们平时经常使用的 maven shade 插件,将所有的依赖 jar 包中的 class 文件解包出来后再密密麻麻的塞进统一的 jar 包中。下面我们将 springboot 打包的 jar 包解压出来看看它的目录结构。

├── BOOT-INF
│   ├── classes
│   │   └── hello
│   └── lib
│       ├── classmate-1.3.4.jar
│       ├── hibernate-validator-6.0.12.Final.jar
│       ├── jackson-annotations-2.9.0.jar
│       ├── jackson-core-2.9.6.jar
│       ├── jackson-databind-2.9.6.jar
│       ├── jackson-datatype-jdk8-2.9.6.jar
│       ├── jackson-datatype-jsr310-2.9.6.jar
│       ├── jackson-module-parameter-names-2.9.6.jar
│       ├── javax.annotation-api-1.3.2.jar
│       ├── jboss-logging-3.3.2.Final.jar
│       ├── jul-to-slf4j-1.7.25.jar
│       ├── log4j-api-2.10.0.jar
│       ├── log4j-to-slf4j-2.10.0.jar
│       ├── logback-classic-1.2.3.jar
│       ├── logback-core-1.2.3.jar
│       ├── slf4j-api-1.7.25.jar
│       ├── snakeyaml-1.19.jar
│       ├── spring-aop-5.0.9.RELEASE.jar
│       ├── spring-beans-5.0.9.RELEASE.jar
│       ├── spring-boot-2.0.5.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-json-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-logging-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-web-2.0.5.RELEASE.jar
│       ├── spring-context-5.0.9.RELEASE.jar
│       ├── spring-core-5.0.9.RELEASE.jar
│       ├── spring-expression-5.0.9.RELEASE.jar
│       ├── spring-jcl-5.0.9.RELEASE.jar
│       ├── spring-web-5.0.9.RELEASE.jar
│       ├── spring-webmvc-5.0.9.RELEASE.jar
│       ├── tomcat-embed-core-8.5.34.jar
│       ├── tomcat-embed-el-8.5.34.jar
│       ├── tomcat-embed-websocket-8.5.34.jar
│       └── validation-api-2.0.1.Final.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.springframework
└── org
    └── springframework
        └── boot

这种打包方式的优势在于最终的 jar 包结构很清晰,所有的依赖一目了然。如果使用 maven shade 会将所有的 class 文件混乱堆积在一起,是无法看清其中的依赖。而最终生成的 jar 包在体积上两也者几乎是相等的。

在运行机制上,使用 FatJar 技术运行程序是需要对 jar 包进行改造的,它还需要自定义自己的 ClassLoader 来加载 jar 包里面 lib 目录中嵌套的 jar 包中的类。我们可以对比一下两者的 MANIFEST 文件就可以看出明显差异

 1 // Generated by Maven Shade Plugin
 2 Manifest-Version: 1.0
 3 Implementation-Title: gs-spring-boot
 4 Implementation-Version: 0.1.0
 5 Built-By: qianwp
 6 Implementation-Vendor-Id: org.springframework
 7 Created-By: Apache Maven 3.5.4
 8 Build-Jdk: 1.8.0_191
 9 Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
10  ot-starter-parent/gs-spring-boot
11 Main-Class: hello.Application
12 
13 // Generated by SpringBootLoader Plugin
14 Manifest-Version: 1.0
15 Implementation-Title: gs-spring-boot
16 Implementation-Version: 0.1.0
17 Built-By: qianwp
18 Implementation-Vendor-Id: org.springframework
19 Spring-Boot-Version: 2.0.5.RELEASE
20 Main-Class: org.springframework.boot.loader.JarLauncher
21 Start-Class: hello.Application
22 Spring-Boot-Classes: BOOT-INF/classes/
23 Spring-Boot-Lib: BOOT-INF/lib/
24 Created-By: Apache Maven 3.5.4
25 Build-Jdk: 1.8.0_191
26 Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
27  ot-starter-parent/gs-spring-boot

SpringBoot 将 jar 包中的 Main-Class 进行了替换,换成了 JarLauncher。还增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 main 方法入口。我们再看看这个 JarLaucher 具体干了什么

 1 public class JarLauncher{
 2     ...
 3   static void main(String[] args) {
 4     new JarLauncher().launch(args);
 5   }
 6 
 7   protected void launch(String[] args) {
 8     try {
 9       JarFile.registerUrlProtocolHandler();
10       ClassLoader cl = createClassLoader(getClassPathArchives());
11       launch(args, getMainClass(), cl);
12     }
13     catch (Exception ex) {
14         ex.printStackTrace();
15         System.exit(1);
16     }
17   }
18 
19   protected void launch(String[] args, String mcls, ClassLoader cl) {
20         Runnable runner = createMainMethodRunner(mcls, args, cl);
21         Thread runnerThread = new Thread(runner);
22         runnerThread.setContextClassLoader(classLoader);
23         runnerThread.setName(Thread.currentThread().getName());
24         runnerThread.start();
25   }
26 
27 }
28 
29 class MainMethodRunner {
30   @Override
31   public void run() {
32     try {
33       Thread th = Thread.currentThread();
34       ClassLoader cl = th.getContextClassLoader();
35       Class<?> mc = cl.loadClass(this.mainClassName);
36       Method mm = mc.getDeclaredMethod("main", String[].class);
37       if (mm == null) {
38         throw new IllegalStateException(this.mainClassName
39                         + " does not have a main method");
40       }
41       mm.invoke(null, new Object[] { this.args });
42     } catch (Exception ex) {
43       ex.printStackTrace();
44       System.exit(1);
45     }
46   }
47 }

从源码中可以看出 JarLaucher 创建了一个特殊的 ClassLoader,然后由这个 ClassLoader 来另启一个单独的线程来加载 MainClass 并运行。

又一个问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?我们继续看这个特别的 ClassLoader 的源码

 1 class LaunchedURLClassLoader extends URLClassLoader {
 2   ...
 3   private Class<?> doLoadClass(String name) {
 4     if (this.rootClassLoader != null) {
 5       return this.rootClassLoader.loadClass(name);
 6     }
 7 
 8     findPackage(name);
 9     Class<?> cls = findClass(name);
10     return cls;
11   }
12 
13 }

这里的 rootClassLoader 就是双亲委派模型里的 ExtensionClassLoader ,JVM 内置的类会优先使用它来加载。如果不是内置的就去查找这个类对应的 Package。

 1 private void findPackage(final String name) {
 2     int lastDot = name.lastIndexOf('.');
 3     if (lastDot != -1) {
 4         String packageName = name.substring(0, lastDot);
 5         if (getPackage(packageName) == null) {
 6             try {
 7                 definePackage(name, packageName);
 8             } catch (Exception ex) {
 9                 // Swallow and continue
10             }
11         }
12     }
13 }
14 
15 private final HashMap<String, Package> packages = new HashMap<>();
16 
17 protected Package getPackage(String name) {
18     Package pkg;
19     synchronized (packages) {
20         pkg = packages.get(name);
21     }
22     if (pkg == null) {
23         if (parent != null) {
24             pkg = parent.getPackage(name);
25         } else {
26             pkg = Package.getSystemPackage(name);
27         }
28         if (pkg != null) {
29             synchronized (packages) {
30                 Package pkg2 = packages.get(name);
31                 if (pkg2 == null) {
32                     packages.put(name, pkg);
33                 } else {
34                     pkg = pkg2;
35                 }
36             }
37         }
38     }
39     return pkg;
40 }
41 
42 private void definePackage(String name, String packageName) {
43   String path = name.replace('.', '/').concat(".class");
44   for (URL url : getURLs()) {
45     try {
46       if (url.getContent() instanceof JarFile) {
47         JarFile jf= (JarFile) url.getContent();
48         if (jf.getJarEntryData(path) != null && jf.getManifest() != null) {
49           definePackage(packageName, jf.getManifest(), url);
50           return null;
51         }
52       }
53     } catch (IOException ex) {
54         // Ignore
55     }
56   }
57   return null;
58 }

ClassLoader 会在本地缓存包名和 jar包路径的映射关系,如果缓存中找不到对应的包名,就必须去 jar 包中挨个遍历搜寻,这个就比较缓慢了。不过同一个包名只会搜寻一次,下一次就可以直接从缓存中得到对应的内嵌 jar 包路径。

深层 jar 包的内嵌 class 的 URL 路径长下面这样,使用感叹号 ! 分割

jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class

不过这个定制的 ClassLoader 只会用于打包运行时,在 IDE 开发环境中 main 方法还是直接使用系统类加载器加载运行的。

不得不说,SpringbootLoader 的设计还是很有意思的,它本身很轻量级,代码逻辑很独立没有其它依赖,它也是 SpringBoot 值得欣赏的点之一。

HelloController 自动注册

还剩下最后一个问题,那就是 HelloController 没有被代码引用,它是如何注册到 Tomcat 服务中去的?它靠的是注解传递机制。

[SpringBoot] SpringApplication.run 执行流程第8张

 

SpringBoot 深度依赖注解来完成配置的自动装配工作,它自己发明了几十个注解,确实严重增加了开发者的心智负担,你需要仔细阅读文档才能知道它是用来干嘛的。Java 注解的形式和功能是分离的,它不同于 Python 的装饰器是功能性的,Java 的注解就好比代码注释,本身只有属性,没有逻辑,注解相应的功能由散落在其它地方的代码来完成,需要分析被注解的类结构才可以得到相应注解的属性。

 

那注解是又是如何传递的呢?

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@ComponentScan
public @interface SpringBootApplication {
...
}

public @interface ComponentScan {
    String[] basePackages() default {};
}

首先 main 方法可以看到的注解是 SpringBootApplication,这个注解又是由ComponentScan 注解来定义的,ComponentScan 注解会定义一个被扫描的包名称,如果没有显示定义那就是当前的包路径。SpringBoot 在遇到 ComponentScan 注解时会扫描对应包路径下面的所有 Class,根据这些 Class 上标注的其它注解继续进行后续处理。当它扫到 HelloController 类时发现它标注了 RestController 注解。

@RestController
public class HelloController {
...
}

@Controller
public @interface RestController {
}

而 RestController 注解又标注了 Controller 注解。SpringBoot 对 Controller 注解进行了特殊处理,它会将 Controller 注解的类当成 URL 处理器注册到 Servlet 的请求处理器中,在创建 Tomcat Server 时,会将请求处理器传递进去。HelloController 就是如此被自动装配进 Tomcat 的。

扫描处理注解是一个非常繁琐肮脏的活计,特别是这种用注解来注解注解(绕口)的高级使用方法,这种方法要少用慎用。SpringBoot 中有大量的注解相关代码,企图理解这些代码是乏味无趣的没有必要的,它只会把你的本来清醒的脑袋搞晕。SpringBoot 对于习惯使用的同学来说它是非常方便的,但是其内部实现代码不要轻易模仿,那绝对算不上模范 Java 代码。

用过 SpringBoot 的同学都知道,其程序的启动类是在一个main方法中调用SpringApplication.run方法执行的,如:

@SpringBootApplication
public class SpringApplicationBootstrap {

    public static void main(String[] args) {
        SpringApplication.run(SpringApplicationBootstrap.class, args);
    }
}

那么,这里面到底做了什么呢?本篇文章将深入源码,带你一起探究底层实现。

SpringApplication 初始化阶段

进入到SpringApplication.run方法,其首先会创建一个SpringApplication对象,我们看其构造函数:

 1 public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
 2         this.resourceLoader = resourceLoader;
 3         Assert.notNull(primarySources, "PrimarySources must not be null");
 4         // primarySources 为 run 方法传入的引导类
 5         this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
 6         // 推断web应用类
 7         this.webApplicationType = deduceWebApplicationType();
 8         // 初始化 initializers 属性
 9         setInitializers((Collection) getSpringFactoriesInstances(
10                 ApplicationContextInitializer.class));
11         // 初始化监听器
12         setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
13         // 推断应用引导类
14         this.mainApplicationClass = deduceMainApplicationClass();
15     }

我们先看推断web应用类的方法deduceWebApplicationType()

 1  private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
 2             "org.springframework.web.context.ConfigurableWebApplicationContext" };
 3 
 4     private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework."
 5             + "web.reactive.DispatcherHandler";
 6 
 7     private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework."
 8             + "web.servlet.DispatcherServlet";
 9 
10     private WebApplicationType deduceWebApplicationType() {
11         if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
12                 && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
13             return WebApplicationType.REACTIVE;
14         }
15         for (String className : WEB_ENVIRONMENT_CLASSES) {
16             if (!ClassUtils.isPresent(className, null)) {
17                 return WebApplicationType.NONE;
18             }
19         }
20         return WebApplicationType.SERVLET;
21     }

根据 classpath 下是否存在某个特征类来决定是否应该创建一个为 Web 应用使用的ApplicationContext类型。具体判断为:

如果仅存在 Reactive 的包,则为WebApplicationType.REACTIVE类型;
如果 Servlet 和 Reactive的包都不存在,则为WebApplicationType.NONE类型;
其他情况都为WebApplicationType.SERVLET类型。

接下来我们看初始化initializers属性的过程,其通过getSpringFactoriesInstances(ApplicationContextInitializer.class)方法获取初始化器:

 1 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
 2             Class<?>[] parameterTypes, Object... args) {
 3         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
 4         // Use names and ensure unique to protect against duplicates
 5         Set<String> names = new LinkedHashSet<>(
 6                 SpringFactoriesLoader.loadFactoryNames(type, classLoader));
 7         List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
 8                 classLoader, args, names);
 9         AnnotationAwareOrderComparator.sort(instances);
10         return instances;
11     }

该方法流程为:

  1. 通过SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法,在 META-INF/spring.factories 文件下查找ApplicationContextInitializer类型对应的资源名称。
  2. 实例化上面的资源信息(初始化器)。
  3. 对初始化器根据Ordered接口或者@Order注解进行排序。

同理,初始化listeners监听器也是类似的,这里不再累赘。

SpringApplication 初始化阶段的最后一步是推断引导类deduceMainApplicationClass():

 1 private Class<?> deduceMainApplicationClass() {
 2         try {
 3             StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
 4             for (StackTraceElement stackTraceElement : stackTrace) {
 5                 if ("main".equals(stackTraceElement.getMethodName())) {
 6                     return Class.forName(stackTraceElement.getClassName());
 7                 }
 8             }
 9         }
10         catch (ClassNotFoundException ex) {
11             // Swallow and continue
12         }
13         return null;
14     }

其将调用栈中main方法所在的类作为引导类。

SpringApplication 运行阶段

SpringApplication 运行阶段属于核心过程,完全围绕 run(String...) 方法展开。该过程结合初始化阶段完成的状态,进一步完善运行时所需要准备的资源,随后启动 Spring 应用上下文。在此期间伴随着 Spring Boot 和 Spring 事件的触发,形成完整的 SpringApplication 生命周期。因此,下面将围绕以下三个子议题进行讨论。

  • SpringApplication 准备阶段
  • ApplicationContext 启动阶段
  • ApplicationContext 启动后阶段
1. SpringApplication 准备阶段

本阶段属于 ApplicationContext 启动阶段的前一阶段,设计的范围从 run(String...)方法调用开始,到refreshContext(ConfigurableApplicationContext)调用前:

 

  •  1 public ConfigurableApplicationContext run(String... args) {
     2         //记录程序运行时间
     3         StopWatch stopWatch = new StopWatch();
     4         stopWatch.start();
     5         //Spring 应用的上下文
     6         ConfigurableApplicationContext context = null;
     7         Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
     8         configureHeadlessProperty();
     9         // 获取 SpringApplicationRunListeners
    10         SpringApplicationRunListeners listeners = getRunListeners(args);
    11         listeners.starting();
    12         try {
    13             // 创建 ApplicationArguments 对象
    14             ApplicationArguments applicationArguments = new DefaultApplicationArguments(
    15                     args);
    16             // 加载属性配置
    17             ConfigurableEnvironment environment = prepareEnvironment(listeners,
    18                     applicationArguments);
    19             // 处理需要忽略的Bean
    20             configureIgnoreBeanInfo(environment);
    21             // 打印 banner
    22             Banner printedBanner = printBanner(environment);
    23             // 创建 Spring 应用上下文
    24             context = createApplicationContext();
    25             // 实例化 SpringBootExceptionReporter,用来报告关于启动过程中的错误
    26             exceptionReporters = getSpringFactoriesInstances(
    27                     SpringBootExceptionReporter.class,
    28                     new Class[] { ConfigurableApplicationContext.class }, context);
    29             // 应用上下文的准备阶段
    30             prepareContext(context, environment, listeners, applicationArguments,
    31                     printedBanner);
    32             // 刷新应用上下文(自动装配,初始化 IOC 容器)
    33             refreshContext(context);
    34             ...
    35         }
    36   }
    getRunListeners(args) 方法:
1 private SpringApplicationRunListeners getRunListeners(String[] args) {
2         Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
3         return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
4                 SpringApplicationRunListener.class, types, this, args));
5     }

该方法会通过getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)方法,获取 META-INF/spring.factories 文件下SpringApplicationRunListener对应的资源,并且实例化这些资源:

1 # Run Listeners
2 org.springframework.boot.SpringApplicationRunListener=
3 org.springframework.boot.context.event.EventPublishingRunListener

我们发现,其有且仅有一个实现类EventPublishingRunListener

 1 public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
 2 
 3     private final SpringApplication application;
 4 
 5     private final String[] args;
 6 
 7     private final SimpleApplicationEventMulticaster initialMulticaster;
 8 
 9     public EventPublishingRunListener(SpringApplication application, String[] args) {
10         this.application = application;
11         this.args = args;
12         this.initialMulticaster = new SimpleApplicationEventMulticaster();
13         for (ApplicationListener<?> listener : application.getListeners()) {
14             this.initialMulticaster.addApplicationListener(listener);
15         }
16     }
17 
18     @Override
19     public int getOrder() {
20         return 0;
21     }
22 
23     @Override
24     public void starting() {
25         this.initialMulticaster.multicastEvent(
26                 new ApplicationStartingEvent(this.application, this.args));
27     }
28 
29     @Override
30     public void environmentPrepared(ConfigurableEnvironment environment) {
31         this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
32                 this.application, this.args, environment));
33     }
34 
35     @Override
36     public void contextPrepared(ConfigurableApplicationContext context) {
37 
38     }
39 
40     @Override
41     public void contextLoaded(ConfigurableApplicationContext context) {
42         for (ApplicationListener<?> listener : this.application.getListeners()) {
43             if (listener instanceof ApplicationContextAware) {
44                 ((ApplicationContextAware) listener).setApplicationContext(context);
45             }
46             context.addApplicationListener(listener);
47         }
48         this.initialMulticaster.multicastEvent(
49                 new ApplicationPreparedEvent(this.application, this.args, context));
50     }
51 
52     @Override
53     public void started(ConfigurableApplicationContext context) {
54         context.publishEvent(
55                 new ApplicationStartedEvent(this.application, this.args, context));
56     }
57 
58     @Override
59     public void running(ConfigurableApplicationContext context) {
60         context.publishEvent(
61                 new ApplicationReadyEvent(this.application, this.args, context));
62     }
63 
64     @Override
65     public void failed(ConfigurableApplicationContext context, Throwable exception) {
66         ApplicationFailedEvent event = new ApplicationFailedEvent(this.application,
67                 this.args, context, exception);
68         if (context != null && context.isActive()) {
69             // Listeners have been registered to the application context so we should
70             // use it at this point if we can
71             context.publishEvent(event);
72         }
73         else {
74             // An inactive context may not have a multicaster so we use our multicaster to
75             // call all of the context's listeners instead
76             if (context instanceof AbstractApplicationContext) {
77                 for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
78                         .getApplicationListeners()) {
79                     this.initialMulticaster.addApplicationListener(listener);
80                 }
81             }
82             this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
83             this.initialMulticaster.multicastEvent(event);
84         }
85     }
86     ...
87 
88 }

在实例化EventPublishingRunListener的过程中,会给它最重要的属性initialMulticaster赋值,其类型是SimpleApplicationEventMulticaster。接着遍历 SpringApplication 初始化阶段的listeners监听器集合,将监听器存入其关联的ListenerRetriever#applicationListeners属性中。

了解 Spring 事件监听机制的同学应该对SimpleApplicationEventMulticaster不陌生,它是ApplicationEvent事件的发布者。Spring Boot 的事件监听机制也是如出一辙,具体可参考我的另一篇文章 深入理解 Spring 的事件发布监听机制

于是接下来调用listeners.starting()方法就会通过其内部的initialMulticaster属性发布ApplicationStartingEvent事件。

  • prepareEnvironment(listeners,applicationArguments) 方法:

加载属性配置。执行完成后,所有的environment的属性都会加载进来,包括 application.properties 和外部的属性配置。

 1 private ConfigurableEnvironment prepareEnvironment(
 2             SpringApplicationRunListeners listeners,
 3             ApplicationArguments applicationArguments) {
 4         // 创建 ConfigurableEnvironment 对象
 5         ConfigurableEnvironment environment = getOrCreateEnvironment();
 6         // 配置 ConfigurableEnvironment
 7         configureEnvironment(environment, applicationArguments.getSourceArgs());
 8         // 发布 ApplicationEnvironmentPreparedEvent 事件
 9         listeners.environmentPrepared(environment);
10         // 将 ConfigurableEnvironment 绑定到 SpringApplication 中
11         bindToSpringApplication(environment);
12         if (this.webApplicationType == WebApplicationType.NONE) {
13             environment = new EnvironmentConverter(getClassLoader())
14                     .convertToStandardEnvironmentIfNecessary(environment);
15         }
16         ConfigurationPropertySources.attach(environment);
17         return environment;
18     }

大致流程为:

  1. 创建ConfigurableEnvironment对象。
  2. 配置environment变量。
  3. 发布ApplicationEnvironmentPreparedEvent事件。
    其对应的监听器为ConfigFileApplicationListener,当接收到上面的事件时,加载并实例化 “META-INF/spring.factories” 文件中EnvironmentPostProcessor类型的实现类,遍历并执行其postProcessEnvironment方法。值得注意的是,ConfigFileApplicationListener自身也是EnvironmentPostProcessor的实现类,于是也会执行其postProcessEnvironment方法:
1 public void postProcessEnvironment(ConfigurableEnvironment environment,
2             SpringApplication application) {
3         // 将配置文件信息添加到 environment 中
4         addPropertySources(environment, application.getResourceLoader());
5         configureIgnoreBeanInfo(environment);
6         // 将 environment 绑定到 Spring 应用上下文中
7         bindToSpringApplication(environment, application);
8     }

该方法的目的是,加载配置文件信息至enviroment,并将enviroment绑定到 Spring 应用上下文中。

当我们需要在配置文件加载完成之后做一些事情的话,我们就可以自定义一个EnvironmentPostProcessor的实现类,操作逻辑写在postProcessEnvironment方法中,当然,别忘了在你的 “META-INF/spring.factories” 文件中加上配置。

  1. 绑定environmentSpringApplication上。
  • createApplicationContext 方法

该方法会根据webApplicationType类型,创建不同的ConfigurableApplicationContextSpring 应用上下文:

 1 protected ConfigurableApplicationContext createApplicationContext() {
 2         Class<?> contextClass = this.applicationContextClass;
 3         if (contextClass == null) {
 4             try {
 5                 switch (this.webApplicationType) {
 6                 case SERVLET:
 7                     contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
 8                     break;
 9                 case REACTIVE:
10                     contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
11                     break;
12                 default:
13                     contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
14                 }
15             }
16             catch (ClassNotFoundException ex) {
17                 throw new IllegalStateException(
18                         "Unable create a default ApplicationContext, "
19                                 + "please specify an ApplicationContextClass",
20                         ex);
21             }
22         }
23         return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
24     }

我们以 SERVLET 类型为例,它会创建AnnotationConfigServletWebServerApplicationContext应用上下文实例。

  • 获取 Spring 异常报告器
    getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context)方法,获取 META-INF/spring.factories 文件下类型为SpringBootExceptionReporter的资源实例:
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=
org.springframework.boot.diagnostics.FailureAnalyzers

其实现类有且仅有一个,即FailureAnalyzers

  • prepareContext 方法
    Spring 应用上下文启动前的准备工作:
 1 private void prepareContext(ConfigurableApplicationContext context,
 2             ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
 3             ApplicationArguments applicationArguments, Banner printedBanner) {
 4         //设置 context 的 environment 属性
 5         context.setEnvironment(environment);
 6         // Spring 应用上下文的后置处理
 7         postProcessApplicationContext(context);
 8         // 运用 Spring 应用上下文初始化器
 9         applyInitializers(context);
10         listeners.contextPrepared(context);
11         if (this.logStartupInfo) {
12             logStartupInfo(context.getParent() == null);
13             logStartupProfileInfo(context);
14         }
15 
16         // Add boot specific singleton beans
17         context.getBeanFactory().registerSingleton("springApplicationArguments",
18                 applicationArguments);
19         if (printedBanner != null) {
20             context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
21         }
22 
23         // Load the sources
24         Set<Object> sources = getAllSources();
25         Assert.notEmpty(sources, "Sources must not be empty");
26         // 加载 BeanDefinition
27         load(context, sources.toArray(new Object[0]));
28         listeners.contextLoaded(context);
29     }

大致流程为:

  1. context的属性做赋值,如设置环境变量,调用初始化器来初始化context
  2. 获取所有配置源信息,包括 Configuration Class、类名、包名及Spring XML 配置资源路径信息。
  3. 加载 Spring 应用上下文配置源。将BeanDefinition加载到context中。
  4. 发布上下文已准备事件ApplicationPreparedEvent

这里,我们要着重看第三步load(context, sources.toArray(new Object[0])):

 1 protected void load(ApplicationContext context, Object[] sources) {
 2         if (logger.isDebugEnabled()) {
 3             logger.debug(
 4                     "Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
 5         }
 6         BeanDefinitionLoader loader = createBeanDefinitionLoader(
 7                 getBeanDefinitionRegistry(context), sources);
 8         if (this.beanNameGenerator != null) {
 9             loader.setBeanNameGenerator(this.beanNameGenerator);
10         }
11         if (this.resourceLoader != null) {
12             loader.setResourceLoader(this.resourceLoader);
13         }
14         if (this.environment != null) {
15             loader.setEnvironment(this.environment);
16         }
17         loader.load();
18     }

该方法将 Spring 应用上下文转载的任务交给了BeanDefinitionLoader:

 1 class BeanDefinitionLoader {
 2 
 3     private final Object[] sources;
 4 
 5     private final AnnotatedBeanDefinitionReader annotatedReader;
 6 
 7     private final XmlBeanDefinitionReader xmlReader;
 8 
 9     private BeanDefinitionReader groovyReader;
10 
11     private final ClassPathBeanDefinitionScanner scanner;
12 
13     private ResourceLoader resourceLoader;
14     ...
15 }
BeanDefinitionLoader组合了多个属性,第一个属性为SpringApplication#getAllSources()方法返回值,而属性annotatedReaderxmlReadergroovyReader分别为注解驱动实现AnnotatedBeanDefinitionReader、XML 配置实现XmlBeanDefinitionReader和 Groovy 实现GroovyBeanDefinitionReader。其中AnnotatedBeanDefinitionReaderClassPathBeanDefinitionScanner配合,形成AnnotationConfigApplicationContext扫描和注册配置类的基础,随后这些配置类被解析为 Bean 定义BeanDefinition
 1 public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
 2 
 3     private final AnnotatedBeanDefinitionReader reader;
 4 
 5     private final ClassPathBeanDefinitionScanner scanner;
 6 
 7 
 8     /**
 9      * Create a new AnnotationConfigApplicationContext that needs to be populated
10      * through {@link #register} calls and then manually {@linkplain #refresh refreshed}.
11      */
12     public AnnotationConfigApplicationContext() {
13         this.reader = new AnnotatedBeanDefinitionReader(this);
14         this.scanner = new ClassPathBeanDefinitionScanner(this);
15     }
16 
17     ...
18 
19     public void register(Class<?>... annotatedClasses) {
20         Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
21         this.reader.register(annotatedClasses);
22     }
23 
24     public void scan(String... basePackages) {
25         Assert.notEmpty(basePackages, "At least one base package must be specified");
26         this.scanner.scan(basePackages);
27     }
28     ...
29 }
不难看出,Spring Boot 中的BeanDefinitionLoader是以上BeanDefinition读取的综合实现。当其load()方法调用时,这些BeanDefinitionReader类型的属性各司其职,为 Spring 应用上下文从不同的配置源装载 Spring Bean 定义(BeanDefinition)。

装载完BeanDefinition到 Spring 应用上下文之后,就调用listeners.contextLoaded(context)方法,发布 Spring 应用上下文已准备ApplicationPreparedEvent事件,以结束 SpringApplication 准备阶段。

2. ApplicationContext 启动阶段

本阶段的执行由refreshContext(ConfigurableApplicationContext)完成,其核心方法是AbstractApplicationContext#refresh:

 1 public void refresh() throws BeansException, IllegalStateException {
 2         synchronized (this.startupShutdownMonitor) {
 3             // Prepare this context for refreshing.
 4             prepareRefresh();
 5 
 6             // Tell the subclass to refresh the internal bean factory.
 7             ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
 8 
 9             // Prepare the bean factory for use in this context.
10             prepareBeanFactory(beanFactory);
11 
12             try {
13                 // Allows post-processing of the bean factory in context subclasses.
14                 postProcessBeanFactory(beanFactory);
15 
16                 // Invoke factory processors registered as beans in the context.
17                 invokeBeanFactoryPostProcessors(beanFactory);
18 
19                 // Register bean processors that intercept bean creation.
20                 registerBeanPostProcessors(beanFactory);
21 
22                 // Initialize message source for this context.
23                 initMessageSource();
24 
25                 // Initialize event multicaster for this context.
26                 initApplicationEventMulticaster();
27 
28                 // Initialize other special beans in specific context subclasses.
29                 onRefresh();
30 
31                 // Check for listener beans and register them.
32                 registerListeners();
33 
34                 // Instantiate all remaining (non-lazy-init) singletons.
35                 finishBeanFactoryInitialization(beanFactory);
36 
37                 // Last step: publish corresponding event.
38                 finishRefresh();
39             }
40 
41             catch (BeansException ex) {
42                 if (logger.isWarnEnabled()) {
43                     logger.warn("Exception encountered during context initialization - " +
44                             "cancelling refresh attempt: " + ex);
45                 }
46 
47                 // Destroy already created singletons to avoid dangling resources.
48                 destroyBeans();
49 
50                 // Reset 'active' flag.
51                 cancelRefresh(ex);
52 
53                 // Propagate exception to caller.
54                 throw ex;
55             }
56 
57             finally {
58                 // Reset common introspection caches in Spring's core, since we
59                 // might not ever need metadata for singleton beans anymore...
60                 resetCommonCaches();
61             }
62         }
63     }

随着该方法的执行,Spring Boot 核心特性也随之启动,如组件自动装配、嵌入式容器启动。了解过 Spring Boot 自动装配机制的同学应该知道(如不清楚,请参考我的另一篇文章 Spring Boot 自动装配),在根据应用类型创建不同的 Spring 应用上下文的方法createApplicationContext()中,会实例化AnnotatedBeanDefinitionReader对象,该对象的构造方法中,会将ConfigurationClassPostProcessor封装成 Spring Bean 定义(BeanDefinition),并将其注入到 Ioc 容器DefaultListableBeanFactory(其实现了BeanDefinitionRegistry,具有注入BeanDefinition的功能)中。于是,在该阶段的invokeBeanFactoryPostProcessors(beanFactory)方法中,就会取出ConfigurationClassPostProcessor对象,随后调用其postProcessBeanFactory(beanFactory)方法进行装配工作。

3. ApplicationContext 启动后阶段

实际上,SpringApplication#afterRefresh方法并未给 Spring 应用上下文启动后阶段提供实现,而是将其交给开发人员自行扩展:

protected void afterRefresh(ConfigurableApplicationContext context,
            ApplicationArguments args) {
    }

所有,直接跳过该步,接下来调用listeners.started(context)方法,发布 Spring 应用上下文已启动ApplicationStartedEvent事件。

该阶段最后,调用callRunners(context, applicationArguments)方法,来调用实现了CommandLineRunner或者ApplicationRunner接口的类的 run 方法,得以满足需要在 Spring 应用上下文完全准备完毕后,执行一些操作的场景。

 1 private void callRunners(ApplicationContext context, ApplicationArguments args) {
 2         List<Object> runners = new ArrayList<>();
 3         runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
 4         runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
 5         AnnotationAwareOrderComparator.sort(runners);
 6         for (Object runner : new LinkedHashSet<>(runners)) {
 7             if (runner instanceof ApplicationRunner) {
 8                 callRunner((ApplicationRunner) runner, args);
 9             }
10             if (runner instanceof CommandLineRunner) {
11                 callRunner((CommandLineRunner) runner, args);
12             }
13         }
14     }

总结

至此,SpringApplciation.run 方法的执行流程已经讲解完毕。下面我们来整理一下大体的步骤:

  1. 初始化 SpringApplication 实例:决定web应用类型、加载初始化器和监听器、推断 main 方法的定义类。
  2. 通过 SpringFactoriesLoader 加载的 SpringApplicationRunListener,调用它们的 started 方法。
  3. 创建并配置当前 Spring Boot 应用将要使用的 Environment,如 applocation.properties 文件和外部配置。
  4. 根据 Web 服务类型创建不同的 Spring 应用上下文,并将之前准备好的 Environment 设置给 Spring 应用上下文 ApplicationContext 使用。
  5. 遍历初始化器,对 ApplicationContext 进行初始化操作。
  6. 加载所有资源,如 Configuration Class、类名、包名以及 Spring XML 配置资源路径,将所有 BeanDefinition 加载至 ApplicationContext。
  7. 初始化上下文 refresh(),进行自动装配,初始化 Ioc 容器等操作。
  8. 寻找当前 ApplicationContext 中是否注册有 CommandLineRunner 或者 ApplicationRunner,如果有,则遍历执行它们。

    [SpringBoot] SpringApplication.run 执行流程第9张
     

作者:habit_learning
链接:https://www.jianshu.com/p/c2789f1548ab
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

免责声明:文章转载自《[SpringBoot] SpringApplication.run 执行流程》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇shiro中用redis做session缓存layer弹层下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

日常编码规范(Java版)

规范: 命名: 接口命名。接口必须是名词,并且接口是能准确的描述要做的事情,命名能清晰的看出输入输出,可以是抽象的行为描述。接口必须以一个动作的名词形式结尾,比如reader,handler等。接口的命名,必须是抽象的,除非接口本身和具体实现紧密相关,否则不应该在接口中包含任何和具体实现相关的名词。接口命名根据行为分为以下几种: 读取某个数据,命名: {...

测试开发进阶——spring boot——MVC——get访问——使用@RequestParam获取参数(前端可传可不传参,以及使用默认值)

控制器: package com.awaimai.web; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframewo...

1034 Head of a Gang (30 分)(图的遍历or并查集)

dfs #include<bits/stdc++.h> using namespace std; const int N=3000; int mp[N][N]; int weight[N]; int vis[N]; map<string,int>si; map<int,string>is; map<string...

srand函数

srand函数是随机数发生器的初始化函数。  原型:void srand(unsigned seed);  用法:它需要提供一个种子,这个种子会对应一个随机数,如果使用相同的种子后面的rand()函数会出现一样的随机数。如: srand(1); 直接使用1来初始化种子。不过为了防止随机数每次重复常常使用系统时间来初始化,即使用 time函数来获得系统时间,...

[转载]ASP.NET Core文件上传与下载(多种上传方式)

ASP.NET Core文件上传与下载(多种上传方式)  前言 前段时间项目上线,实在太忙,最近终于开始可以研究研究ASP.NET Core了. 打算写个系列,但是还没想好目录,今天先来一篇,后面在整理吧. ASP.NET Core 2.0 发展到现在,已经很成熟了.下个项目争取使用吧. 正文 1.使用模型绑定上传文件(官方例子) 官方机器翻译的地址:...

JPA-映射MySql text类型

JPA 映射到 MySql 的 text 类型 ——墨问苍生 创建一个Bean jpa如果直接映射mysql的text/longtext/tinytext类型到String字段会报错。需要设置一下@Lob和@Column。 @Lob代表是长字段类型,默认的话,是longtext类型,所以需要下面这个属性来指定对应的类型。 Notice.java...