前言
在拥有 Spring Boot 以前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat ),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。
在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main () 方法就可以启动一个 Web 应用了。
追本溯源
只需要下面几行代码我们就可以跑起一个 Web 服务器:
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
去掉类的声明和方法定义这些样板代码,核心代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run (SpringbootApplication.class, args) 方法。而我们知道注解相当于是一种配置,那么这个 run () 方法必然就是 Spring Boot 的启动入口了。
容器启动流程
接下来,我们沿着 run () 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run () 方法的具体实现:
public class SpringApplication {
......
public ConfigurableApplicationContext run(String... args) {
// 1 应用启动计时开始
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 2 声明上下文
ConfigurableApplicationContext context = null;
// 3 初始化异常报告集合
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 4 设置 java.awt.headless 属性
configureHeadlessProperty();
// 5 启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 6 初始化默认应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 7 准备应用环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
// 8 打印 Banner(Spring Boot 的 LOGO)
Banner printedBanner = printBanner(environment);
// 9 通过反射创建上下文实例
context = createApplicationContext();
// 10 构建异常报告
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 11 构建上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 12 刷新上下文
refreshContext(context);
// 13 刷新上下文后处理
afterRefresh(context, applicationArguments);
// 14 应用启动计时结束
stopWatch.stop();
if (this.logStartupInfo) {
// 15 打印启动时间日志
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 16 发布上下文启动完成事件
listeners.started(context);
// 17 调用 runners
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 18 应用启动发生异常后的处理
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 19 发布上下文就绪事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
......
}
Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run () 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run () 方法并不是一个静态方法,所以需要一个对象实例才能被调用。可以看到,方法的返回值类型为ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(上下文(context)是 Spring 中的核心概念)。
创建上下文实例createApplicationContext
下面我们来到 run () 方法中编号 9 的位置,这里调用了一个 createApplicationContext () 方法,点进去我们会看到它的代码如下:
public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由WebApplicationType.deduceFromClasspath()获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。
我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会通过反射加载DEFAULT_SERVLET_WEB_CONTEXT_CLASS,最后返回一个 AnnotationConfigServletWebServerApplicationContext实例(就像我们上文所说的那样)。
构建容器上下文prepareContext
接着我们来到 run () 方法编号 11 的 prepareContext () 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
......
// 加载资源
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。
加载资源
上面的代码中,又调用了一个很关键的方法 —— load ()。这个 load () 方法真正的作用是去调用 BeanDefinitionLoader 类的 load () 方法。源码如下:
class BeanDefinitionLoader {
......
int load() {
int count = 0;
for (Object source : this.sources) {
count += load(source);
}
return count;
}
private int load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class<?>) {
return load((Class<?>) source);
}
if (source instanceof Resource) {
return load((Resource) source);
}
if (source instanceof Package) {
return load((Package) source);
}
if (source instanceof CharSequence) {
return load((CharSequence) source);
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
......
}
可以看到,load () 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load ((Class<?>) source) 和 load ((Package) source) 了。一个用来加载类,一个用来加载扫描的包。
load ((Class<?>) source) 中会通过调用 isComponent () 方法来判断资源是否为 Spring 容器管理的组件。 isComponent () 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。
而 load ((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。
总结
我们知道,Spring 是一个容器,我们喜欢它的一个重要原因就是它帮我们把 Bean 进行了统一的管理。Bean 的创建与销毁都由 Spring 来完成,而我们只需要关注使用,这也是 Spring IoC 的核心工作内容。
到此,Spring 真正开始开展 Bean 管理的工作了,prepareContext () 方法把所有需要管理的 Bean 统计出来,在后面的 refreshContext () 方法中会进行更进一步的操作。 refreshContext () 方法和自动配置关系紧密。
评论 (0)