SpringBoot2核心功能面试题库
生成时间:2026-05-21 22:07
【面试题】1. SpringBoot自动配置的"条件化装配"底层是怎么实现的?如果一个自动配置类上同时标注了@ConditionalOnClass和@ConditionalOnMissingBean,它们的判断顺序和短路机制是怎样的?
【标准答案】条件化装配的核心是Condition接口及其子接口ConfigurationCondition。SpringBoot在AutoConfigurationImportSelector筛选自动配置类时,会先通过FilteringSpringBootCondition做第一阶段过滤——此时只处理@ConditionalOnClass、@ConditionalOnProperty这类不依赖Bean容器的条件(即ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN阶段),因为类路径判断和配置属性判断不需要容器实例化就能完成。通过第一阶段的类才会被加入候选列表。第二阶段是容器刷新时ConfigurationClassPostProcessor解析每个自动配置类的@Bean方法,此时才执行@ConditionalOnMissingBean等依赖容器状态的条件(PARSE_CONFIGURATION阶段)。所以本质上是两阶段过滤:先过滤"要不要注册这个配置类",再过滤"这个Bean定义要不要生效"。短路机制:多个@Conditional注解是AND关系,任何一个返回false就整体跳过,Spring的ConditionEvaluator会遍历所有条件注解,遇到第一个不满足就立即返回false,不再判断后续条件。
【面试题】2. application.yml和application.properties同时存在时,SpringBoot如何处理?配置优先级覆盖的完整链路是什么?
【标准答案】同时存在时,properties优先级高于yml,两者的配置会合并,相同key以properties为准。但这个优先级在整个外部化配置体系里只是冰山一角。完整优先级从高到低:1)命令行参数(--server.port=8081);2)JNDI属性;3)Java系统属性(System.getProperties());4)操作系统环境变量;5)jar包外application-{profile}.yml/properties;6)jar包内application-{profile}.yml/properties;7)jar包外application.yml/properties;8)jar包内application.yml/properties;9)@PropertySource注解指定的配置;10)SpringApplication.setDefaultProperties设置的默认值。关键记忆口诀:命令行最高、外部覆盖内部、profile覆盖默认、properties覆盖yml。实际项目中用得最多的是:命令行参数 > 环境变量 > 外部配置文件 > 内部配置文件,部署时通过环境变量或命令行参数覆盖jar内默认值。
【面试题】3. BeanFactory和ApplicationContext到底有什么区别?为什么SpringBoot启动后拿到的容器是AnnotationConfigServletWebServerApplicationContext,而不是BeanFactory?
【标准答案】BeanFactory是Spring最底层的容器接口,只提供Bean的获取和基础DI功能,采用的是延迟初始化策略——调用getBean时才创建。ApplicationContext是BeanFactory的超集,额外集成了:1)国际化MessageSource;2)资源访问ResourceLoader;3)事件发布ApplicationEventPublisher;4)AOP集成(自动注册BeanPostProcessor);5)Environment集成;6)Bean定义自动检测(ComponentScan)。最关键的区别是ApplicationContext在refresh()时会预实例化所有单例Bean,而BeanFactory是懒加载的。SpringBoot用ApplicationContext是因为Web应用需要启动时就完成所有Bean的装配和依赖注入,嵌入式Servlet容器也要在refresh阶段启动,懒加载会导致运行时才发现配置错误。AnnotationConfigServletWebServerApplicationContext的继承链是:该类→ServletWebServerApplicationContext→GenericWebApplicationContext→GenericApplicationContext→AbstractApplicationContext,它同时具备注解配置驱动和Web服务器管理能力,这就是SpringBoot的选择。
【面试题】4. @Import的三种用法分别是什么?自定义Starter时应该用哪种,为什么?
【标准答案】@Import三种用法:1)直接导入配置类:@Import(RedisConfig.class),等价于把该类当配置类注册;2)导入ImportSelector实现类:返回String数组指定要导入的配置类全限定名,适合根据条件动态选择导入哪些配置;3)导入ImportBeanDefinitionRegistrar实现类:直接操作BeanDefinitionRegistry手动注册BeanDefinition,最灵活,可以控制Bean的名称、作用域、是否懒加载等元数据。自定义Starter推荐第三种——ImportBeanDefinitionRegistrar,原因:Starter的核心诉求是"引入依赖就自动装配,不需要任何配置",而spring.factories里指定的自动配置类通常配合@Conditional系列注解做条件判断,在Registrar里可以根据类路径是否存在某个类来决定是否注册Bean,而且可以直接编程式注册,避免@Bean方式的代理限制。实际上SpringBoot官方Starter大量使用的是第一种+条件注解的组合,因为简单场景够用;但涉及动态注册、多Bean批量注册时,ImportBeanDefinitionRegistrar才是正解。
【面试题】5. AOP中Around通知如果忘记调用ProceedingJoinPoint.proceed(),会发生什么?JDK动态代理和CGLIB代理在切面执行上的差异是什么?
【标准答案】忘记调用proceed()会导致目标方法完全不会执行,而且后续的所有通知也不会触发——直接短路。这是Around通知最常见的事故,因为它包裹了目标方法的执行,不调用proceed等于拦死了整条链路。更隐蔽的是:即使你调用了proceed(),如果你没有返回proceed()的结果,调用方拿到的就是null,这会导致NPE。所以Around的标准写法是Object result = joinPoint.proceed(); return result;。关于代理方式差异:JDK动态代理基于接口,代理对象和目标对象实现同一接口,切面织入在InvocationHandler.invoke()中;CGLIB基于继承,代理对象是目标类的子类,切面织入在MethodInterceptor.intercept()中。核心差异在于:1)JDK代理只能代理接口方法,目标类自身新增的方法不会被代理;2)CGLIB可以代理类的方法(包括非接口方法),但不能代理final方法和final类;3)CGLIB代理在Spring中存在"自调用失效"问题——目标类内部方法A调用方法B时,B上的切面不会触发,因为走的是this引用而非代理对象。SpringBoot2.x默认使用CGLIB(spring.aop.proxy-target-class默认true)。
【面试题】6. SpringBoot的事件机制中,ContextRefreshedEvent在什么时机触发?如果在@EventListener中处理这个事件来初始化数据,需要注意什么坑?
【标准答案】ContextRefreshedEvent在ApplicationContext的refresh()方法执行到最后一步finishRefresh()时触发,此时所有单例Bean已经实例化并完成依赖注入,所有BeanPostProcessor已执行完毕,BeanDefinition已全部解析注册。用它来初始化数据很常见,但有三个坑:1)父子容器问题——SpringMVC中存在Root ApplicationContext和Servlet WebApplicationContext两层容器,refresh时两个容器都会触发ContextRefreshedEvent,所以@EventListener方法会被调用两次。解法是判断event.getApplicationContext().getParent() == null来区分是否是根容器。2)事务还未就绪——虽然Bean已创建,但如果你的初始化逻辑依赖@Transactional,需要注意事务管理器的初始化顺序,极端情况下可能拿到未完成初始化的TransactionManager。3)异步事件陷阱——如果@EventListener标注了@Async,事件处理是异步的,主容器启动不会等待初始化完成,可能导致数据还没初始化完就已有请求进来。最佳实践:简单初始化用@EventListener同步处理并加父容器判断;复杂的初始化逻辑用ApplicationRunner或CommandLineRunner,它们在容器完全就绪后执行,更可靠。
【面试题】7. 嵌入式Tomcat和Undertow在生产环境怎么选?SpringBoot自动配置嵌入式容器的源码入口在哪里,启动流程是怎样的?
【标准答案】Tomcat vs Undertow选择:Tomcat是默认选项,生态成熟、文档丰富、对Servlet规范支持最完整,适合大多数Web应用;Undertow是JBoss出品的轻量级容器,基于NIO,非阻塞IO模型,高并发场景下内存占用更低、吞吐量更高,尤其适合微服务网关和长连接场景。性能对比:Undertow在纯IO吞吐上比Tomcat高20%-30%,但Tomcat在复杂Servlet Filter链和JSP支持上更完善。选型建议:常规CRUD应用用Tomcat;高并发短连接API服务用Undertow;需要WebSocket的看具体场景,两者都支持但Undertow的NIO模型更有优势。自动配置源码入口:ServletWebServerFactoryAutoConfiguration,它通过@Import导入了ServletWebServerFactoryConfiguration.EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow三个内部配置类,每个类上都有@ConditionalOnClass做类路径判断,classpath里有哪个就实例化哪个工厂。启动流程:SpringApplication.run()→createApplicationContext()→refreshContext()→AbstractApplicationContext.refresh()→onRefresh()(ServletWebServerApplicationContext重写)→createWebServer()→从容器获取ServletWebServerFactory Bean→调用factory.getWebServer()创建容器→启动容器。注意容器启动在Bean实例化之后但before refresh完成。
【面试题】8. @Transactional的 propagation = Propagation.REQUIRES_NEW 和 NESTED 有什么区别?什么场景下必须用REQUIRES_NEW而不能用NESTED?
【标准答案】核心区别:REQUIRES_NEW会挂起当前事务,开启一个全新的物理事务,内层事务独立提交或回滚,与外层事务完全隔离;NESTED是在当前事务中创建一个保存点(Savepoint),内层回滚只是回滚到保存点,外层仍然可以决定提交还是回滚,本质是同一个物理事务中的逻辑子事务。三个关键差异:1)回滚隔离性不同——REQUIRES_NEW的内层回滚不影响外层(除非内层抛出的异常被外层catch后又抛出),NESTED的内层回滚后外层仍可继续执行并提交;2)外层回滚的影响不同——REQUIRES_NEW的外层回滚不影响已提交的内层事务,NESTED的外层回滚会把内层一起回滚(因为是同一物理事务);3)资源消耗不同——REQUIRES_NEW需要两个独立的数据库连接,NESTED只用一个。必须用REQUIRES_NEW的场景:日志记录/审计追踪——无论业务事务成功还是失败,审计日志必须持久化。比如用户操作日志,业务回滚了但日志不能丢,这时候用NESTED就会跟着业务一起回滚,只有REQUIRES_NEW能保证日志独立提交。另一个场景是限流计数器扣减——扣减操作不能因为业务失败而回滚。
【面试题】9. @Cacheable的key属性 spel表达式和keyGenerator可以同时指定吗?缓存穿透场景下,@Cacheable缓存null值有什么坑,怎么解决?
【标准答案】不能同时指定。Spring源码中CacheOperation.buildKey()的逻辑是:如果key不为空就用SpEL解析key,否则才走keyGenerator。同时指定的话key优先,keyGenerator被忽略,不会报错但容易误导。缓存null值的问题:@Cacheable默认不缓存null结果,每次查询不存在的数据都会穿透到数据库。解决方式是指定unless = "#result == null"的反向条件配合sync = true——但这是不对的,正确做法是用@Cacheable(value="user", key="#id", unless="#result == null")只缓存非null值,或者用@Cacheable(value="user", key="#id")配合@CachePut(value="user", key="#id", unless="#result == null")的组合。实际防穿透的标准方案:1)缓存空对象——@Cacheable(value="user", key="#id"),查询返回null时也缓存,但设置较短的TTL(如5分钟),数据库新增数据后删除该空缓存;2)布隆过滤器——在缓存层前加一层布隆过滤器,不存在的key直接拦截,不进缓存也不查库,适合key空间有限且查询密集的场景;3)Spring Cache的sync=true参数——在缓存未命中时只允许一个线程去查库,其他线程等待结果,防止缓存击穿(注意这是防击穿不是防穿透)。坑点:缓存null值如果不设TTL,数据库已新增数据但缓存还是null,会一直返回空,必须配合缓存更新策略。
【面试题】10. SpringBoot Actuator的端点默认暴露了哪些?生产环境如何安全地暴露监控端点并集成Prometheus?
【标准答案】SpringBoot2.x Actuator默认只暴露/actuator/health和/actator/info两个端点,其他所有端点(metrics、env、beans、heapdump等)默认关闭,这是安全考量——/env会暴露配置信息(含数据库密码),/heapdump可以下载堆转储,泄露出去是灾难。生产环境安全暴露方案:1)在application.yml中配置management.endpoints.web.exposure.include=health,info,prometheus,metrics,只暴露需要的端点,绝不暴露全部;2)配置management.endpoints.web.base-path=/actuator(默认值),然后通过Nginx或网关做路径过滤,只允许内网IP访问/actuator/**;3)敏感端点加management.endpoint.env.show-values=never防止配置值泄露。Prometheus集成步骤:1)引入micrometer-registry-prometheus依赖;2)配置暴露prometheus端点:management.endpoints.web.exposure.include=prometheus;3)prometheus端点会自动暴露在/actuator/prometheus,输出Prometheus格式的指标数据;4)在prometheus.yml中配置scrape任务:targets: ['app-host:8080'],metrics_path: '/actuator/prometheus';5)Grafana导入JVM Micrometer仪表盘(推荐4701号模板),即可看到线程池、GC、内存、HTTP请求等核心指标。关键提醒:/health端点要配置management.endpoint.health.show-details=when-authorized,默认只显示status,不暴露磁盘空间、数据源连接等细节。