Spring Boot 自动配置机制
1. 自动配置是如何工作的?
Spring Boot 自动配置的核心在于 SPI 机制与条件注解的配合。启动时,@SpringBootApplication 包含 @EnableAutoConfiguration,它会扫描所有 JAR 包中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.7+)或旧版的 spring.factories。
读取到 EnableAutoConfiguration 对应的全限定类名后,Spring 容器会评估每个配置类上的 @ConditionalOn... 注解(如 @ConditionalOnClass)。只有满足条件的配置类才会生效,并通过 @Bean 方法向容器注册组件。这里有个关键点:用户自定义的 Bean 优先于自动配置,因为自动配置通常带有 @ConditionalOnMissingBean。如果需要控制顺序,可以使用 @AutoConfigureBefore/After。
Spring Boot 启动流程
2. 完整启动流程是怎样的?
从 main() 到 Web 容器启动,关键步骤如下:
- 执行 SpringApplication.run():创建实例并推断应用类型(SERVLET/REACTIVE/NONE)。
- 初始化监听器和初始器:加载
spring.factories中注册的ApplicationContextInitializer和ApplicationListener。 - 创建 ApplicationContext:根据类型创建
AnnotationConfigServletWebServerApplicationContext。 - 准备上下文:应用初始器,注册主配置类。
- 刷新上下文:调用
AbstractApplicationContext.refresh()。这是核心生命周期,包括执行BeanFactoryPostProcessor、扫描注册BeanDefinition、实例化非懒加载单例 Bean。 - 内嵌 Web 容器启动:
ServletWebServerFactoryAutoConfiguration生效,创建工厂并在refresh末尾启动 Tomcat。 - 发布事件:最后发布
ApplicationStartedEvent和ApplicationReadyEvent。
Starter 定制与扩展
3. 如何自定义 Starter?
Starter 结构通常分为依赖管理模块和自动配置逻辑模块。设计时要遵循分离原则,starter 只声明依赖,autoconfigure 模块包含实际逻辑。
在自动配置类中,务必使用条件注解来避免冲突。例如:
@Configuration
@ConditionalOnClass(MyService.class)
@ConditionalOnMissingBean(MyService.class)
public class MyServiceAutoConfiguration {
@Bean
public MyService myService() {
return new MyService();
}
}
同时提供配置属性绑定,通过 @ConfigurationProperties("my.service") 注入。最后记得将配置类注册到 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中。
配置管理与动态刷新
4. 外部化配置加载顺序与动态刷新?
加载优先级从高到低依次为:命令行参数 > 环境变量 > application.properties > 默认属性等。注意 application.properties 在不同路径下的优先级不同。
关于动态刷新,推荐方案是结合 Spring Cloud Config 和 @RefreshScope。在需要刷新的 Bean 上加注解,调用 /actuator/refresh 即可。如果不想引入 Spring Cloud,也可以监听 EnvironmentChangeEvent 手动更新内部状态,但普通 @ConfigurationProperties Bean 不会自动刷新,需配合 Scope 使用。
Web 容器可插拔性
5. 如何实现内嵌 Web 容器的可插拔?
Spring Boot 通过 ServletWebServerFactory 接口抽象了 Web 容器。自动配置类会根据 Classpath 条件判断是否启用 Tomcat、Jetty 或 Undertow。
若要切换为 Undertow,只需排除 Tomcat 依赖并引入 Undertow Starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
Actuator 安全与健康检查
6. Actuator 端点安全与自定义健康检查?
默认只暴露 /health 和 /info。生产环境若开放所有端点,必须集成 Spring Security 进行权限控制。可以配置 management.endpoints.web.exposure.include 限制访问范围。
自定义健康检查很简单,实现 HealthIndicator 接口即可。例如检查外部服务状态,返回 Health.up() 或 Health.down()。
异步任务处理
7. 正确使用 @Async 与线程池配置?
开启异步需在启动类加 @EnableAsync。注意不能在同一个类中调用 @Async 方法,因为基于代理的内部调用会绕过代理,导致不异步执行。
建议自定义线程池,避免使用默认的 SimpleAsyncTaskExecutor。配置核心线程数、队列容量及拒绝策略,并设置线程名前缀以便排查问题。
内存泄漏排查
8. 常见内存泄漏原因与优化?
常见原因包括静态集合持有引用、未关闭资源、ThreadLocal 未清理等。排查工具推荐使用 jstat、jmap 配合 MAT 分析堆转储文件。
优化方面,缓存建议使用 WeakHashMap 或带 TTL 的 Caffeine;资源操作尽量用 try-with-resources;ThreadLocal 使用后务必调用 remove()。
多数据源切换
9. 如何实现动态数据源切换?
静态多数据源定义多个配置类并用 @Primary 指定默认。动态切换则基于 AOP 实现。
思路是自定义注解标记方法,通过 AOP 切面获取注解值,存入 ThreadLocal,然后继承 AbstractRoutingDataSource 重写 determineCurrentLookupKey 方法从 ThreadLocal 获取当前数据源。这样就能在事务上下文中灵活切换。
Spring Boot 3.x 迁移
10. 主要变化与 GraalVM 支持?
Spring Boot 3 最低要求 JDK 17,包名从 javax.* 变为 jakarta.*。这带来了兼容性问题,第三方库需升级。
GraalVM Native Image 支持让应用编译为原生可执行文件,启动快且内存占用低。但需注意反射和动态代理需显式配置 @NativeHint。
配置类代理机制
11. @Configuration 为何是 CGLIB 代理?
Spring 默认对 @Configuration 类启用 CGLIB 代理,目的是保证 @Bean 方法的单例语义。如果没有代理,类内部调用 @Bean 方法会创建新实例,破坏单例。有了代理,调用会被拦截并从容器中获取已注册的 Bean。
启动性能优化
12. 启动慢的原因与诊断?
常见原因有自动配置过多、组件扫描范围大、数据库连接初始化慢等。诊断可开启 StartupInfoLogger 或使用 Startup Metrics。
优化手段包括排除不必要的自动配置、缩小扫描范围、开启延迟初始化,或者在 Spring Boot 3 中使用 GraalVM Native Image 实现毫秒级启动。
优雅停机
13. 如何实现优雅停机?
Spring Boot 2.3+ 支持配置 server.shutdown: graceful。收到 SIGTERM 信号后,会停止接收新请求,等待正在处理的请求完成,再依次关闭容器和上下文。
注意陷阱:长任务阻塞会导致无法中断,异步任务可能未完成。K8s 环境下应配合就绪探针,在停机前将 Pod 标记为 NotReady。
初始化时机区别
14. Initializer 与 Runner 的区别?
ApplicationContextInitializer 用于在 Context 刷新前修改配置,适合 Vault 注入或 K8s 绑定。
CommandLineRunner / ApplicationRunner 则在 Context 刷新完成后执行,适合启动时做 DB migration 或加载字典表。两者层级不同,用途分明。
循环依赖处理
15. 如何防止循环依赖?
Spring 依然依赖三级缓存解决循环依赖,但构造器注入无法解决,会直接报错。字段或 Setter 注入虽能解决,但属于'带伤运行'。最佳实践是通过重构消除循环依赖,而非依赖框架兜底。
健康检查聚合
16. Health 端点如何聚合?
默认只要有一个指示器 DOWN,整体就是 DOWN。可以通过实现 ManagementHealthAggregator 自定义聚合策略。例如,只有核心服务 DOWN 才标记整体 DOWN,其他非核心服务失败不影响 UP 状态。
分布式事务选型
17. 如何处理分布式事务?
Seata 适用于金融核心系统(AT/TCC),电商下单可用 Saga 模式,日志通知类可用消息队列最终一致。选型依据是业务对一致性的要求和性能权衡。
K8s 探针设计
18. 存活与就绪探针如何设计?
Liveness Probe 判断应用是否活着,失败重启 Pod,不应包含外部依赖以免误杀。
Readiness Probe 判断是否准备好接收流量,失败移除 Endpoint,可包含 DB、Redis 检查。Spring Boot 2.3+ 内置支持 livenessState 和 readinessState。
JSON 序列化安全
19. 防止无限递归与敏感泄露?
实体类双向关联可能导致栈溢出,推荐使用 DTO 转换或 @JsonManagedReference。敏感信息泄露可通过 @JsonIgnore 或 @JsonView 控制返回字段。全局配置 Jackson 时也可禁用日期时间戳等功能。
Jakarta EE 迁移
20. 兼容性问题与平滑升级?
主要变化是包名替换。兼容性问题主要来自未升级的第三方库。平滑升级策略是先升级到 Spring Boot 2.7 修复警告,再升级 3.x 并批量替换 import,最后验证依赖树兼容性。


