Skip to content

本轮 SpringBoot2核心功能面试题库(带答案) ​

【面试题】1. SpringBoot中@ConditionalOnClass和@ConditionalOnMissingBean这两个条件注解的执行时机有何不同?在自动配置源码中各自承担什么角色? ​

【标准答案】这两个注解执行时机完全不同。@ConditionalOnClass是类路径条件判断,在BeanDefinition注册阶段就生效,它检查当前classpath下是否存在某个类——如果不存在,整个Configuration类都不会被解析,连BeanDefinition都不会注册。而@ConditionalOnMissingBean是Bean实例条件判断,发生在BeanDefinition注册之后、Bean实例化之前,它检查容器中是否已存在某个类型的Bean。在自动配置中,@ConditionalOnClass通常加在Configuration类上做第一层过滤,比如DataSourceAutoConfiguration要求classpath下有DataSource类,没有就直接跳过整个配置类,避免ClassNotFound异常;@ConditionalOnMissingBean加在@Bean方法上做第二层保护,比如容器中没有自定义的DataSource才创建默认的,让用户自定义的Bean优先。这就是经典的"类路径先过滤、Bean实例后兜底"的双层条件机制。

【面试题】2. SpringBoot的application.yml和application.properties加载顺序是怎样的?如果两者同时存在会怎样? ​

【标准答案】SpringBoot同时支持properties和yml两种格式,加载优先级是properties > yml。具体来说,如果在同一目录下同时存在application.properties和application.yml,properties中的属性会覆盖yml中的同名属性。但这个覆盖是按属性粒度来的,不是整个文件级别的覆盖——也就是说两个文件中不同的属性会合并共存,只有key冲突时properties的值才胜出。加载位置也有优先级:classpath根目录 > classpath的/config包 >项目根目录 > 项目根目录的/config目录,后面的优先级更高会覆盖前面的。实际开发中建议统一用一种格式,避免混用造成混乱。如果必须混用,要清楚properties优先的事实,团队里最好有明确约定。

【面试题】3. 请详细说明SpringBoot中@Bean方法的代理模式(Full模式与Lite模式),以及@Configuration(proxyBeanMethods=true)的作用和性能影响。 ​

【标准答案】这是SpringBoot面试的高频深挖点。@Configuration默认proxyBeanMethods=true,叫Full模式,Spring会为配置类生成CGLIB代理对象,保证同一个配置类中多个@Bean方法互相调用时返回的是同一个Bean实例——这就是"full"的含义,保证单语义完整。而@Component、@Import注册的配置类或proxyBeanMethods=false时是Lite模式,不会生成代理,@Bean方法互相调用就是普通Java方法调用,每次都new新对象,可能破坏单例。性能上,Full模式需要CGLIB生成代理类,启动时有额外开销,而且每个@Bean方法调用都有代理拦截。SpringBoot 2.x引入了@Bean方法间的依赖检测,如果配置类中没有@Bean方法间的互相调用,设置为Lite模式(proxyBeanMethods=false)可以显著加速启动。SpringBoot官方的自动配置类大量使用了proxyBeanMethods=false来优化启动性能。实际开发中,如果没有@Bean方法间依赖,就设为false。

【面试题】4. SpringBoot自定义Starter的开发规范是什么?命名有什么约定?spring.factories和AutoConfiguration.imports有什么区别? ​

【标准答案】自定义Starter开发有三条核心规范:第一,命名约定——官方Starter用spring-boot-starter-xxx格式(如spring-boot-starter-web),第三方自定义Starter用xxx-spring-boot-starter格式(如mybatis-spring-boot-starter),这个约定不能搞反。第二,结构规范——推荐创建两个模块:一个是autoconfigure模块放自动配置代码和spring.factories,另一个是starter模块仅做依赖聚合(把autoconfigure和相关第三方依赖打包在一起)。第三,配置注册——SpringBoot 2.7之前在autoconfigure模块的META-INF/spring.factories文件中写org.springframework.boot.autoconfigure.EnableAutoConfiguration=\xxxAutoConfiguration;SpringBoot 2.7+引入了新方式,在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中每行写一个AutoConfiguration全限定类名,新方式更简洁清晰,SpringBoot 3.x已经完全移除了spring.factories方式。两个文件同时存在时都会生效,但推荐新项目用imports方式。

【面试题】5. BeanFactoryPostProcessor和BeanPostProcessor的执行时机和用途有什么本质区别?分别举一个SpringBoot自动配置中的应用场景。 ​

【标准答案】这是Spring容器扩展机制的核心区别。BeanFactoryPostProcessor在BeanDefinition注册完成之后、Bean实例化之前执行,它操作的是BeanDefinition——可以修改Bean的属性值、作用域、甚至移除BeanDefinition。BeanPostProcessor在Bean实例化之后执行,分beforeInitialization和afterInitialization两个回调,它操作的是Bean实例本身。在SpringBoot自动配置中:BeanFactoryPostProcessor的典型应用是ConfigurationPropertiesBindingPostProcessor的注册阶段,它负责处理@ConfigurationProperties标注的Bean的属性绑定,在Bean还没实例化时就准备好绑定元数据;另一个例子是PropertySourceOrderingBeanFactoryPostProcessor,它调整外部化配置源的顺序。BeanPostProcessor的典型应用是AutowiredAnnotationBeanPostProcessor处理@Autowired注入,以及CommonAnnotationBeanPostProcessor处理@PostConstruct/@PreDestroy回调。简单记:BeanFactoryPostProcessor改定义,BeanPostProcessor改实例。

【面试题】6. SpringBoot中@Async异步任务的线程池如何配置?默认线程池有什么问题?如何自定义异步线程池并处理异常? ​

【标准答案】SpringBoot使用@Async默认用的是SimpleAsyncTaskExecutor——它不是真正的线程池,每次执行都新建线程,不复用,高并发下会创建大量线程导致OOM,这是必须替换的。配置方式:第一步,启动类或配置类加@EnableAsync;第二步,自定义线程池Bean——实现AsyncConfigurer接口重写getAsyncExecutor方法,或者直接定义一个ThreadPoolTaskExecutor的Bean并标注为@Primary。推荐配置:核心线程数=CPU核数,最大线程数=CPU核数*2,队列容量1000-2000,线程名前缀async-方便排查,拒绝策略用CallerRunsPolicy让提交线程自己跑防止任务丢失。异常处理是关键:@Async方法在独立线程执行,异常不会被调用方捕获。解决方案是实现AsyncUncaughtExceptionHandler,或者让异步方法返回CompletableFuture,调用方通过future.handle()或future.exceptionally()处理异常。也可以自定义AsyncConfigurer的getAsyncUncaughtExceptionHandler。

【面试题】7. SpringBoot中@Cacheable缓存的key生成策略是怎样的?如何自定义?#root对象有哪些可用属性? ​

【标准答案】@Cacheable默认使用SimpleKeyGenerator生成key——如果没有参数,key为空SimpleKey.EMPTY;一个参数直接用该参数作为key;多个参数用所有参数构造SimpleKey。需要注意的是,默认key生成不包含方法名,如果不同方法有相同参数会导致key冲突。自定义key有三种方式:第一种,用key属性配SpEL表达式,最常用,如@Cacheable(key="#userId")或@Cacheable(key="#user.id+'::'+#type");第二种,实现KeyGenerator接口注册为Bean,通过cacheManager指定;第三种,keyGenerator属性指定自定义KeyGenerator的Bean名。SpEL中#root对象可用属性包括:#root.methodName方法名、#root.method当前方法对象、#root.target被调用的目标对象、#root.targetClass目标类、#root.caches当前缓存集合、#root.args参数数组。实际开发中最常用的是#root.methodName拼接参数的方式,避免不同方法key冲突,比如key="#root.methodName + ':' + #id"。

【面试题】8. SpringBoot Actuator的/health端点的健康状态是怎么聚合的?如何自定义HealthIndicator?如何控制端点的暴露和安全策略? ​

【标准答案】/health端点的状态是聚合出来的:HealthAggregator(或ReactiveHealthAggregator)收集所有HealthIndicator的状态,按照状态优先级聚合为最终状态,优先级从高到低是DOWN > OUT_OF_SERVICE > UP > UNKNOWN。比如数据库连接DOWN了,即使磁盘UP,整体状态也是DOWN。自定义HealthIndicator两种方式:一是实现HealthIndicator接口,重写health()方法返回Health对象,如Health.down().withDetail("error","connection refused").build();二是继承AbstractHealthIndicator重写doHealthCheck方法,后者更规范,自动处理异常。端点暴露策略通过management.endpoints.web.exposure.include/exclude配置,默认只暴露health和info,生产环境按需开放,metrics、env等敏感端点不要随便暴露。安全策略推荐两层:网络层通过management.server.port设置独立管理端口,只在内网开放;应用层配合Spring Security,给Actuator端点配置角色权限。Prometheus集成需要额外引入micrometer-registry-prometheus依赖,开启prometheus端点,Grafana通过Prometheus数据源即可可视化监控。

【面试题】9. SpringBoot的分层JAR是什么?为什么需要分层?如何配合Docker构建优化镜像? ​

【标准答案】分层JAR是SpringBoot引入的一种打包格式,把JAR内容按变化频率分成多层:dependencies(第三方依赖,变化最少)、spring-boot-loader(启动加载器)、snapshot-dependencies(SNAPSHOT依赖)、application(项目自身代码,变化最频繁)。核心目的是配合Docker的分层缓存机制——Dockerfile每层构建时,如果该层内容没变就复用缓存。传统FatJAR是单层,改一行代码重新构建整个镜像,依赖层也要重传。分层JAR让Docker构建时先COPY依赖层(变化极少,几乎永远命中缓存),再COPY应用层(经常变),只重传变化的部分。使用方式:spring-boot-maven-plugin配置<layout>LAYERED_JAR</layout>,然后用java -Djarmode=layertools -jar app.jar extract命令解压出各层目录,Dockerfile中逐层COPY。Dockerfile示例:第一层COPY dependencies/,第二层COPY snapshot-dependencies/,第三层COPY spring-boot-loader/,第四层COPY application/。这样改业务代码重新构建时,前三层全部命中缓存,构建速度从分钟级降到秒级,镜像推送也只传输最后一层的增量。

【面试题】10. SpringBoot中@Transactional的传播行为REQUIRED和REQUIRES_NEW有什么区别?在同一个类中方法A调用方法B,B上的@Transactional会生效吗?为什么? ​

【标准答案】REQUIRED是默认传播行为——如果当前有事务就加入,没有就新建。REQUIRES_NEW是无论如何都新建事务——如果当前有事务,先挂起当前事务,开新事务,新事务完成后再恢复旧事务。两者的核心区别:REQUIRES_NEW会创建独立事务,内层事务回滚不影响外层;REQUIRED是同一个事务,任何位置回滚整个事务都回滚。第二个问题是经典陷阱——同类内A调用B,B上的@Transactional不会生效。原因:Spring事务基于AOP代理,@Transactional通过TransactionInterceptor在代理对象上织入增强。同类内部调用是this.B(),走的是目标对象本身而不是代理对象,增强逻辑根本不会执行。解决方案有三种:一是把B方法抽到另一个Service中,通过注入的代理对象调用;二是自己注入自己(@Autowired注入自身,注意循环依赖问题,用@Lazy解决);三是通过AopContext.currentProxy()获取当前代理对象再调用,需要开启@EnableAspectJAutoProxy(exposeProxy=true)。生产中最推荐第一种,最干净。