Skip to content

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外部化配置有非常明确的优先级,从高到低核心几层:

  1. 命令行参数(--server.port=8081)——最高优先级
  2. JNDI属性
  3. Java系统属性(System.getProperties())
  4. 操作系统环境变量
  5. jar包外application-{profile}.yml(跟jar同目录/config或同目录)
  6. jar包内application-{profile}.yml(classpath:/config或classpath:/)
  7. jar包外application.yml
  8. 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有三种用法,从简单到灵活:

第一种:直接导入普通类

java
@Import(UserService.class)

直接把UserService注册为Bean,不需要UserService加@Component。适合导入第三方jar包里的类,没法加注解的场景。

第二种:导入ImportSelector实现类

java
@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实现类

java
@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通知是功能最强大的通知类型,它包裹了整个方法执行过程,可以决定是否执行目标方法、何时执行、修改参数和返回值。

执行流程:

java
@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注册完毕之后。这是容器完全就绪的信号。

实际用途:

java
@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:

xml
<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为例):

yaml
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步骤:

  1. 引入依赖:spring-boot-starter-data-redis
  2. 配置连接:spring.redis.host/port/password
  3. 启用缓存:@EnableCaching
  4. 直接用注解即可,SpringBoot自动配置RedisCacheManager
java
@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内容按变更频率分成多层:

从下到上(变更频率从低到高):

  1. dependencies:第三方依赖jar(变更最少)
  2. spring-boot-loader:启动加载器
  3. snapshot-dependencies:SNAPSHOT版本依赖
  4. application:项目自己的class文件(变更最多)

Docker构建时利用分层的核心思想:变更频率低的层缓存复用,只重建变更频率高的层。

传统方式:改一行代码,整个镜像重新构建,几百MB的依赖层全丢了。

分层Dockerfile:

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。