本轮 SpringBoot2核心功能面试题库(带答案)
【面试题】1. SpringBoot自动配置中,@ConditionalOnClass和@ConditionalOnMissingBean的区别和使用场景分别是什么?
【标准答案】这两个都是条件注解,但作用层面完全不同。@ConditionalOnClass检查的是类路径上是否存在某个类,它决定的是"这个自动配置类要不要被解析",属于类级别的宏观判断。比如你在自动配置类上写@ConditionalOnClass(DataSource.class),意思是classpath里没有DataSource这个类,整个配置类就直接跳过了,连BeanDefinition都不会注册。@ConditionalOnMissingBean检查的是容器里是否已存在某个类型的Bean,它决定的是"这个Bean还要不要创建",属于Bean级别的微观判断。典型场景是自动配置类里定义默认Bean时加@ConditionalOnMissingBean,用户如果自己定义了同类型Bean,默认的就不创建,这就是"用户优先"原则。面试里可以这么总结:@ConditionalOnClass管"配不配置",@ConditionalOnMissingBean管"创不创建"。一个在类层面做排除,一个在Bean层面做兜底,两者经常配合使用,先判断有没有依赖(OnClass),再判断有没有自定义(OnMissingBean),保证自动配置既安全又不覆盖用户意图。
【面试题】2. SpringBoot的application.yml和application.properties同时存在时,加载优先级是怎样的?多Profile场景下配置覆盖规则是什么?
【标准答案】首先说文件格式,properties和yml如果同时存在且同名,properties优先级高于yml。然后说位置优先级,SpringBoot外部化配置有个核心原则:外部覆盖内部,先加载的被后加载的覆盖。优先级从高到低依次是:命令行参数 > JNDI属性 > Java系统属性(System.getProperties()) > 操作系统环境变量 > jar包外application-{profile}.yml > jar包内application-{profile}.yml > jar包外application.yml > jar包内application.yml。多Profile的覆盖规则是:特定Profile的配置覆盖默认Profile的配置。比如你同时有application.yml和application-dev.yml,激活了dev Profile,那dev里的配置会覆盖默认配置里的同名项。实际项目中我建议统一用一种格式(推荐yml),避免混用导致优先级混乱。面试时要强调的核心点:外>内、特定>默认、properties>yml,这三条规则把优先级问题就理清了。
【面试题】3. BeanFactory和ApplicationContext的区别是什么?SpringBoot启动时用的是哪个?
【标准答案】BeanFactory是Spring最底层的容器接口,提供了最基本的IOC功能:getBean、Bean定义注册、单例缓存这些。ApplicationContext是BeanFactory的子接口,在它基础上扩展了大量企业级功能:国际化(MessageSource)、事件发布(ApplicationEventPublisher)、资源加载(ResourceLoader)、AOP集成、环境抽象(Environment)等。关键区别在两点:第一,BeanFactory的Bean是懒加载的,调用getBean时才创建;ApplicationContext默认预加载,启动时就把所有单例Bean都初始化好了。第二,BeanFactory手动注册BeanPostProcessor不自动生效,ApplicationContext自动识别并注册。SpringBoot启动用的就是ApplicationContext,具体实现是AnnotationConfigServletWebServerApplicationContext(Web场景)或AnnotationConfigApplicationContext(非Web场景)。面试时可以这样回答:BeanFactory是"最小可用容器",ApplicationContext是"企业级容器",SpringBoot肯定用功能完整的那个。但底层ApplicationContext组合了一个DefaultListableBeanFactory作为实际干活的对象,所以本质上是ApplicationContext持有BeanFactory的引用来委派具体操作。
【面试题】4. BeanPostProcessor和BeanFactoryPostProcessor的执行时机和作用分别是什么?能否在BeanPostProcessor中修改BeanDefinition?
【标准答案】这两个接口名字很像但作用时机完全不同。BeanFactoryPostProcessor在Bean定义注册完成之后、Bean实例化之前执行,操作的是BeanDefinition,可以修改Bean的元数据,比如修改属性值、修改作用域等。经典应用是PropertyPlaceholderConfigurer,在Bean创建前把${}占位符替换成实际值。BeanPostProcessor在Bean实例化之后执行,分前后两个回调:postProcessBeforeInitialization在初始化方法(@PostConstruct、InitializingBean.afterPropertiesSet)之前调用,postProcessAfterInitialization在初始化方法之后调用。AOP代理就是在postProcessAfterInitialization里创建的。至于能不能在BeanPostProcessor里修改BeanDefinition——技术上你可以拿到BeanFactory然后修改,但不应该这么做。因为BeanPostProcessor执行时Bean已经实例化完成了,再改BeanDefinition对已创建的Bean无效,对后续创建的Bean又有时序问题,属于典型的反模式。面试的核心结论:BeanFactoryPostProcessor改定义(元数据层面),BeanPostProcessor改实例(对象层面),各司其职别越界。
【面试题】5. @Import的三种用法是什么?@EnableAutoConfiguration中的@Import是哪种?
【标准答案】@Import有三种导入方式。第一种:直接导入配置类,@Import(UserConfig.class),效果等同于在主配置类里直接写这个配置类,里面@Bean都会被注册。第二种:导入ImportSelector实现类,返回String数组指定要导入的配置类全限定名,可以实现动态选择导入哪些配置。第三种:导入ImportBeanDefinitionRegistrar实现类,直接操作BeanDefinitionRegistry手动注册BeanDefinition,最灵活,MyBatis的@MapperScan就是用的这种方式。@EnableAutoConfiguration里的@Import用的是第二种:ImportSelector,具体是AutoConfigurationImportSelector。它通过SpringFactoriesLoader从spring.factories或AutoConfiguration.imports文件中读取所有自动配置类的全限定名,然后返回给容器注册。但这里有个细节:AutoConfigurationImportSelector不是直接返回文件里所有类,而是先经过spring.boot.enableautoconfiguration等过滤,再通过@Conditional系列注解在注册时做条件过滤。面试总结:三种Import从简单到灵活依次递进,@EnableAutoConfiguration用的是ImportSelector方式实现动态加载。
【面试题】6. Spring AOP中JDK动态代理和CGLIB代理的区别是什么?SpringBoot默认用哪种?怎么切换?
【标准答案】JDK动态代理基于接口,要求目标类必须实现至少一个接口,通过java.lang.reflect.Proxy生成代理对象,代理类和目标类实现相同接口。CGLIB基于继承,通过生成目标类的子类来实现代理,不要求目标类实现接口,但不能代理final类和final方法。性能方面,JDK代理在JDK8之后调用效率已经很高,CGLIB创建代理较慢但首次调用后性能也不错。SpringBoot2.x默认使用CGLIB代理,这是通过spring.aop.proxy-target-class=true控制的。SpringBoot2.x把这个默认值改成了true(1.x默认是false用JDK代理),原因是大多数场景下业务类不一定实现接口,用CGLIB更通用,避免"因为没接口AOP就失效"的问题。切换方式:在application.yml中设置spring.aop.proxy-target-class=false就切回JDK代理,但前提是你的目标类必须实现接口,否则会报错。面试要点:JDK代理基于接口,CGLIB基于继承,Boot2默认CGLIB,通过proxy-target-class切换。
【面试题】7. Spring事件机制的发布顺序和监听顺序是否一致?@EventListener是同步还是异步?如何实现异步事件?
【标准答案】默认情况下,Spring事件是同步的,发布者在同一个线程里依次调用所有监听器。所以发布顺序和监听执行顺序是一致的——先注册的监听器先执行。如果多个@EventListener监听同一个事件,执行顺序可以通过@Order注解控制,数值越小优先级越高。@EventListener默认是同步的,发布者调用applicationEventPublisher.publishEvent()后,会阻塞等所有监听器执行完才返回。要实现异步事件,在@EventListener上再加@Async注解即可,这样监听器会在独立线程池中执行,发布者不阻塞。但要注意:用@Async的前提是启动类或配置类上要加@EnableAsync,否则@Async不生效。另外异步事件有个陷阱:如果异步监听器抛异常,发布者是感知不到的,异常会被线程池的UncaughtExceptionHandler吞掉或者打印日志,不会传播给发布者。还有一点,内置事件的触发顺序是:ContextRefreshedEvent(容器刷新完成)> ContextStartedEvent(调用start())> ContextClosedEvent(容器关闭),面试常考这个顺序。
【面试题】8. SpringBoot中@Transactional的propagation属性有哪些常见传播行为?REQUIRED和REQUIRES_NEW的核心区别是什么?
【标准答案】@Transactional的propagation常见传播行为有7种,面试重点掌握前4种。REQUIRED(默认):当前有事务就加入,没有就新建一个。SUPPORTS:有事务就加入,没有就以非事务执行。MANDATORY:必须在事务中调用,否则抛异常。REQUIRES_NEW:不管当前有没有事务,都新建一个独立事务,外层事务挂起。NOT_SUPPORTED:以非事务方式执行,如果当前有事务就挂起。NEVER:以非事务方式执行,如果当前有事务就抛异常。NESTED:如果当前有事务,就在嵌套事务中执行(savepoint),没有就新建。REQUIRED和REQUIRES_NEW的核心区别:REQUIRED是加入外层事务,内外层是同一个事务,任何一方回滚整个事务都回滚;REQUIRES_NEW是新建独立事务,内层事务独立提交或回滚,不受外层影响。典型场景:订单主流程用REQUIRED,操作日志用REQUIRES_NEW,即使订单回滚了日志也要保留。但要注意REQUIRES_NEW的性能开销——它会挂起外层事务再开新事务,涉及两次连接获取。面试时把REQUIRED的"同生共死"和REQUIRES_NEW的"各自独立"对比清楚就够了。
【面试题】9. SpringBoot Actuator的/health端点是如何工作的?如何自定义HealthIndicator?端点安全暴露策略怎么配置?
【标准答案】/health端点的核心机制是HealthIndicator接口。SpringBoot自动配置了多个内置HealthIndicator:DataSourceHealthIndicator检查数据库连接、DiskSpaceHealthIndicator检查磁盘空间、RedisHealthIndicator检查Redis连接等。当你访问/health时,框架遍历所有HealthIndicator,聚合它们的状态生成最终的健康状态。状态有UP、DOWN、OUT_OF_SERVICE、UNKNOWN四种,聚合规则是取最严重的——只要有一个DOWN,整体就是DOWN。自定义HealthIndicator很简单:实现HealthIndicator接口,在health()方法里做检查,返回Health.up().withDetail("key","value").build()或Health.down().build()即可。类上加@Component注解,名字默认取类名去掉HealthIndicator后缀。端点安全暴露策略通过management.endpoints.web.exposure.include和exclude配置。默认只暴露health和info,要暴露更多就设置include=*(暴露所有)或include=prometheus,metrics等指定端点。生产环境建议最小化暴露,敏感端点如env、beans、heapdump一定要exclude掉,或者配合Spring Security做端点访问控制。面试时强调两点:HealthIndicator是可扩展的检查点,暴露策略要遵循最小权限原则。
【面试题】10. spring-boot-maven-plugin的打包原理是什么?分层JAR结构是怎样的?Docker构建时如何利用分层特性优化镜像?
【标准答案】spring-boot-maven-plugin的repackage目标在Maven package之后执行,把普通JAR重新打包成可执行Fat JAR。Fat JAR结构分三层:BOOT-INF/classes/放项目编译后的class文件,BOOT-INF/lib/放所有依赖JAR,org/springframework/boot/loader/放SpringBoot自己的类加载器代码,MANIFEST.MF中指定Main-Class为JarLauncher。启动时JarLauncher先创建SpringBoot自定义类加载器LaunchedURLClassLoader,用它加载BOOT-INF下的类和依赖,再反射调用项目的main方法。分层JAR(Layered JAR)是SpringBoot 2.3+引入的特性,把Fat JAR内容按变更频率分层:dependencies(第三方依赖,最稳定)> spring-boot-loader > snapshot-dependencies > application(项目代码,最常变)。Docker构建时利用分层可以大幅减少镜像体积和构建时间:把dependencies层放在Dockerfile的前面,application层放后面。项目代码变了只重新构建application层,依赖层走Docker缓存。实际操作是在pom.xml中配置<layers><enabled>true</enabled></layers>,然后用java -Djarmode=layertools -jar app.jar extract命令解压出各层目录,在Dockerfile里每层一个COPY指令。面试核心:Fat JAR = 可执行壳 + 依赖 + 项目代码,分层JAR = 按变更频率拆层,Docker里利用缓存层只重建变化层。