SpringBoot2核心功能面试题库
生成时间:2026-05-24 22:19
【面试题】1. SpringBoot中@ConditionalOnMissingBean和@ConditionalOnBean的区别是什么?在自定义Starter中如何正确使用?
【标准答案】这两个都是条件装配注解,但逻辑相反。@ConditionalOnBean表示容器中存在某个Bean时才生效,@ConditionalOnMissingBean表示容器中不存在某个Bean时才生效。
在自定义Starter中,@ConditionalOnMissingBean用得更多,这是核心设计模式——让用户可以覆盖默认配置。比如MyBatis的SqlSessionFactory自动配置类上标注@ConditionalOnMissingBean,用户自己定义了就用用户的,没定义就用Starter提供的默认实现。这就是"约定大于配置"的体现。
@ConditionalOnBean要慎用,容易产生Bean创建顺序依赖问题。比如A依赖B的Bean存在才创建,但B可能还没注册,A就跳过了,导致意想不到的结果。如果必须用,可以通过@AutoConfigureAfter指定自动配置类的顺序。
实际开发中,自定义Starter的自动配置类典型写法:默认实现标注@ConditionalOnMissingBean,用户想覆盖就自己定义一个同类型的Bean,自动配置就自动退场。
【面试题】2. SpringBoot的配置文件加载优先级是怎样的?多个配置源冲突时谁覆盖谁?
【标准答案】SpringBoot外部化配置有非常明确的优先级,从高到低核心几层:
- 命令行参数(--server.port=8081)——最高优先级
- JNDI属性
- Java系统属性(System.getProperties())
- 操作系统环境变量
- jar包外application-{profile}.yml(跟jar同目录/config或同目录)
- jar包内application-{profile}.yml(classpath:/config或classpath:/)
- jar包外application.yml
- jar包内application.yml
关键记忆口诀:命令行 > 环境变量 > 外部配置文件 > 内部配置文件 > 带Profile > 不带Profile。
实际场景:生产环境通过命令行参数覆盖默认值,比如--spring.datasource.url=xxx直接覆盖jar包内的数据源配置,不用改代码不用重新打包。这也是SpringBoot运维友好的核心设计。
还有一个容易忽略的点:@PropertySource加载的配置优先级低于以上所有,它只能算补充源。
【面试题】3. BeanFactory和ApplicationContext有什么区别?SpringBoot启动时用的是哪个?
【标准答案】BeanFactory是Spring最底层的容器接口,只提供最基本的IoC功能——getBean、依赖查找。ApplicationContext是BeanFactory的子接口,在它基础上扩展了国际化、事件发布、资源加载、AOP集成等企业级功能。
核心区别:
- BeanFactory懒加载Bean,调用getBean时才创建;ApplicationContext默认预加载,启动时就把所有单例Bean初始化
- ApplicationContext内置MessageSource、ResourceLoader、ApplicationEventPublisher等能力
- ApplicationContext自动注册BeanFactoryPostProcessor和BeanPostProcessor,BeanFactory需要手动注册
SpringBoot启动时用的是ApplicationContext的具体实现——AnnotationConfigServletWebServerApplicationContext(Web应用)或AnnotationConfigApplicationContext(非Web应用)。启动流程中会先创建BeanFactory(DefaultListableBeanFactory),然后把BeanDefinition注册进去,再执行BeanFactoryPostProcessor处理,最后由ApplicationContext统一管理Bean的完整生命周期。
面试加分点:可以说出BeanFactoryPostProcessor在BeanDefinition注册后、Bean实例化前执行,用来修改BeanDefinition;而BeanPostProcessor在Bean实例化后执行,用来增强Bean实例。
【面试题】4. @Import的三种用法是什么?分别在什么场景下使用?
【标准答案】@Import有三种用法,从简单到灵活:
第一种:直接导入普通类
@Import(UserService.class)直接把UserService注册为Bean,不需要UserService加@Component。适合导入第三方jar包里的类,没法加注解的场景。
第二种:导入ImportSelector实现类
@Import(MyImportSelector.class)
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
return new String[]{"com.example.ServiceA", "com.example.ServiceB"};
}
}根据条件动态决定导入哪些类。返回的是全限定类名数组,Spring会把这些类注册为Bean。@EnableAutoConfiguration就是这么干的——通过AutoConfigurationImportSelector读取spring.factories,动态导入所有自动配置类。
第三种:导入ImportBeanDefinitionRegistrar实现类
@Import(MyRegistrar.class)
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
RootBeanDefinition bd = new RootBeanDefinition(ServiceC.class);
registry.registerBeanDefinition("serviceC", bd);
}
}直接操作BeanDefinitionRegistry,可以精确控制BeanDefinition的注册过程,比如设置作用域、懒加载、构造参数等。MyBatis的@MapperScan底层就是用这种方式批量注册Mapper的BeanDefinition。
面试关键:说清楚三种方式的递进关系——直接导入最简单,ImportSelector支持动态选择,ImportBeanDefinitionRegistrar可以精细控制BeanDefinition注册过程。
【面试题】5. SpringBoot AOP中Around通知的执行流程是什么?多个切面的执行顺序怎么控制?
【标准答案】Around通知是功能最强大的通知类型,它包裹了整个方法执行过程,可以决定是否执行目标方法、何时执行、修改参数和返回值。
执行流程:
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 前置逻辑
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
log.info("方法耗时: {}ms", cost);
return result; // 可以修改返回值
}pjp.proceed()是关键——调用它才执行目标方法,不调用则目标方法不执行。还可以传参pjp.proceed(args)修改方法参数。
多切面执行顺序:
- SpringBoot 2.x默认使用CGLIB代理
- 多个切面按@Order值从小到大形成嵌套:Order小的在外层,先执行Before、后执行After
- 没有@Order则按切面类名字典序
比如切面A(@Order(1))和切面B(@Order(2)):
A.before → B.before → 目标方法 → B.afterReturning → A.afterReturning像洋葱模型,先进后出。
实际项目中用@Order(1)给切面排序,1的优先级最高、最外层。日志切面一般设最低优先级(Order值最大),包在最外面记录完整耗时;事务切面设较高优先级(Order值小),确保事务包裹在日志内部。
【面试题】6. SpringBoot的事件机制是怎样的?ContextRefreshedEvent在什么时机触发?有什么实际用途?
【标准答案】SpringBoot事件机制基于观察者模式,核心三要素:ApplicationEvent(事件)、ApplicationEventPublisher(发布者)、ApplicationListener/@EventListener(监听者)。
ContextRefreshedEvent在ApplicationContext刷新完成时触发,即所有BeanDefinition加载完毕、所有单例Bean初始化完成、所有BeanPostProcessor注册完毕之后。这是容器完全就绪的信号。
实际用途:
@Component
public class InitListener {
@EventListener(ContextRefreshedEvent.class)
public void onRefresh(ContextRefreshedEvent event) {
// 容器启动完成后执行初始化逻辑
// 比如预热缓存、加载配置到内存、启动定时任务
CacheManager.getInstance().warmUp();
}
}关键注意点:在SpringMVC中会有两个ApplicationContext(Root和Servlet),ContextRefreshedEvent可能触发两次。判断方式:event.getApplicationContext().getParent() == null表示Root容器。
SpringBoot内置的核心事件按顺序:ApplicationStartingEvent → ApplicationEnvironmentPreparedEvent → ApplicationContextInitializedEvent → ApplicationPreparedEvent → ContextRefreshedEvent → ServletWebServerInitializedEvent → ApplicationStartedEvent → ApplicationReadyEvent。
异步事件用法:@EventListener + @Async,事件处理在新线程执行不阻塞发布者。但要注意线程池配置和事务边界——异步监听器里拿不到发布者的事务上下文。
【面试题】7. SpringBoot如何切换嵌入式容器?Tomcat、Undertow、Jetty分别适合什么场景?
【标准答案】SpringBoot默认内嵌Tomcat,切换方式很简单——排除Tomcat依赖,引入对应容器的starter:
切Undertow:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>Jetty同理,换成spring-boot-starter-jetty。
选型建议:
- Tomcat:最成熟、社区最广、文档最多,一般场景首选。默认200个maxThreads,适合常规Web应用
- Undertow:JBoss出品,基于NIO,轻量高性能,内存占用小。适合高并发I/O密集型场景,微服务网关常用。不支持EL表达式(如果用JSP就别选它)
- Jetty:长连接支持好,架构灵活可嵌入,适合WebSocket场景和嵌入式设备。阿里开源的SOFA-Bolt默认用Jetty
容器调优核心参数(以Tomcat为例):
server:
tomcat:
max-threads: 500
min-spare-threads: 20
max-connections: 10000
accept-count: 200
connection-timeout: 5000面试加分:说清楚切换的本质是ServletWebServerFactory接口的不同实现,Tomcat→TomcatServletWebServerFactory,Undertow→UndertowServletWebServerFactory,自动配置类通过@ConditionalOnClass判断classpath下有哪个容器依赖就用哪个。
【面试题】8. @Transactional的传播行为有哪些?REQURES_NEW和NESTED有什么区别?实战中怎么选?
【标准答案】7种传播行为,实际开发中常用的就3-4个:
- REQUIRED(默认):当前有事务就加入,没有就新建。最常用,90%场景都用它
- REQUIRES_NEW:不管当前有没有事务,都新建一个独立事务,外层事务挂起。内外事务互不影响
- NESTED:当前有事务就在嵌套事务中执行(savepoint),没有就同REQUIRED。嵌套事务回滚不影响外层,但外层回滚会连带嵌套一起回滚
- SUPPORTS:有事务就加入,没有就非事务执行。适合查询方法
- NOT_SUPPORTED:非事务执行,如果当前有事务就挂起
- MANDATORY:必须在事务中调用,否则抛异常
- NEVER:不能在事务中调用,否则抛异常
REQUIRES_NEW vs NESTED核心区别: REQUIRES_NEW是完全独立的新事务,内层事务提交/回滚与外层无关——内层提交了,外层回滚,内层的数据不受影响(已提交)。NESTED是外层事务的子事务,共享同一个物理事务但用savepoint设置保存点——如果内层回滚到savepoint,外层继续;但如果外层回滚,内层也一起回滚。
实战选择:
- 主流程+独立日志记录 → REQUIRES_NEW(主流程回滚,日志不丢)
- 主流程+可降级的子操作 → NESTED(子操作失败回滚到保存点,主流程继续)
- 普通增删改 → REQUIRED(默认就行)
避坑:同一个类中方法A调方法B,B上的@Transactional不生效——因为没走代理,this调用绕过了AOP。解决方案:注入自身(@Lazy注入自己)、或者把B抽到另一个Service。
【面试题】9. SpringBoot缓存抽象@Cacheable的执行原理是什么?如何集成Redis实现分布式缓存?
【标准答案】@Cacheable的执行原理基于AOP——Spring为标注了缓存注解的方法生成代理,方法调用时先查缓存,命中直接返回不执行方法;未命中则执行方法,把返回值写入缓存。
核心注解:
- @Cacheable:查询时缓存,key默认是所有参数的hash
- @CachePut:更新缓存,方法一定执行,返回值刷入缓存
- @CacheEvict:删除缓存,allEntries=true清空整个缓存区
集成Redis步骤:
- 引入依赖:spring-boot-starter-data-redis
- 配置连接:spring.redis.host/port/password
- 启用缓存:@EnableCaching
- 直接用注解即可,SpringBoot自动配置RedisCacheManager
@Cacheable(value = "user", key = "#id")
public User getUser(Long id) { return userMapper.selectById(id); }
@CachePut(value = "user", key = "#user.id")
public User updateUser(User user) { userMapper.updateById(user); return user; }
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) { userMapper.deleteById(id); }高级配置要点:
- 序列化:默认JDK序列化(可读性差),建议换成Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer
- TTL:通过RedisCacheConfiguration设置entryTtl(Duration.ofMinutes(30))
- 缓存穿透:@Cacheable配合cache-null-values=true缓存空值
- 缓存一致性:更新时@CachePut,删除时@CacheEvict,复杂场景用Redisson的读写锁
原理层面:@EnableCaching导入CachingConfiguration,注册CacheInterceptor(BeanPostProcessor),对@Cacheable方法织入切面。CacheManager是核心接口,RedisCacheManager实现类通过RedisTemplate操作Redis。
【面试题】10. SpringBoot的分层JAR是什么?Docker镜像构建时如何利用分层优化?
【标准答案】SpringBoot默认打fat jar——所有依赖和类全打在一个JAR里。分层JAR(Layered JAR)是SpringBoot 2.3+引入的特性,把JAR内容按变更频率分成多层:
从下到上(变更频率从低到高):
- dependencies:第三方依赖jar(变更最少)
- spring-boot-loader:启动加载器
- snapshot-dependencies:SNAPSHOT版本依赖
- application:项目自己的class文件(变更最多)
Docker构建时利用分层的核心思想:变更频率低的层缓存复用,只重建变更频率高的层。
传统方式:改一行代码,整个镜像重新构建,几百MB的依赖层全丢了。
分层Dockerfile:
FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM openjdk:11-jre-slim
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]效果:只改了业务代码(application层),Docker构建时前3层全部命中缓存,只重建最后几KB的层,构建时间从分钟级降到秒级。
启用方式:spring-boot-maven-plugin配置<layers><enabled>true</enabled></layers>,SpringBoot 2.4+默认开启。
面试加分:提到Buildpacks(pack build命令),SpringBoot 2.4+支持直接用Maven生成分层镜像到Docker,不需要手写Dockerfile。