1. 知识回顾
为了后文方便,我们先来回顾一下spring的一些核心概念。
spring最核心的功能无非是ioc容器,这个容器里管理着各种bean。ioc容器反映在java类上就是spring的核心类ApplicationContext。ApplicationContext有众多的子接口和子类,不同的实现类有不同的功能。比如ClassPathXmlApplicationContext支持从xml读取bean定义并注册到容器中,AnnotationConfigApplicationContext支持读取@Configuration、@Service等注解定义的bean。
ClassPathXmlApplicationContext和AnnotationConfigApplicationContext代表了我们定义bean的两种最常见方式,即xml和注解方式。还有一种方式是通过@Import来导入bean的定义。如下代码示例了@Import的一种用法。
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@Import(AppConfig.class)
public class AppMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppMain.class);
MyBean bean = context.getBean(MyBean.class);
System.out.println(bean);
}
@Import还有一种用法就是@Import(XxxImportSelector.class),其中XxxImportSelector是ImportSelector这个接口的实现类。下面演示了@Import(XxxImportSelector.class)的用法。
// 一个普通的java类
public class TestImportSelector {
}
// ImportSelector的实现类。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {"com.example.springbootsimple.service.TestImportSelector"};
}
}
// 测试ImportSelector
@Import(MyImportSelector.class)
public class AppMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppMain.class);
TestImportSelector bean = context.getBean(TestImportSelector.class);
System.out.println(bean);
}
}
如上代码运行后,spring容器中就会有TestImportSelector这个bean了,原理是如果@Import()中要import的是一个ImportSelector的实现类,spring就会自动调用ImportSelector.selectImports方法,这个方法会返回一个包含类名的String数组,spring会根据这些类名实例化类,注册到spring容器中。
2. 最简单的springboot启动
我们用springboot启动web应用用得比较多,用多了以后,往往会觉得springboot只能这么用,其实不然,我们先抛开各种配置,从最简单的springboot启动开始看,如下代码是最简单的springboot启动,注意AppMain类上没有任何注解。
public class AppMain {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AppMain.class);
// 打印 org.springframework.context.annotation.AnnotationConfigApplicationContext
System.out.println(context.getClass().getName());
}
}
上面代码可以成功运行(注意成功运行的前提是只引入org.springframework.boot:spring-boot,不要引入spring-boot-starter-web)。我把SpringApplication.run的返回值也打印出来了,是想告诉你,springboot的启动过程其实就是创建一个ApplicationContext的过程。所以,别人问你springboot启动过程是什么时,你就可以说,就是创建了一个ApplicationContext,完事。如果我们从宏观角度来看,这个问题就是这么简单。当然,这个答案肯定不会让人满意,但是不要着急,我们要从简单到复杂,从宏观到微观,这样才能看清事物的本质。
3. 带Web应用的springboot启动
3.1 启动示例
我们引入spring-boot-starter-web,然后执行如下代码。
@SpringBootApplication
public class AppMain {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AppMain.class);
System.out.println(context.getClass().getName());
}
}
此时,打印出的结果是org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext,可以看到和我们第一个例子打印的ApplicationContext不一样了。springboot是如何确定用哪个ApplicationContext的?AnnotationConfigServletWebServerApplicationContext做了什么事情,让我们什么也没做就启动了一个tomcat的web容器?
3.2 如何确定用哪个ApplicationContext?
我们追溯SpringApplication.run方法的源码,会看到如下run方法,这个方法就是比较核心的启动流程了。
我们不需要每一行代码都关心,因为那样会让我们迷失在源码里,我们通过方法名,我们很容易看出来 context = createApplicationContext();这一行代码就是在创建一个ApplicationContext,所以我们看下这个方法源码。
createApplicationContext();会调用DefaultApplicationContextFactory.create(WebApplicationType webApplicationType)这个工厂类来创建ApplicationContext。从上面代码可以看出,DefaultApplicationContextFactory相当于是一个代理,它会通过SpringFactoriesLoader.loadFactories加载其他ApplicationContextFactory来创建Application,如果其他工厂类没的返回结果,就会用默认的AnnotationConfigApplicationContext。SpringFactoriesLoader.loadFactories这个方法有兴趣的可以研究下,这个方法会到spring.factories这个文件里加载你想要的类,spring-boot jar包的spring.factories里定义了如下两个ApplicationContextFactory类,所以这两个类会被加载进来用于创建ApplicationContext。
上面两个Factory从名字也能很容易辨别,一个是用来创建ServletWeb应用的,一个是用来创建ReactiveWeb应用的。如下图是AnnotationConfigServletWebServerApplicationContext.Factory的create方法,可以看到如果webApplicationType是SERVLET类型,就会创建AnnotationConfigServletWebServerApplicationContext。
从上述源码来看,我们第一个疑问已经解决了,springboot会根据wepApplicationType的类型来决定创建什么样的ApplicationContext。至于WebApplicationType如何确定,看下WebApplicationType#deduceFromClasspath就可以了,比较简单,比如会根据类路径下是否有javax.servlet.Servlet等来判断web类型是否是SERVLET等等。当我们引入spring-boot-starter-web时,这个引用就会包含对servlet的引用,所以我们创建的就是SERVLET类型的WebApplicationType。
3.3 AnnotationConfigServletWebServerApplicationContext做了什么?
现在ApplicationContext创建好了,我们也知道什么情况下会创建什么类型的ApplicationContext了,但是这个ApplicationContext还是个比较原始的,很多事情都还没做。了解过一些spring源码的应该知道,ConfigurableApplicationContext有个非常重要的方法:refresh方法。这个方法内部会解析bean的定义,创建好单例bean,应用bean后处理器等。AnnotationConfigServletWebServerApplicationContext就是在refresh方法中搞了点事情,启动了tomcat容器。org.springframework.context.support.AbstractApplicationContext#refresh方法内会调用onRefresh()方法,AnnotationConfigServletWebServerApplicationContext就是重写了onRefresh方法,在这个方法里启动了tomcat。
至于tomcat是如何创建并启动的,这又是另一个话题了,感兴趣的可以自行研究。