XML方式声明bean
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="cat" class="com.example.bean.Cat"/>
<bean class="com.example.bean.Dog"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
beans>
XML+注解方式声明bean
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.example"/>
beans>
使用@Component及其衍生注解@Controller 、@Service、@Repository定义bean
@Service
public class BookServiceImpl implements BookService {
}
使用@Bean定义第三方bean,并将所在类定义为配置类或Bean
@Component
public class DbConfig {
@Bean
public DruidDataSource getDataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
注解方式声明配置类
@Configuration
@ComponentScan("com.example")
public class SpringConfig {
@Bean
public DruidDataSource getDataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
@Configuration配置项如果不用于被扫描可以省略
扩展1
初始化实现FactoryBean接口的类,实现对bean加载到容器之前的批处理操作
public class BookFactoryBean implements FactoryBean<Book> {
public Book getObject() throws Exception {
Book book = new Book();
// 进行book对象相关的初始化工作
return book;
}
public Class<?> getObjectType() {
return Book.class;
}
}
public class SpringConfig8 {
@Bean
public BookFactoryBean book(){
return new BookFactoryBean();
}
}
扩展2
加载配置类并加载配置文件(系统迁移)
@Configuration
@ComponentScan("com.example")
@ImportResource("applicationContext-config.xml")
public class SpringConfig2 {
}
扩展3
使用proxyBeanMethods=true可以保障调用此方法得到的对象是从容器中获取的而不是重新创建的
@Configuration(proxyBeanMethods = false)
public class SpringConfig3 {
@Bean
public Book book(){
System.out.println("book init ...");
return new Book();
}
}
public class AppObject {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
SpringConfig3 config = ctx.getBean("Config", Config.class);
config.book();
config.book();
}
}
使用@Import注解导入要注入的bean对应的字节码
@Import(Dog.class)
public class SpringConfig5 {
}
被导入的bean无需使用注解声明为bean
public class Dog {
}
此形式可以有效的降低源代码与Spring技术的耦合度,在spring技术底层及诸多框架的整合中大量使用
扩展4
使用@Import注解导入配置类
@Import(DbConfig.class)
public class SpringConfig {
}
使用上下文对象在容器初始化完毕后注入bean
public class AppImport {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig5.class);
ctx.register(Cat.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}
导入实现了ImportSelector接口的类,实现对导入源的编程式处理
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Import");
if(flag){
return new String[]{"com.example.domain.Dog"};
}
return new String[]{"com.example.domain.Cat"};
}
}
导入实现了ImportBeanDefinitionRegistrar接口的类,通过BeanDefinition的注册器注册实名bean,实现对容器中bean的裁定,例如对现有bean的覆盖,进而达成不修改源代码的情况下更换实现的效果
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
registry.registerBeanDefinition("bookService", beanDefinition);
}
}
导入实现了BeanDefinitionRegistryPostProcessor接口的类,通过BeanDefinition的注册器注册实名bean,实现对容器中bean的最终裁定
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
registry.registerBeanDefinition("bookService", beanDefinition);
}
}
bean的加载控制指根据特定情况对bean进行选择性加载以达到适用于项目的目标
编程式
根据任意条件确认是否加载bean
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
try {
Class<?> clazz = Class.forName("com.example.ebean.Mouse");
if(clazz != null) {
return new String[]{"com.example.bean.Cat"};
}
} catch (ClassNotFoundException e) {
return new String[0];
}
return null;
}
}
注解式
使用@Conditional注解的派生注解设置各种组合条件控制bean的加载
① 匹配指定类
public class SpringConfig {
@Bean
@ConditionalOnClass(Mouse.class)
public Cat tom(){
return new Cat();
}
}
② 未匹配指定类
public class SpringConfig {
@Bean
@ConditionalOnClass(Mouse.class)
@ConditionalOnMissingClass("com.itheima.bean.Wolf")
public Cat tom(){
return new Cat();
}
}
③ 匹配指定类型的bean
@Import(Mouse.class)
public class SpringConfig {
@Bean
@ConditionalOnBean(Mouse.class)
public Cat tom(){
return new Cat();
}
}
④ 匹配指定名称的bean
@Import(Mouse.class)
public class SpringConfig {
@Bean
@ConditionalOnBean(name="com.itheima.bean.Mouse")
public Cat tom(){
return new Cat();
}
}
@Import(Mouse.class)
public class SpringConfig {
@Bean
@ConditionalOnBean(name="jerry")
public Cat tom(){
return new Cat();
}
}
⑤ 匹配指定环境
@Configuration
@Import(Mouse.class)
public class MyConfig {
@Bean
@ConditionalOnClass(Mouse.class)
@ConditionalOnMissingClass("com.itheima.bean.Dog")
@ConditionalOnNotWebApplication
public Cat tom(){
return new Cat();
}
}
public class SpringConfig {
@Bean
@ConditionalOnClass(name = "com.mysql.jdbc.Driver")
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
总结:使用@ConditionalOn***注解为bean的加载设置条件
环境准备
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
package com.example.bean;
import lombok.Data;
@Data
public class Cat {
private String name;
private Integer age;
}
package com.example.bean;
import lombok.Data;
@Data
public class Mouse {
private String name;
private Integer age;
}
实验
package com.example.bean;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "cartoon")
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
cartoon:
cat:
name: "Tom_2"
age: 5
mouse:
name: "jerry_2"
age: 6
package com.example.bean;
import lombok.Data;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.util.StringUtils;
@Data
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse {
private Cat cat;
private Mouse mouse;
private CartoonProperties cartoonProperties;
public CartoonCatAndMouse(CartoonProperties cartoonProperties){
this.cartoonProperties = cartoonProperties;
cat = new Cat();
cat.setName(cartoonProperties.getCat()!=null &&
StringUtils.hasText(cartoonProperties.getCat().getName()) ?
cartoonProperties.getCat().getName() : "Tom");
cat.setAge(cartoonProperties.getCat()!=null &&
cartoonProperties.getCat().getAge()!=null ?
cartoonProperties.getCat().getAge() : 3);
mouse = new Mouse();
mouse.setName(cartoonProperties.getMouse()!=null &&
StringUtils.hasText(cartoonProperties.getMouse().getName()) ?
cartoonProperties.getMouse().getName() : "Jerry");
mouse.setAge(cartoonProperties.getMouse()!=null &&
cartoonProperties.getMouse().getAge()!=null ?
cartoonProperties.getMouse().getAge() : 4);
}
public void play(){
System.out.println(cat.getAge()+"岁的"+cat.getName()+"和"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了");
}
}
package com.example;
import com.example.bean.CartoonCatAndMouse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import(CartoonCatAndMouse.class)
public class SpringbootPropertiesApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(SpringbootPropertiesApplication.class, args);
CartoonCatAndMouse bean = ctx.getBean(CartoonCatAndMouse.class);
bean.play();
}
}
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
}
总的来说,SpringBoot中包含了很多常用的技术,且写有条件控制注解,当添加依赖后在符合控制条件的情况下,将会加载该技术相关配置的bean,而且每种技术配备有默认参数,可以通过配置文件覆盖默认参数。
#Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.bean.CartoonCatAndMouse
这样springboot就会自动加载CartoonCatAndMouse类,而不需要@Import(CartoonCatAndMouse.class)
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
- org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration
@EnableAutoConfiguration(excludeName = "",exclude = {})
变更自动配置:去除tomcat自动配置(条件激活),添加jetty自动配置(条件激活)
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jettyartifactId>
dependency>
dependencies>
案例:记录系统访客独立IP访问次数
需求分析
数据记录位置:Map / Redis
功能触发位置:每次web请求(拦截器)
① 步骤一:降低难度,主动调用,仅统计单一操作访问次数(例如查询)
② 步骤二:开发拦截器
业务参数(配置项)
① 输出频度,默认10秒
② 数据特征:累计数据 / 阶段数据,默认累计数据
③ 输出格式:详细模式 / 极简模式
校验环境,设置加载条件
IP计数业务功能开发(自定义starter)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
@Autowired
//当前的request对象的注入工作由使用当前starter的工程提供自动装配
private HttpServletRequest httpServletRequest;
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问次数
//1.获取当前操作的IP地址
String ip = httpServletRequest.getRemoteAddr();
System.out.println("---------------------------" + ip);
//2.根据IP地址从Map取值,并递增
Integer count = ipCountMap.get(ip);
if(count == null){
ipCountMap.put(ip,1);
}else{
ipCountMap.put(ip,ipCountMap.get(ip) + 1);
}
}
}
package com.example.autoconfig;
import com.example.service.IpCountService;
import org.springframework.context.annotation.Import;
@Import(IpCountService.class)
public class IpAutoConfiguration {
}
#Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfig.IpAutoConfiguration
6.Maven ==> clean ==> instal
在图书馆系统上模拟调用
(链接:https://pan.baidu.com/s/1gb1n2I9YRUsiK5qnvaes6w 提取码:f1zs)
<dependency>
<groupId>com.examplegroupId>
<artifactId>ip_spring_boot_starterartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
@Autowired
private IpCountService ipCountService;
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
// TODO 追加ip访问统计
ipCountService.count();
IPage<Book> page = bookService.getPage(currentPage, pageSize,book);
//如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize,book);
}
return new R(true, page);
}
package com.example.autoconfig;
import com.example.service.IpCountService;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
@Import(IpCountService.class)
@EnableScheduling
public class IpAutoConfiguration {
}
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
@Autowired
//当前的request对象的注入工作由使用当前starter的工程提供自动装配
private HttpServletRequest httpServletRequest;
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问次数
//1.获取当前操作的IP地址
String ip = httpServletRequest.getRemoteAddr();
//2.根据IP地址从Map取值,并递增
Integer count = ipCountMap.get(ip);
if(count == null){
ipCountMap.put(ip,1);
}else{
ipCountMap.put(ip,ipCountMap.get(ip) + 1);
}
}
@Scheduled(cron = "0/5 * * * * ?")
public void print(){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
for (Map.Entry<String,Integer> entry : ipCountMap.entrySet()){
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}
public static void main(String[] args) {
new IpCountService().print();
}
}
package com.example;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {
/**
* 日志显示周期
*/
private Long cycle = 5L;
/**
* 是否周期内重置数据
*/
private Boolean cycleReset = false;
/**
* 日志输出模式 detail:详细模式 simple:极简模式
*/
private String model = LogModel.DETAIL.value;
public enum LogModel{
DETAIL("detail"),
SIMPLE("simple");
private String value;
LogModel(String value){
this.value = value;
}
public String getValue(){
return value;
}
}
public Long getCycle() {
return cycle;
}
public void setCycle(Long cycle) {
this.cycle = cycle;
}
public Boolean getCycleReset() {
return cycleReset;
}
public void setCycleReset(Boolean cycleReset) {
this.cycleReset = cycleReset;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}
package com.example.autoconfig;
import com.example.IpProperties;
import com.example.service.IpCountService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
@Import(IpCountService.class)
@EnableConfigurationProperties(IpProperties.class)
@EnableScheduling
public class IpAutoConfiguration {
}
@Scheduled(cron = "0/5 * * * * ?")
public void print(){
//模式切换
if(ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){
//明细模式
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
for (Map.Entry<String,Integer> entry : ipCountMap.entrySet()){
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}else if(ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){
//极简模式
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+");
for (String key: ipCountMap.keySet()){
System.out.println(String.format("|%18s |",key));
}
System.out.println("+--------------------+");
}
//周期内重置数据
if(ipProperties.getCycleReset()){
ipCountMap.clear();
}
}
tools:
ip:
cycle-reset: true
model: simple
查看重置数据和模式切换功能是否成功
添加参数控制定时任务执行时间间隔
@Component("ipProperties")
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {
...
}
package com.example.autoconfig;
import com.example.IpProperties;
import com.example.service.IpCountService;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
@Import({IpCountService.class,IpProperties.class})
//@EnableConfigurationProperties({IpProperties.class})
@EnableScheduling
public class IpAutoConfiguration {
}
@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")
public void print(){
...
}
tools:
ip:
cycle: 1
package com.example.interceptor;
import com.example.service.IpCountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class IpCountInterceptor implements HandlerInterceptor {
@Autowired
private IpCountService ipCountService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ipCountService.count();
return true;
}
}
package com.example.config;
import com.example.interceptor.IpCountInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
}
@Bean
public IpCountInterceptor ipCountInterceptor(){
return new IpCountInterceptor();
}
}
开启yml提示功能
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
Maven重新安装
然后将target.classes.META-INF.spring-configuration-metadata.json文件拷贝到resources.META-INF下
去掉上一步骤添加的处理器坐标
进行自定义提示功能开发
修改spring-configuration-metadata.json中的"hints"
"hints": [
{
"name": "tools.ip.model",
"values": [
{
"value": "detail",
"description": "详细模式."
},
{
"value": "simple",
"description": "极简模式."
}
]
}
]
观看黑马程序员的springboot2原理篇视频p169-174,便于理解
视频网址(https://www.bilibili.com/video/BV15b4y1a7yG?p=174&spm_id_from=pageDriver&vd_source=50cc6e53411d643b31628740ae6c2b0a)
SpringBoot启动流程
初始化各种属性,加载成对象
读取环境属性(Environment)
系统配置(spring.factories)
参数(Arguments、application.properties)
创建Spring容器对象ApplicationContext,加载各种配置
在容器创建前,通过监听器机制,应对不同阶段加载数据、更新数据的需求
容器初始化过程中追加各种功能,例如统计时间、输出日志等
工作流程
Springboot30StartupApplication【10】->SpringApplication.run(Springboot30StartupApplication.class, args);
SpringApplication【1332】->return run(new Class>[] { primarySource }, args);
SpringApplication【1343】->return new SpringApplication(primarySources).run(args);
SpringApplication【1343】->SpringApplication(primarySources)
# 加载各种配置信息,初始化各种配置对象
SpringApplication【266】->this(null, primarySources);
SpringApplication【280】->public SpringApplication(ResourceLoader resourceLoader, Class>... primarySources)
SpringApplication【281】->this.resourceLoader = resourceLoader;
# 初始化资源加载器
SpringApplication【283】->this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
# 初始化配置类的类名信息(格式转换)
SpringApplication【284】->this.webApplicationType = WebApplicationType.deduceFromClasspath();
# 确认当前容器加载的类型
SpringApplication【285】->this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
# 获取系统配置引导信息
SpringApplication【286】->setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
# 获取ApplicationContextInitializer.class对应的实例
SpringApplication【287】->setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
# 初始化监听器,对初始化过程及运行过程进行干预
SpringApplication【288】->this.mainApplicationClass = deduceMainApplicationClass();
# 初始化了引导类类名信息,备用
SpringApplication【1343】->new SpringApplication(primarySources).run(args)
# 初始化容器,得到ApplicationContext对象
SpringApplication【323】->StopWatch stopWatch = new StopWatch();
# 设置计时器
SpringApplication【324】->stopWatch.start();
# 计时开始
SpringApplication【325】->DefaultBootstrapContext bootstrapContext = createBootstrapContext();
# 系统引导信息对应的上下文对象
SpringApplication【327】->configureHeadlessProperty();
# 模拟输入输出信号,避免出现因缺少外设导致的信号传输失败,进而引发错误(模拟显示器,键盘,鼠标...)
java.awt.headless=true
SpringApplication【328】->SpringApplicationRunListeners listeners = getRunListeners(args);
# 获取当前注册的所有监听器
SpringApplication【329】->listeners.starting(bootstrapContext, this.mainApplicationClass);
# 监听器执行了对应的操作步骤
SpringApplication【331】->ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
# 获取参数
SpringApplication【333】->ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
# 将前期读取的数据加载成了一个环境对象,用来描述信息
SpringApplication【333】->configureIgnoreBeanInfo(environment);
# 做了一个配置,备用
SpringApplication【334】->Banner printedBanner = printBanner(environment);
# 初始化logo
SpringApplication【335】->context = createApplicationContext();
# 创建容器对象,根据前期配置的容器类型进行判定并创建
SpringApplication【363】->context.setApplicationStartup(this.applicationStartup);
# 设置启动模式
SpringApplication【337】->prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
# 对容器进行设置,参数来源于前期的设定
SpringApplication【338】->refreshContext(context);
# 刷新容器环境
SpringApplication【339】->afterRefresh(context, applicationArguments);
# 刷新完毕后做后处理
SpringApplication【340】->stopWatch.stop();
# 计时结束
SpringApplication【341】->if (this.logStartupInfo) {
# 判定是否记录启动时间的日志
SpringApplication【342】-> new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
# 创建日志对应的对象,输出日志信息,包含启动时间
SpringApplication【344】->listeners.started(context);
# 监听器执行了对应的操作步骤
SpringApplication【345】->callRunners(context, applicationArguments);
# 调用运行器
SpringApplication【353】->listeners.running(context);
# 监听器执行了对应的操作步骤
监听器类型