本轮 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属性名不完全一致。
四大规则:
- 大小写不敏感:
my-prop、MY_PROP、myProp都能绑定到myProp属性 - 短横线转驼峰:
first-name→firstName,配置文件推荐用短横线格式(kebab-case) - 下划线等价短横线:
first_name等价first-name - 大写转短横线:
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,没接口用CGLIBspring.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异步事件注意事项:
- 必须配合
@EnableAsync使用 - 异步事件在独立线程执行,主线程不等待,不影响主流程性能
- 异步事件没有事务上下文,事务不会传播到异步线程
- 异步事件执行失败不会影响发布者和其他监听器,但异常不会向上抛,需要自行处理
- 可以自定义线程池:
@Async("eventExecutor")指定,避免用默认的SimpleAsyncTaskExecutor(不限制线程数) - 异步事件的执行顺序不可控,不要依赖监听器之间的执行顺序
面试加分:说出事务不传播这一点,很多人踩坑——在异步事件里操作数据库以为还在同一个事务里,结果回滚不了。
【面试题】7. SpringBoot中拦截器(HandlerInterceptor)和过滤器(Filter)的执行顺序是什么?如何选择?
【标准答案】执行顺序:请求进来先过Filter,再过DispatcherServlet,再到Interceptor,最后到Controller。
完整链路:Filter链 → DispatcherServlet → Interceptor.preHandle → Controller → Interceptor.postHandle → 视图渲染 → Interceptor.afterCompletion → DispatcherServlet → Filter链
核心区别:
| 维度 | Filter | Interceptor |
|---|---|---|
| 规范 | 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种传播行为:
- REQUIRED(默认):有事务就加入,没有就新建
- SUPPORTS:有事务就加入,没有就非事务执行
- MANDATORY:必须在事务中,否则抛异常
- REQUIRES_NEW:总是新建事务,挂起当前事务
- NOT_SUPPORTED:总是非事务执行,挂起当前事务
- NEVER:必须非事务执行,否则抛异常
- 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三种方式:
- SpEL表达式(最常用):
@Cacheable(value = "users", key = "#id"),支持#p0、#a0、#result、#root.methodName等 - 拼接表达式:
key = "#user.id + ':' + #user.type" - 自定义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:
@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集成:
- 引入
micrometer-registry-prometheus依赖 - 暴露
prometheus端点:management.endpoints.web.exposure.include=prometheus - 访问
/actuator/prometheus获取Prometheus格式指标 - Prometheus配置scrape目标指向该端点
- Grafana导入JVM/SpringBoot仪表盘模板(如ID 4701)
核心链路:应用 → Micrometer埋点 → /actuator/prometheus → Prometheus拉取 → Grafana可视化。
面试加分:说出Micrometer是门面模式,后端可以无缝切换Prometheus/InfluxDB/Datadog,代码零改动。