Skip to content

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

【面试题】1. @EnableAutoConfiguration的exclude属性和spring.autoconfigure.exclude配置有什么区别?分别在什么场景下使用? ​

【标准答案】两种方式都能排除自动配置类,但使用场景不同。

exclude属性:写在@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})里,是编译期确定的静态排除,IDE能直接提示类名,类型安全。适合你明确知道要排除哪些类、且这些类在classpath中存在的场景。

spring.autoconfigure.exclude:写在application.yml里,如spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,是运行时配置,用全限定类名字符串。适合需要根据环境动态排除、或者排除类不在classpath中(写了不报错)的场景。

核心区别:exclude属性要求排除的类必须存在,否则编译报错;yaml配置方式类不存在也不报错。实战中,如果排除逻辑跟环境无关、代码可控,优先用exclude属性,类型安全好维护;如果排除逻辑需要按环境切换(比如测试环境排除某些自动配置),就用yaml配置,灵活。

【面试题】2. SpringBoot配置文件中的松散绑定(Relaxed Binding)有哪些规则?在@ConfigurationProperties中如何正确使用? ​

【标准答案】松散绑定是SpringBoot对配置属性名的一种容错机制,允许配置文件中的key和Java Bean属性名不完全一致。

四大规则:

  1. 大小写不敏感:my-prop、MY_PROP、myProp都能绑定到myProp属性
  2. 短横线转驼峰:first-name → firstName,配置文件推荐用短横线格式(kebab-case)
  3. 下划线等价短横线:first_name 等价 first-name
  4. 大写转短横线:FIRST_NAME 等价 first-name

使用注意:

  • @ConfigurationProperties的prefix必须用kebab-case:@ConfigurationProperties(prefix = "my-service")
  • @Value不支持松散绑定,必须严格匹配,比如@Value("${my-service.name}")就只能匹配my-service.name
  • Map类型的key不参与松散绑定,只对简单属性和嵌套POJO生效
  • 推荐配置文件统一用kebab-case,Java属性用camelCase,这是SpringBoot官方风格

面试加分点:说出@Value不支持松散绑定,这个细节很多人忽略,面试官一听就知道你真用过。

【面试题】3. BeanPostProcessor和BeanFactoryPostProcessor的区别是什么?它们的执行时机和典型应用场景有哪些? ​

【标准答案】这是两个不同维度的扩展点:

BeanFactoryPostProcessor:

  • 执行时机:BeanDefinition注册完成后、Bean实例化之前
  • 操作对象:BeanDefinition(修改Bean的元数据定义)
  • 典型场景:PropertySourcesPlaceholderConfigurer解析${...}占位符、ConfigurationClassPostProcessor处理@Configuration类、修改Bean的scope或属性值

BeanPostProcessor:

  • 执行时机:Bean实例化之后,初始化前后(postProcessBeforeInitialization在@PostConstruct之前,postProcessAfterInitialization在@PostConstruct之后)
  • 操作对象:Bean实例本身(可以包装、代理、替换Bean)
  • 典型场景:AutowiredAnnotationBeanPostProcessor处理@Autowired注入、CommonAnnotationBeanPostProcessor处理@Resource、AOP的AbstractAutoProxyCreator创建代理对象

关键区别:BeanFactoryPostProcessor改的是"图纸"(BeanDefinition),BeanPostProcessor改的是"产品"(Bean实例)。前者只执行一次,后者每个Bean创建时都会走一遍。

面试陷阱:别把执行顺序说反了。完整顺序是:BeanDefinition注册 → BeanFactoryPostProcessor → Bean实例化 → BeanPostProcessor.before → @PostConstruct → InitializingBean.afterPropertiesSet → BeanPostProcessor.after。

【面试题】4. @Import注解的三种导入方式分别是什么?各自的应用场景和底层原理是什么? ​

【标准答案】@Import有三种用法,由ConfigurationClassParser解析:

1. 直接导入普通类:@Import(ServiceImpl.class)

  • 把普通类注册为Bean,等同于在配置类里写@Bean
  • 场景:导入没有加@Component的第三方类,或者想精确控制导入哪些类

2. 导入@Configuration配置类:@Import(AppConfig.class)

  • 导入一个配置类,其中所有@Bean方法都会被注册
  • 场景:组合多个配置类,比@ComponentScan更精确,不会扫描多余类

3. 导入ImportSelector/ImportBeanDefinitionRegistrar:

  • ImportSelector:实现selectImports()方法,返回要导入的类全限定名数组。场景:根据条件动态选择导入哪些配置,@EnableAutoConfiguration就是通过AutoConfigurationImportSelector实现的
  • ImportBeanDefinitionRegistrar:直接操作BeanDefinitionRegistry手动注册。场景:需要精确控制BeanDefinition的注册过程,比如MyBatis的@MapperScan用MapperScannerRegistrar扫描注册Mapper

底层原理:Spring在解析配置类时遇到@Import,根据导入类型走不同分支——普通类直接注册BeanDefinition,配置类递归解析,ImportSelector调selectImports拿结果再递归处理,ImportBeanDefinitionRegistrar直接回调registerBeanDefinitions。

面试加分:说出@EnableAutoConfiguration底层就是@Import(AutoConfigurationImportSelector.class),直接串联自动配置原理。

【面试题】5. Spring AOP在什么情况下用JDK动态代理?什么情况下用CGLIB代理?如何强制指定代理方式? ​

【标准答案】默认策略(SpringBoot2.x):

  • 如果目标类实现了接口 → 默认用JDK动态代理(但SpringBoot2.x默认spring.aop.proxy-target-class=true,所以实际默认都是CGLIB)
  • 如果目标类没实现接口 → 只能用CGLIB

两者区别:

  • JDK动态代理:基于接口,生成接口的实现类作为代理,目标类必须实现接口。只能代理接口方法
  • CGLIB代理:基于继承,生成目标类的子类作为代理,不能代理final类和final方法

强制指定方式:

  • @EnableAspectJAutoProxy(proxyTargetClass = true) → 强制CGLIB
  • @EnableAspectJAutoProxy(proxyTargetClass = false) → 有接口用JDK,没接口用CGLIB
  • spring.aop.proxy-target-class=true/false → 配置文件方式

实战建议:SpringBoot2.x默认就是CGLIB,保持默认就好。强制切JDK代理容易出现"代理对象转目标类型失败"的问题,因为JDK代理是实现接口不是继承目标类。如果遇到同类内部方法调用AOP不生效,那不是代理方式的问题,是自调用问题,需要用AopContext.currentProxy()或拆分到不同类。

【面试题】6. @EventListener和实现ApplicationListener接口有什么区别?@Async异步事件需要注意什么? ​

【标准答案】两种方式对比:

ApplicationListener<E>接口:

  • 需要实现onApplicationEvent方法,类型通过泛型指定
  • 监听器必须注册为Bean
  • 可以用@Order控制多个监听器的执行顺序

@EventListener注解:

  • 加在任意Bean的方法上,通过方法参数类型推断监听的事件类型
  • 支持SpEL条件过滤:@EventListener(condition = "#event.type == 'CREATE'")
  • 支持返回值自动发布:方法返回的对象会作为新事件继续发布
  • 更灵活、代码更简洁,Spring官方推荐

@Async异步事件注意事项:

  1. 必须配合@EnableAsync使用
  2. 异步事件在独立线程执行,主线程不等待,不影响主流程性能
  3. 异步事件没有事务上下文,事务不会传播到异步线程
  4. 异步事件执行失败不会影响发布者和其他监听器,但异常不会向上抛,需要自行处理
  5. 可以自定义线程池:@Async("eventExecutor")指定,避免用默认的SimpleAsyncTaskExecutor(不限制线程数)
  6. 异步事件的执行顺序不可控,不要依赖监听器之间的执行顺序

面试加分:说出事务不传播这一点,很多人踩坑——在异步事件里操作数据库以为还在同一个事务里,结果回滚不了。

【面试题】7. SpringBoot中拦截器(HandlerInterceptor)和过滤器(Filter)的执行顺序是什么?如何选择? ​

【标准答案】执行顺序:请求进来先过Filter,再过DispatcherServlet,再到Interceptor,最后到Controller。

完整链路:Filter链 → DispatcherServlet → Interceptor.preHandle → Controller → Interceptor.postHandle → 视图渲染 → Interceptor.afterCompletion → DispatcherServlet → Filter链

核心区别:

维度FilterInterceptor
规范Servlet规范Spring规范
作用范围所有请求(包括静态资源)只拦截Handler(Controller方法)
获取Spring Bean需要额外配置天然可以注入
细粒度只能做粗粒度拦截可以在preHandle/postHandle/afterCompletion三个节点处理
获取Handler信息拿不到可以拿到HandlerMethod,知道具体执行哪个方法

选择原则:

  • 需要拦截所有请求(含静态资源)、跨域CORS、字符编码 → Filter
  • 需要权限校验、日志记录、拿Controller方法信息 → Interceptor
  • 需要和Spring Bean深度交互 → Interceptor

实战中Filter注册用FilterRegistrationBean,Interceptor注册实现WebMvcConfigurer.addInterceptors()。

【面试题】8. @Transactional的7种传播行为分别是什么?最常用的是哪几个?NESTED和REQUIRES_NEW有什么区别? ​

【标准答案】7种传播行为:

  1. REQUIRED(默认):有事务就加入,没有就新建
  2. SUPPORTS:有事务就加入,没有就非事务执行
  3. MANDATORY:必须在事务中,否则抛异常
  4. REQUIRES_NEW:总是新建事务,挂起当前事务
  5. NOT_SUPPORTED:总是非事务执行,挂起当前事务
  6. NEVER:必须非事务执行,否则抛异常
  7. NESTED:有事务就在嵌套事务中执行(savepoint),没有就新建

最常用:REQUIRED(默认值,90%场景)和REQUIRES_NEW(需要独立事务回滚的场景)。

NESTED vs REQUIRES_NEW(面试高频考点):

  • NESTED:外层事务和内层事务是父子关系,内层回滚到savepoint不影响外层,但外层回滚会把内层一起回滚。共用同一个物理事务
  • REQUIRES_NEW:内层是完全独立的新事务,内层回滚不影响外层,外层回滚也不影响内层。两个物理事务

实战场景:下单主流程用REQUIRED,记录操作日志用REQUIRES_NEW(主流程回滚日志不能丢)。NESTED适合部分失败可以回滚、整体还能继续的场景。

注意点:NESTED依赖数据库savepoint,不是所有数据库都支持。MySQL支持,Oracle也支持,但某些场景下表现有差异。

【面试题】9. SpringBoot缓存抽象中@Cacheable的key生成策略是什么?如何自定义key?缓存穿透和雪崩怎么在代码层面应对? ​

【标准答案】默认key生成:如果没指定key,使用SimpleKeyGenerator:

  • 无参数:返回空SimpleKey
  • 一个参数:直接用该参数
  • 多个参数:返回包含所有参数的SimpleKey

自定义key三种方式:

  1. SpEL表达式(最常用):@Cacheable(value = "users", key = "#id"),支持#p0、#a0、#result、#root.methodName等
  2. 拼接表达式:key = "#user.id + ':' + #user.type"
  3. 自定义KeyGenerator:实现KeyGenerator接口,注册为Bean,在注解上指定keyGenerator = "myKeyGenerator"

缓存穿透应对:

  • 缓存空值:@Cacheable配合unless = "#result == null"不缓存null,但可以手动在方法内缓存空对象
  • 布隆过滤器:在缓存前加一层布隆过滤器拦截不存在的key

缓存雪崩应对:

  • 过期时间加随机偏移:@Cacheable不直接支持,需要在Redis层面设置TTL时加随机值
  • 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis),避免同时失效
  • 热点数据永不过期:逻辑过期而非物理过期,异步刷新

注意:@Cacheable的unless条件为true时不缓存,condition条件为false时整个缓存逻辑不生效,两者方向相反别搞混。

【面试题】10. SpringBoot Actuator端点的安全暴露策略是什么?如何自定义HealthIndicator?如何与Prometheus+Grafana集成监控? ​

【标准答案】端点暴露策略:

  • 默认只暴露/actuator/health和/actuator/info
  • 配置方式:management.endpoints.web.exposure.include=health,info,metrics,prometheus
  • 排除方式:management.endpoints.web.exposure.exclude=env,beans
  • 全部暴露(生产慎用):management.endpoints.web.exposure.include=*
  • 安全建议:通过management.server.port=8081把Actuator端口和应用端口隔离,配合Spring Security限制访问

自定义HealthIndicator:

java
@Component
public class CustomHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        // 检查自定义逻辑
        boolean ok = checkSomeService();
        if (ok) {
            return Health.up().withDetail("service", "正常运行").build();
        }
        return Health.down().withDetail("service", "异常").build();
    }
}

访问/actuator/health会自动聚合所有HealthIndicator的结果。

Prometheus+Grafana集成:

  1. 引入micrometer-registry-prometheus依赖
  2. 暴露prometheus端点:management.endpoints.web.exposure.include=prometheus
  3. 访问/actuator/prometheus获取Prometheus格式指标
  4. Prometheus配置scrape目标指向该端点
  5. Grafana导入JVM/SpringBoot仪表盘模板(如ID 4701)

核心链路:应用 → Micrometer埋点 → /actuator/prometheus → Prometheus拉取 → Grafana可视化。

面试加分:说出Micrometer是门面模式,后端可以无缝切换Prometheus/InfluxDB/Datadog,代码零改动。