什么是 Spring Boot?

Spring Boot 是一个简化 Spring 应用程序开发的框架。它的主要目标是减少 Spring 应用程序的配置和开发复杂性,使我们能够更快地构建、测试和部署 Spring 应用。

简单来说它通过提供默认配置自动化配置嵌入式服务器等功能,简化了传统 Spring 应用的繁琐配置过程。

扩展知识

1)自动配置(Auto-Configuration):

Spring Boot 会根据项目中的依赖和配置自动配置 Spring 应用程序,无需手动编写大量的配置代码。可以通过 @EnableAutoConfiguration 注解和自动配置类(@Configuration)即可实现。

2)内嵌 web 服务器:

Spring Boot 支持嵌入式的 Web 服务器,如 Tomcat、Jetty 和 Undertow,使应用程序无需外部部署服务器,简化了开发和部署流程。

3)简化的依赖管理:

Spring Boot 提供了非常多的 starter 依赖,用于简化常见功能的依赖管理。例如 spring-boot-starter-web 包含了开发 Web 应用所需的常见依赖。

4)Spring Boot CLI:

Spring Boot CLI 是一个命令行工具,用于快速创建和运行 Spring Boot 应用程序。

5)Spring Boot Actuator:

提供了应用程序的监控和管理功能。包括健康检查、度量信息、环境信息等,方便对应用进行监控和管理。

6)Spring Boot DevTools:

是一个开发工具,提供热重启、LiveReload 和其他开发环境下的便利功能,可提升开发效率。

Spring Boot 的核心特性有哪些?

1)开箱即用,内嵌服务器。这个特点是程序员最直观的感受,相较于原本的开发,spring boot 可以省略以前繁琐的 tomcat 配置,快速创建一个 web 容器。

2)自动化配置。在 spring boot 中我们可以按照自动配置的规定(将自动加载的 bean 写在自己 jar 包当中的 meta/info/spring.factories 文件中或者通过的注解 @Import 导入时加载指定的类)这样我们的配置类就会被 springboot 自动加载到容器当中。 同时还支持通过改写yaml 和 propreties来覆盖默认配置

3)支持 jar 包运行。传统部署web 容器都是打成 war 包放在 tomcat 中。spring boot 可以打成 jar 包只要有 java 运行环境即可运行 web 容器。

4)完整的生态支持。springboot 可以随意整合 spring 全家桶的支持。像 Actuator 健康检查模块,Spring Data JPA 数据库模块,Spring Test 测试模块。这些都可以很优雅的集成在 springboot 当中

扩展知识

面试时可以跟面试官说说自己是如何实现一个 spring boot starter 来帮助项目简化某些通用的业务的。

比如现在项目中有很多列表接口需要导出 excel。我利用自动化配置写了一个 excel 导出模块。

实现思路如下:

1)写一个 @ExcelExport 注解,放在列表接口上。利用切面来切列表的返回结果,拿到列表结果后写入 excel 当中。

2)然后写一个配置类放在 meta/info/spring.factories 文件下。这样让 spring boot 自动装配我们这个写 excel 文件的处理类。

3)在整个项目中有需要列表导出 excel 的地方,仅需把注解写在列表接口上即可。

通过这个例子让面试官相信你有实际使用自动化装配的经验。

Spring Boot 是如何通过 main 方法启动 web 项目的?

Spring Boot 应用的启动流程都封装在 SpringApplication.run 方法中,它的大部分逻辑都是复用 Spring 启动的流程,只不过在它的基础上做了大量的扩展。

在启动的过程中有一个刷新上下文的动作,这个方法内会触发 webServer 的创建,此时就会创建并启动内嵌的 web 服务,默认的 web 服务就是 tomcat。

分析

SpringApplication.run 方法实际上会触发 refreshContext 方法

最终会调用到 ServletWebServerApplicationContext.onRefresh 方法,这个方法内会调用 createWebServer

createWebServer 从名字就能理解这个方法是干嘛的,可以看到内部通过一个 ServletWebServerFactory 获取 webServer

默认的 WebServer 是 tomcat,可以看到源码里就是直接 new 了一个 Tomcat,且包装成一个 WebServer:

实际上这个 getTomcatWebServer 会 new 一个 WebServer:

而 new WebServer 会触发 initialize 方法,这个方法内就会触发 tomcat.start:

这个时候 tomcat 就被启动了,web 服务就起来了!

这里再扩展下,实际上有 5 种 webServer,默认用的是 TomcatWebServer,还有 jetty、undertow 等。

Spring Boot 中 application.properties 和 application.yml 的区别是什么?

它们两者的区别就在于书写格式,对配置而言效果是一样的,就是个人偏好问题。

application.properties 使用键值对配置,键和值之间用等号或冒号分隔

application.yml 使用 YAML (YAML Ain’t Markup Language)格式,具有层级结构,使用缩进表示嵌套关系。适合复杂配置,阅读性更佳。

如何在 Spring Boot 中定义和读取自定义配置?

简单来看可以有三种:

1)使用 @Value 注解:

@Value("${my.custom.property}")
private String myProperty;

2)使用 @ConfigurationProperties 注解:

@Component
@ConfigurationProperties(prefix = "my.custom")
public class MyCustomProperties {
private String property;
}

3)使用 Environment 接口:

@Autowired
private Environment env;

public void someMethod() {
String value = env.getProperty("my.custom.property");
}

Spring Boot 配置文件加载优先级你知道吗?

简单优先级:

命令行参数 > JAR包外面的 application-{profile}.properties > JAR包内的 application-{profile}.properties > JAR包外的 application.properties > JAR包内的 application.properties

注意:

application.propertiesapplication.yml 同时存在,同样的参数,最终生效的是 application.properties 中的配置。

扩展 bootstrap 和 application 配置文件知识:

boostrap 由父 ApplicationContext 加载,比 applicaton 优先加载,且它里面的内容不会被覆盖!

比如一些配置中心的配置,需要填写在 boostrap 中,父上下文先去配置中心获取额外的一些配置,然后再启动 SpringBoot 上下文。

SpringBoot 的启动流程?

启动类

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

@SpringBootApplication

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

这些注解虽然看起来很多,但是除去元注解,真正起作用的注解只有以下三个注解:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

@SpringBootConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration

@SpringBootConfiguration继承自@Configuration,二者功能也一致,标注当前类是配置类, 并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中,并且实例名就是方法名。

@ComponentScan

@ComponentScan 的作用就是扫描当前包以及子包,将有@Component,@Controller,@Service,@Repository等注解的类注册到容器中,以便调用。

注:如果@ComponentScan不指定basePackages,那么默认扫描当前包以及其子包,而@SpringBootApplication里的@ComponentScan就是默认扫描,所以我们一般都是把springboot启动类放在最外层,以便扫描所有的类。

@EnableAutoConfiguration

这里先总结下@EnableAutoConfiguration的工作原理,大家后面看的应该会更清晰: 它主要就是通过内部的方法,扫描classpath的META-INF/spring.factories配置文件(key-value),将其中的 org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项实例化并且注册到spring容器。

ok,我们同样打开@EnableAutoConfiguration源码,可以发现他是由以下几个注解组成的:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)

除去元注解,主要注解就是@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)。我们springboot项目为什么可以自动载入应用程序所需的bean?就是因为这个神奇的注解@Import。那么这个@Import怎么这么牛皮?没关系!我们一步一步的看下去!

首先我们先进入AutoConfigurationImportSelector类,可以看到他有一个方法selectImports()

继续跟踪,进入getAutoConfigurationEntry()方法,可以看到这里有个List集合,那这个List集合又是干嘛的?没事,我们继续跟踪getCandidateConfigurations()方法!

可以看到这里有个方法,这个方法的作用就是读取classpath下的META-INF/spring.factories文件的配置,将key为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项读取出来,通过反射机制实例化为配置文件,然后注入spring容器。

注:假如你想要实例化一堆bean,可以通过配置文件先将这些bean实例化到容器,等其他项目调用时,在spring.factories中写入这个配置文件的路径即可!

小结

SpringApplication.run()

SpringApplication创建

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//获取应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//获取所有初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//获取所有监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//定位main方法
this.mainApplicationClass = deduceMainApplicationClass();
}

获取应用类型

跟踪deduceFromClasspath方法:

从返回结果我们可以看出应用类型一共有三种,分别是

  • NONE: 非web应用,即不会启动服务器
  • SERVLET: 基于servlet的web应用
  • REACTIVE: 响应式web应用(暂未接触过)

判断一共涉及四个常量:

  • WEBFLUX_INDICATOR_CLASS
  • WEBMVC_INDICATOR_CLASS
  • JERSEY_INDICATOR_CLASS
  • SERVLET_INDICATOR_CLASSES

springboot在初始化容器的时候,会对以上四个常量所对应的class进行判断,看看他们是否存在,从而返回应用类型!

获取初始化器

跟踪进入getSpringFactoriesInstances方法:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
//获取所有初始化器的名称集合
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
//根据名称集合实例化这些初始化器
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
//排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

从代码可以看出是在META-INF/spring.factories配置文件里获取初始化器,然后实例化、排序后再设置到initializers属性中

获取初监听器

监听器和初始化的操作是基本一样的

定位main方法

跟踪源码进入deduceMainApplicationClass方法:

private Class<?> deduceMainApplicationClass() {
try {
//通过创建运行时异常的方式获取栈
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
//遍历获取main方法所在的类并且返回
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}

创建小结

创建了SpringApplication实例之后,就完成了SpringApplication类的初始化工作,这个实例里包括监听器、初始化器,项目应用类型,启动类集合,类加载器。如图所示。

run()方法

public ConfigurableApplicationContext run(String... args) {
//1、创建并启动计时监控类
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//2、初始化应用上下文和异常报告集合
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//3、设置系统属性“java.awt.headless”的值,默认为true,用于运行headless服务器,进行简单的图像处理,多用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如jconsole 需要将该值设置为true
configureHeadlessProperty();
//4、创建所有spring运行监听器并发布应用启动事件,简单说的话就是获取SpringApplicationRunListener类型的实例(EventPublishingRunListener对象),并封装进SpringApplicationRunListeners对象,然后返回这个SpringApplicationRunListeners对象。说的再简单点,getRunListeners就是准备好了运行时监听器EventPublishingRunListener。
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
//5、初始化默认应用参数类
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//6、根据运行监听器和应用参数来准备spring环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
//将要忽略的bean的参数打开
configureIgnoreBeanInfo(environment);
//7、创建banner打印类
Banner printedBanner = printBanner(environment);
//8、创建应用上下文,可以理解为创建一个容器
context = createApplicationContext();
//9、准备异常报告器,用来支持报告关于启动的错误
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//10、准备应用上下文,该步骤包含一个非常关键的操作,将启动类注入容器,为后续开启自动化提供基础
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//11、刷新应用上下文
refreshContext(context);
//12、应用上下文刷新后置处理,做一些扩展功能
afterRefresh(context, applicationArguments);
//13、停止计时监控类
stopWatch.stop();
//14、输出日志记录执行主类名、时间信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//15、发布应用上下文启动监听事件
listeners.started(context);
//16、执行所有的Runner运行器
callRunners(context, applicationArguments);
}catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
//17、发布应用上下文就绪事件
listeners.running(context);
}catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
//18、返回应用上下文
return context;
}

如何在 SpringBoot 启动时执行特定代码?有哪些方式?

常见一共有六种方式:

1、实现 CommandLineRunner 接口

CommandLineRunner 接口用于在 Spring Boot 应用启动完成后,执行特定的代码逻辑。可以有多个实现类,按照 @Order 注解的顺序执行。

@Component
@Order(1)
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("Application started!");
}
}

2、实现 ApplicationRunner 接口

ApplicationRunner 接口与 CommandLineRunner 类似,但可以接受和处理 ApplicationArguments 对象。

ApplicationArguments 其实就是获取应用程序启动参数。

例如 idea 可以在这里配置启动参数:

@Component
@Order(2)
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner: started with arguments: " + args);
}
}

3、使用 @PostConstruct 注解

Spring 容器初始化 bean 后,会执行初始化方法。这个注解适用于需要在 bean 初始化后立即执行的代码。

@Component
public class MyPostConstructBean {
@PostConstruct
public void init() {
System.out.println("PostConstruct: Bean initialized!");
}
}

4、使用 InitializingBean 接口

InitializingBean 接口提供了 afterPropertiesSet 方法,用于在 Spring 容器初始化 bean 的属性后,执行特定的初始化逻辑。

@Component
public class MyInitializingBean implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean: Properties set!");
}
}

5、使用 Spring 事件监听器

可以通过监听 Spring 的 ContextRefreshedEvent 期事件,在应用启动时执行特定代码。

@Component
public class MyEventListener {
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("EventListener: Context refreshed!");
}
}

6、自定义 BeanFactoryPostProcessor 和 BeanPostProcessor

它们都是 Spring 容器启动给的扩展点,可以在 Spring 容器初始化 bean 之前或之后执行特定逻辑。

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// bean 初始化前
if (bean instanceof MySpecificBean) {
System.out.println("BeanPostProcessor: Before initialization of " + mianshiya.com);
}
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// bean 初始化后
if (bean instanceof MySpecificBean) {
System.out.println("BeanPostProcessor: After initialization of " + mianshiya.com);
}
return bean;
}
}

Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?

Spring Boot 打成的 JAR 包不仅包含了应用程序的源代码和依赖库,还包含了程序运行需要的配置、脚本和服务依赖(内嵌的服务器如 Tomcat、Jetty、Undertow),可以直接部署运行。

普通的 JAR 包只有源代码和一些依赖,通常需要外部服务器或容器来运行。

参考链接

SpringBoot启动都做了什么?看完就懂了! - 知乎 (zhihu.com)

Spring Boot:最全SpringBoot启动流程原理分析(全网最全最完善)-腾讯云开发者社区-腾讯云 (tencent.com)