1 Springboot 启动流程
-
创建一个StopWatch实例,用来记录SpringBoot的启动时间。
-
通过SpringFactoriesLoader加载listeners:比如EventPublishingRunListener。
-
发布SprintBoot开始启动事件(EventPublishingRunListener#starting())。
-
创建和配置environment(environmentPrepared())。
-
打印SpringBoot的banner和版本。
-
创建对应的ApplicationContext:Web类型,Reactive类型,普通的类型(非Web)
-
刷新上下文 prepareContext:自动装配和启动 tomcat就是在这个方法里面完成的
7.1 准备ApplicationContext,Initializers设置到ApplicationContext(contextPrepared())。
7.2 打印启动日志,打印profile信息(如dev, test, prod)。 7.3 最终会调用到AbstractApplicationContext#refresh方法,实际上就是Spring IOC容器的创建过程,并且会进行自动装配的操作,以及发布ApplicationContext已经refresh事件,标志着ApplicationContext初始化完成(contextLoaded())
-
afterRefresh hook方法。
-
stopWatch停止计时,日志打印总共启动的时间。
-
发布SpringBoot程序已启动事件(started())。
-
调用ApplicationRunner和CommandLineRunner。
-
最后发布就绪事件ApplicationReadyEvent,标志着SpringBoot可以处理就收的请求了(running())。
public ConfigurableApplicationContext run(String... args) {
// 创建一个StopWatch实例,用来记录SpringBoot的启动时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 通过SpringFactoriesLoader加载listeners:比如EventPublishingRunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布SprintBoot启动事件:ApplicationStartingEvent
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 创建和配置environment,发布事件:SpringApplicationRunListeners#environmentPrepared
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印SpringBoot的banner和版本
Banner printedBanner = printBanner(environment);
// 创建对应的ApplicationContext:Web类型,Reactive类型,普通的类型(非Web)
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 准备ApplicationContext,Initializers设置到ApplicationContext后发布事件:ApplicationContextInitializedEvent
// 打印启动日志,打印profile信息(如dev, test, prod)
// 调用EventPublishingRunListener发布ApplicationContext加载完毕事件:ApplicationPreparedEvent
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 最终会调用到AbstractApplicationContext#refresh方法,实际上就是Spring IOC容器的创建过程,并且会进行自动装配的操作
// 以及发布ApplicationContext已经refresh事件,标志着ApplicationContext初始化完成
refreshContext(context);
// hook方法
afterRefresh(context, applicationArguments);
// stopWatch停止计时,日志打印总共启动的时间
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 发布SpringBoot程序已启动事件ApplicationStartedEvent
listeners.started(context);
// 调用ApplicationRunner和CommandLineRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 最后发布就绪事件ApplicationReadyEvent,标志着SpringBoot可以处理就收的请求了
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
2 Springboot IOC容器初始化过程
Springboot 启动过程,其核心就是IOC容器初始化过程,所以可以把上诉过程简化为
-
第一步,创建IOC容器;
IOC容器是由类AnnotationConfigServletWebServerApplicationContext初始化的一个实例,其中包含了bean工厂、类加载器、bean定义信息和环境变量等主要信息。
通过AnnotationConfigServletWebServerApplicationContext的初始化,IOC容器初始化了一些属性,设置了一些特定的bean定义信息。其主要的作用,还是为后续的IOC容器创建Bean做准备。
-
第二步,准备IOC容器相关信息
在项目启动后,进行一系列的初始化。包括SpringApplication实例准备,listener监听器准备,environment环境准备。
-
第三步,刷新IOC容器,也就是我们通常说的bean的初始化过程
加载当前主启动类下的所有类文件到IOC容器:经过ConfigurationClassParser类中的parse方法调用AnnotatedBeanDefinition类型的parse方法,我们完成了对当前项目工程下所有类,注册到IOC容器中的操作。
加入Spring自带的框架类到IOC容器(自动装配):在ConfigurationClassParser类中的执行方法为:this.deferredImportSelectorHandler.process(); 这里用到Spring Factories 机制,见Springboot自动配置
关于bean信息注册的方法,基本都在ConfigurationClassParser这个类中,processConfigBeanDefinitions实现bean信息注册的入口方法在ConfigurationClassParser的parse方法中:
public void parse(Set<BeanDefinitionHolder> configCandidates) {
//主启动类bean信息集合
Iterator var2 = configCandidates.iterator();
while(var2.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder)var2.next();
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
//注册项目工程类信息
this.parse(((AnnotatedBeanDefinition)bd).getMetadata(), holder.getBeanName());
.......
}
//注册Spring提供默认类信息
this.deferredImportSelectorHandler.process();
}
然后,就可以完成bean的大规模实例化
-
第四步,完成刷新IOC容器后处理
这是一个抽象方法,且没有继承类。
-
第五步,结束Springboot启动流程
记录结束时间;
发布SpringBoot程序已启动事件;
执行容器中所有的ApplicationRunner和CommandLineRunner类型的run方法
public ConfigurableApplicationContext run(String... args) {
......
ConfigurableApplicationContext context = null;
......
//1创建容器
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//2准备容器相关信息
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//3刷新容器,也就是通常我们说的bean的初始化
this.refreshContext(context);
//4完成初始化后处理
this.afterRefresh(context, applicationArguments);
......
}
3 自定义应用
3.1 自定义初始化器
自定义初始化器首先要实现 ApplicationContextInitializer 接口
执行自定义初始化器的方式有三种
-
方式一:通过通过SpringApplication对象调用addInitializers(new 自定义初始化器)
@SpringBootApplication public class NettyTestApplication { public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(NettyTestApplication.class); springApplication.addInitializers(new SecondInitializer()); springApplication.run(args); } } @Order(2) public class SecondInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment(); Map<String, Object> map = new HashMap<>(); map.put("key2","value2"); MapPropertySource firstInitializer = new MapPropertySource("secondInitializer", map); environment.getPropertySources().addLast(firstInitializer); System.out.println("run second initializer"); } }
-
方式二:通过META-INF/spring.factories中指定
-
org.springframework.context.ApplicationContextInitializer=自定义初始化器
# 在 resources 文件夹下新建 META-INF 文件夹,META-INF 文件夹下新建 spring.factories 配置文件,添加内容如下: org.springframework.context.ApplicationContextInitializer=com.xxx.FirstInitializer // order 注解用于排序 @Order(1) public class FirstInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment(); Map<String, Object> map = new HashMap<>(); map.put("key1","value1"); MapPropertySource firstInitializer = new MapPropertySource("firstInitializer", map); environment.getPropertySources().addLast(firstInitializer); System.out.println("run first initializer"); } }
-
方式三:通过application.properties中指定 context.initializer.classes=自定义初始化器
-
利用 ApplicationContextInitializer 接口的实现类 DelegatingApplicationContextInitializer
在 springboot 的默认配置文件 application.properties 中添加 context.initializer.classes=com.xxx.ThirdInitializer @Order(3) public class ThirdInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment(); Map<String, Object> map = new HashMap<>(); map.put("key3","value3"); MapPropertySource firstInitializer = new MapPropertySource("thirdInitializer", map); environment.getPropertySources().addLast(firstInitializer); System.out.println("run third initializer"); } } @SpringBootApplication public class NettyTestApplication { public static void main(String[] args) { SpringApplication.run(NettyTestApplication.class, args); } }
3.2 自定义监听器
springboot进行事件监听有四种方式:
-
手工向ApplicationContext中添加监听器
public class MyListener1 implements ApplicationListener<MyEvent>{ Logger logger = Logger.getLogger(MyListener1.class); public void onApplicationEvent(MyEvent event) { logger.info(String.format("%s监听到事件源:%s.", MyListener1.class.getName(), event.getSource())); } } @SpringBootApplication public class LisenterApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(LisenterApplication.class, args); //装载监听 context.addApplicationListener(new MyListener1()); } }
-
使用@Component注解将监听器装载入spring容器
@Component public class MyListener2 implements ApplicationListener<MyEvent> { Logger logger = Logger.getLogger(MyListener2.class); public void onApplicationEvent(MyEvent event) { logger.info(String.format("%s监听到事件源:%s.", MyListener2.class.getName(), event.getSource())); } }
-
在application.properties中配置监听器
在application.properties中配置监听 context.listener.classes=com.listener.MyListener3 public class MyListener3 implements ApplicationListener<MyEvent> { Logger logger = Logger.getLogger(MyListener3.class); public void onApplicationEvent(MyEvent event) { logger.info(String.format("%s监听到事件源:%s.", MyListener3.class.getName(), event.getSource())); } }
-
通过@EventListener注解实现事件监听
@Component public class MyListener4 { Logger logger = Logger.getLogger(MyListener4.class); @EventListener public void listener(MyEvent event) { logger.info(String.format("%s监听到事件源:%s.", MyListener4.class.getName(), event.getSource())); } }
3.3 自定义 banner信息
在启动 Spring Boot 项目时会在控制台打印如下内容(logo 和版本信息):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.6)
如果我们想要使用自己的banner,我们可以直接替换,在项目的 resources 资源目录下创建 banner.txt
文件,banner.txt
内容如下
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永无BUG 永不修改 //
:: Spring Boot :: (${spring-boot.version})
3.4 自定义启动异常堆栈信息打印方式
SpringBoot在项目启动时如果遇到异常并不能友好的打印出具体的堆栈错误信息,我们只能查看到简单的错误消息,以致于并不能及时解决发生的问题,针对这个问题SpringBoot提供了故障分析仪的概念(failure-analyzer),内部根据不同类型的异常提供了一些实现。
实现方法是,在springboot启动的核心方法run中会加载所有的SpringBootExceptionReporter,SpringBootExceptionReporter是一个回调接口,用于支持对SpringApplication启动错误的自定义报告。里面就一个报告启动失败的方法。
其实现类:org.springframework.boot.diagnostics.FailureAnalyzers
用于触发从spring.factories加载的FailureAnalyzer和FailureAnalysisReporter实例。
SpringBoot内部通过实现AbstractFailureAnalyzer抽象类定义了一系列的针对性异常类型的启动分析,如下图所示:
指定异常分析
SpringBoot内部提供的启动异常分析都是指定具体的异常类型实现的,最常见的一个错误就是端口号被占用(PortInUseException),虽然SpringBoot内部提供一个这个异常的启动分析,我们也是可以进行替换这一异常分析的,我们只需要创建PortInUseException异常的AbstractFailureAnalyzer,并且实现类注册给SpringBoot即可,实现自定义如下所示:
/**
* 端口号被占用{@link PortInUseException}异常启动分析
*/
public class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
/**
* logger instance
*/
static Logger logger = LoggerFactory.getLogger(PortInUseFailureAnalyzer.class);
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
logger.error("端口被占用。", cause);
return new FailureAnalysis("端口号:" + cause.getPort() + "被占用", "PortInUseException", rootFailure);
}
}
注册启动异常分析
在上面我们只是编写了指定异常启动分析,我们接下来需要让它生效,这个生效方式比较特殊,类似于自定义SpringBoot Starter AutoConfiguration的形式,我们需要在META-INF/spring.factories文件内进行定义,如下所示:
org.springframework.boot.diagnostics.FailureAnalyzer=\ org.minbox.chapter.springboot.failure.analyzer.PortInUseFailureAnalyzer
项目启动遇到的异常顺序不能确定,很可能在Spring IOC并未执行初始化之前就出现了异常,我们不能通过@Component注解的形式使其生效,所以SpringBoot提供了通过spring.factories配置文件的方式定义。
3.5 自定义初始化任务 Runner
在项目启动的时候需要做一些初始化的操作,比如初始化线程池,提前加载好加密证书等。可以通过实现Runner接口完成以上工作。
-
实现 ApplicationRunner 接口
@Component public class VipSoftServerRunner implements ApplicationRunner{ private static final Logger logger = LoggerFactory.getLogger(JmxhphServerRunner.class); @Override public void run(ApplicationArguments args) throws Exception { System.out.println("项目启动,执行 CommandLineRunner 实现类的方法"); } }
-
实现 CommandLineRunner 接口
@Component public class VipSoftServerRunner implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(JmxhphServerRunner.class); @Override public void run(String... args) throws Exception { System.out.println("项目启动,执行 CommandLineRunner 实现类的方法"); } }
两者只是参数上的区别而已。
-
ApplicaitonRunner的run方法接收一个ApplicationArguments类型的参数,ApplicationArguments会对spring-boot程序的启动参数进行解析和分类,把[--{operation-name}={operation-value}]解析操作参数,其它情况被分类为非操作参数。
使用选项参数必须遵循精确的语法,也就是说,选项必须以”–“为前缀,并且可以指定值,也可以不指定值;如果指定了一个值,则名称和值必须用”=“号分割,且不带空格,值可以是空字符串。
选项参数的有效示例:
--foo --foo= --foo="" --foo=bar --foo="bar then baz" --foo=bar,baz,biz java -jar test.jar --server.port=8080
-
CommandLineRunner的run方法接收的是一个String类型的可变参数,它的值就是我们main函数接收到的命令行参数。