本轮 SpringBoot2核心功能面试题库(带答案)
【面试题】1. SpringBoot自动配置的@Conditional条件注解有哪些?它们的执行时机和判断逻辑是什么?
【标准答案】SpringBoot提供了丰富的条件注解来实现按需装配,核心包括:
- 类条件:
@ConditionalOnClass(类路径存在指定类则生效)、@ConditionalOnMissingClass(类路径不存在指定类则生效) - Bean条件:
@ConditionalOnBean(容器中存在指定Bean则生效)、@ConditionalOnMissingBean(容器中不存在指定Bean则生效) - 属性条件:
@ConditionalOnProperty(配置属性满足条件则生效,支持havingValue、matchIfMissing) - 资源条件:
@ConditionalOnResource(类路径存在指定资源文件则生效) - Web应用条件:
@ConditionalOnWebApplication/@ConditionalOnNotWebApplication - 表达式条件:
@ConditionalOnExpression(SpEL表达式为true则生效)
执行时机:条件注解在配置类解析阶段(ConfigurationClassPostProcessor处理时)就会进行判断,而非Bean实例化阶段。判断逻辑由ConditionEvaluator实现,通过matches方法依次校验各条件。关键点是@ConditionalOnMissingBean会在当前配置类解析完成后再次检查,支持"后置处理"风格的条件判断,这也是为什么自定义配置可以覆盖自动配置的原因——用户的Bean先注册,自动配置的条件不满足就不创建。
【面试题】2. SpringBoot启动时如何加载自动配置类?spring.factories和imports文件有什么区别?
【标准答案】SpringBoot自动配置类的加载机制经历了版本演进:
- SpringBoot 2.7之前:通过
META-INF/spring.factories文件,键为org.springframework.boot.autoconfigure.EnableAutoConfiguration,值为自动配置类的全限定名列表 - SpringBoot 2.7开始:新增
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,每行一个自动配置类全限定名
核心流程:
@EnableAutoConfiguration注解导入AutoConfigurationImportSelector- 调用
getCandidateConfigurations()通过SpringFactoriesLoader.loadFactoryNames()或新版的ImportCandidates.load()扫描类路径 - 去重、排除
@EnableAutoConfiguration.exclude指定的类、过滤@Conditional条件 - 按
@AutoConfigureOrder/@Order排序后返回
两者区别:
spring.factories是Spring原有的SPI机制,支持多种键值对,SpringBoot复用它做自动配置imports文件是SpringBoot 2.7为自动配置专门设计的新方式,更轻量、更清晰- SpringBoot 3.x已废弃spring.factories方式的自动配置加载,统一使用imports文件
- 第三方Starter如果需要兼容SpringBoot 2.x和3.x,建议同时提供两种文件
【面试题】3. BeanFactory和ApplicationContext有什么区别?SpringBoot启动过程中它们是如何协作的?
【标准答案】这是Spring容器的核心分层设计问题:
BeanFactory(基础容器):
- 提供最基础的IoC功能:Bean定义注册、依赖查找、作用域管理
- 懒加载设计,获取Bean时才实例化
- 不支持AOP、事件、国际化等高级特性
- 核心实现:
DefaultListableBeanFactory
ApplicationContext(高级容器):
- 继承BeanFactory,同时实现
MessageSource(国际化)、ApplicationEventPublisher(事件发布)、ResourceLoader(资源加载)等接口 - 预加载设计,启动时实例化所有单例Bean
- 自动注册
BeanPostProcessor、支持注解驱动、AOP代理
SpringBoot启动协作流程:
- 创建
DefaultListableBeanFactory作为底层BeanFactory - 创建
AnnotationConfigServletWebServerApplicationContext包装它 - 注册主配置类(
@SpringBootApplication标注的类)的BeanDefinition - 执行
BeanFactoryPostProcessor处理配置类,扫描并注册所有BeanDefinition - 执行
BeanPostProcessor,创建Bean实例并处理AOP代理 - 刷新完成后,ApplicationContext持有完整的Bean工厂,对外提供服务
面试关键点:BeanFactory是"基础设施",ApplicationContext是"业务入口"。SpringBoot选择ApplicationContext作为启动入口,是因为Web应用需要完整的生命周期管理、事件机制和资源处理能力。
【面试题】4. BeanPostProcessor和BeanFactoryPostProcessor有什么区别?各自的应用场景是什么?
【标准答案】两者都是Spring容器扩展点,但作用阶段和目标不同:
BeanFactoryPostProcessor(Bean工厂后置处理器):
- 作用阶段:Bean定义加载完成后,Bean实例化之前
- 操作对象:
BeanDefinition(Bean的元数据) - 典型应用:修改Bean定义属性、注册新的BeanDefinition、解析占位符
- 经典案例:
PropertySourcesPlaceholderConfigurer解析${...}占位符、ConfigurationClassPostProcessor处理@Configuration注解
BeanPostProcessor(Bean后置处理器):
- 作用阶段:Bean实例化后,初始化前后
- 操作对象:Bean实例本身
- 核心方法:
postProcessBeforeInitialization(初始化前)、postProcessAfterInitialization(初始化后) - 典型应用:AOP代理创建、属性注入、注解处理
- 经典案例:
AutowiredAnnotationBeanPostProcessor处理@Autowired、AbstractAutoProxyCreator创建AOP代理
执行顺序:
1. BeanFactoryPostProcessor处理BeanDefinition
2. 实例化Bean(调用构造方法)
3. BeanPostProcessor.postProcessBeforeInitialization()
4. 执行初始化方法(@PostConstruct、InitializingBean)
5. BeanPostProcessor.postProcessAfterInitialization() ← AOP代理在此创建
6. Bean准备就绪面试话术:BeanFactoryPostProcessor改的是"图纸"(BeanDefinition),BeanPostProcessor改的是"产品"(Bean实例)。Spring的AOP代理就是在BeanPostProcessor阶段创建的,这也是为什么构造方法无法被AOP拦截的原因——那时候BeanPostProcessor还没介入。
【面试题】5. SpringBoot的配置文件加载顺序和优先级覆盖规则是怎样的?外部化配置有哪些来源?
【标准答案】SpringBoot配置加载遵循"外层覆盖内层、后加载覆盖先加载"的原则,优先级从高到低:
最高优先级(命令行/系统级):
- 命令行参数:
--server.port=8081 - JNDI属性:
java:comp/env - JVM系统属性:
System.getProperties() - 环境变量:
SERVER_PORT=8081
配置文件(按优先级): 5. jar包外部的application-{profile}.properties/yml 6. jar包内部的application-{profile}.properties/yml 7. jar包外部的application.properties/yml 8. jar包内部的application.properties/yml
低优先级(类路径): 9. @PropertySource注解指定的配置文件 10. 默认属性:SpringApplication.setDefaultProperties
关键规则:
- 高优先级配置覆盖低优先级配置,而非合并
- 相同属性:高优先级生效;不同属性:各自生效(互补)
- 配置文件位置:当前目录 > config子目录 > 类路径根目录 > 类路径config目录
- Profile激活:
spring.profiles.active可在任意层级设置,优先级同上
面试实战话术:"生产环境通常用环境变量或命令行参数覆盖配置文件中的数据库密码、API密钥等敏感信息,避免敏感配置打入镜像。配置中心如Nacos、Apollo的优先级在SpringBoot原生命名空间之上,通过EnvironmentPostProcessor注入。"
【面试题】6. @ConfigurationProperties绑定的松散绑定规则是什么?如何实现属性校验?
【标准答案】松散绑定(Relaxed Binding)是SpringBoot的配置映射特性,允许配置文件的属性名与Java字段名不完全一致:
支持的格式映射:
| 配置文件写法 | Java字段名 |
|---|---|
my.property-name | myPropertyName |
my.property_name | myPropertyName |
my.propertyName | myPropertyName |
MY_PROPERTY_NAME | myPropertyName |
适用范围:
@ConfigurationProperties标注的类- 仅支持
properties/yml文件,不支持@Value注解 - 不支持嵌套属性的松散绑定(如
my.nested-property对my.nestedProperty)
属性校验:结合javax.validation注解实现:
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank
private String name;
@Min(1) @Max(65535)
private int port = 8080;
@Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
private String adminEmail;
}校验生效需要:
- 类上添加
@Validated - 依赖
spring-boot-starter-validation - 在
@ConfigurationProperties类上直接校验,或在嵌套对象上添加@Valid
面试话术:松散绑定的核心价值是配置文件可以用kebab-case(推荐)或环境变量的UPPER_CASE,而Java代码保持camelCase规范。校验失败会抛出BindValidationException,SpringBoot启动时就会报错,避免了运行时才发现配置问题。
【面试题】7. SpringBoot的AOP默认使用什么代理方式?什么情况下会切换?
【标准答案】SpringBoot的AOP代理机制遵循以下规则:
默认行为:
- 目标类实现了接口 → JDK动态代理
- 目标类未实现接口 → CGLIB代理
强制指定代理方式:
@EnableAspectJAutoProxy(proxyTargetClass = true)→ 强制使用CGLIBspring.aop.proxy-target-class=true→ 配置文件方式(SpringBoot默认已开启)
SpringBoot的特殊处理: 从SpringBoot 2.x开始,默认配置spring.aop.proxy-target-class=true,即优先使用CGLIB。这意味着即使目标类实现了接口,也默认用CGLIB代理,与Spring Framework的默认行为不同。
两种代理方式对比:
| 特性 | JDK动态代理 | CGLIB代理 |
|---|---|---|
| 要求 | 必须实现接口 | 无要求 |
| 原理 | 实现接口,反射调用 | 继承目标类,方法重写 |
| 限制 | 无法代理类方法 | 无法代理final方法和类 |
| 性能 | 调用时反射开销 | 生成代理类快,调用快 |
| SpringBoot默认 | 被覆盖 | 默认使用 |
切换场景:
- 需要代理类本身的方法(而非接口方法)时,必须用CGLIB
- 目标类是final类或方法时,只能用JDK代理(如果有接口)
- 需要暴露
proxy对象给外部使用时,JDK代理更明确(接口类型)
面试话术:"SpringBoot选择CGLIB作为默认代理方式,是因为实际项目中很多类并没有接口,用CGLIB更通用。但要注意CGLIB无法代理final方法,这也是为什么Spring组件类的方法不建议加final的原因。"
【面试题】8. SpringBoot的事件机制是如何工作的?核心内置事件有哪些?它们的触发顺序是什么?
【标准答案】SpringBoot事件机制基于观察者模式,核心组件:
基础组件:
ApplicationEvent:事件基类,所有事件继承它ApplicationListener:事件监听器接口ApplicationEventPublisher:事件发布接口,ApplicationContext实现@EventListener:注解式监听器,更简洁
核心内置事件(按触发顺序):
ApplicationStartingEvent:启动开始,Environment和ApplicationContext还未创建ApplicationEnvironmentPreparedEvent:Environment准备完成ApplicationContextInitializedEvent:ApplicationContext初始化,BeanDefinition未加载ApplicationPreparedEvent:BeanDefinition加载完成,Bean未实例化ContextRefreshedEvent:容器刷新完成,所有Bean就绪ApplicationStartedEvent:应用启动完成,CommandLineRunner执行前ApplicationReadyEvent:应用完全就绪,可接收请求ContextClosedEvent:容器关闭时触发
使用方式:
@Component
public class MyListener {
@EventListener
public void onReady(ApplicationReadyEvent event) {
// 应用启动完成后的初始化逻辑
}
@EventListener
@Async // 异步事件处理
public void onCustomEvent(CustomEvent event) {
// 异步处理
}
}异步事件:配合@Async注解,需要启用@EnableAsync,事件监听在新线程执行,不影响主流程。
面试话术:"事件机制的核心价值是解耦,比如ApplicationReadyEvent适合做启动后的健康检查、缓存预热、服务注册等。ContextRefreshedEvent在SpringBoot启动过程中可能触发两次(主容器和MVC容器),如果只需要执行一次,要判断event.getApplicationContext().getParent() == null来区分根容器。"
【面试题】9. SpringBoot的事务传播行为有哪些?REQUIRED和REQUIRES_NEW有什么区别?
【标准答案】Spring事务的传播行为定义了事务方法相互调用时的行为规则,共7种:
常用传播行为:
REQUIRED(默认):有事务则加入,无则新建REQUIRES_NEW:总是新建,挂起当前事务NESTED:有事务则嵌套执行,无则新建SUPPORTS:有事务则加入,无则非事务执行NOT_SUPPORTED:非事务执行,挂起当前事务MANDATORY:必须在事务中,否则抛异常NEVER:不能在事务中,否则抛异常
REQUIRED vs REQUIRES_NEW 关键区别:
假设方法A调用方法B:
| 场景 | REQUIRED | REQUIRES_NEW |
|---|---|---|
| A有事务,B正常执行 | B加入A的事务 | B新建事务,A的事务挂起 |
| B抛出异常 | A和B都回滚 | B回滚,A可选择回滚或提交 |
| B成功,A失败 | A和B都回滚 | B已提交,不受A影响 |
NESTED vs REQUIRES_NEW:
NESTED:外层事务回滚时,内层也回滚;内层回滚时,外层可选择回滚或继续(savepoint机制)REQUIRES_NEW:内外事务完全独立
面试陷阱:
- 同一个类中方法互调,事务不生效(绕过了代理)
private方法加@Transactional无效(CGLIB无法代理)- 默认只回滚
RuntimeException和Error,检查异常需配置rollbackFor
面试话术:"REQUIRES_NEW的典型场景是日志记录,即使业务回滚,日志也要保存。NESTED适合部分失败不影响整体的场景,比如批量处理,某条失败只回滚那一条。但要注意NESTED依赖数据库的savepoint,MySQL支持,Oracle支持,但有些数据库不支持。"
【面试题】10. SpringBoot打包成可执行JAR的原理是什么?分层JAR有什么优势?
【标准答案】SpringBoot可执行JAR通过spring-boot-maven-plugin插件实现,核心原理:
传统JAR vs SpringBoot可执行JAR:
- 传统JAR:扁平结构,
META-INF/MANIFEST.MF指定Main-Class - SpringBoot JAR:嵌套结构,类库以JAR形式包含在
BOOT-INF/lib/,应用类在BOOT-INF/classes/
启动流程:
MANIFEST.MF指定Main-Class: org.springframework.boot.loader.JarLauncherJarLauncher加载BOOT-INF/lib/下所有依赖JAR- 创建自定义
ClassLoader(LaunchedURLClassLoader) - 反射调用应用主类的
main方法
分层JAR(Layered JAR): SpringBoot 2.3+支持分层构建,将JAR内容按变更频率分层:
Layer 0: dependencies(第三方依赖,变更最少)
Layer 1: spring-boot-loader(启动器)
Layer 2: snapshot-dependencies(SNAPSHOT依赖)
Layer 3: application(应用类,变更最频繁)Docker镜像优化:
FROM openjdk:17 as builder
WORKDIR app
COPY target/app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM openjdk:17
WORKDIR app
COPY --from=builder app/dependencies/ ./
COPY --from=builder app/spring-boot-loader/ ./
COPY --from=builder app/snapshot-dependencies/ ./
COPY --from=builder app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]优势:
- 依赖层不变时,Docker使用缓存,构建秒级
- 推送镜像时只推送变更的层(通常是application层)
- 结合CI/CD实现快速迭代
面试话术:"传统JAR打包后几十MB甚至上百MB,每次部署都要完整传输。分层JAR让变更部分可能只有几MB,配合Docker的层缓存,构建和部署速度提升10倍以上。SpringBoot 3.x的buildpacks更是原生支持分层,一条命令就能生成优化的镜像。"