《SpringBoot4进阶实战》第22招 自己写AutoConfiguration
# 从零手写 Petclinic 审计 Starter:把默认配置变成可替换自动装配 ```text 旧方案:业务代码里到处写审计切面、指标统计、配置项 冲突:每个服务复制粘贴,默认实现无法统一升级,替换成本高 后果:配置漂移、行为不一致、升级困难、新人不敢改 ``` Petclinic 已经具备 Web、数据、缓存、可观测等能力,但请求审计和业务指标仍然散落在控制器、服务层甚至过滤器里。 我是老李,今天要解决的不是“能不能记录”,而是“怎么让所有业务应用只加一个依赖就自动拥有审计能力,同时还能按需替换默认实现”。 企业里大量 xxx-spring-boot-starter 到底是怎么做出来的? 是继续在业务工程里维护一套 @Configuration,还是把它抽成独立 Starter 并用 AutoConfiguration 自动装配? 如果选错,会不会导致类加载冲突、配置不生效、用户无法覆盖默认 Bean,最终 Starter 没人敢用? # 01、故事 ```text 周五 18:40 → 研发区 → 任务是给 Petclinic 加统一请求审计 → 冲突是审计逻辑散落、指标口径不一致、联调催得紧 ``` 周五傍晚,运维在群里贴出告警:Petclinic 几个核心接口 P99 突然升高,但没人能说清是哪个环节变慢。 任务是给所有 HTTP 请求加统一审计和业务指标,方便定位慢请求和热点接口。 我打开代码一看,有的控制器自己记日志,有的服务层手动埋点,还有人在过滤器里写临时统计。 联调同事在旁边催:“能不能先给一版,别让我每个服务都改一遍。” 我意识到,如果继续在业务工程里堆配置,今晚肯定改不完,而且下个服务还得重来。 > 暮色压工位,告警一声催。 > 审计四处散,指标各自飞。 > 若问根何在,配置未成规。 > 欲解千般乱,先立一模块。 # 02、问题 ```text 需求变化:从“能记录”升级为“所有服务统一自动拥有审计能力” 冲突:普通 @Configuration 需要业务方手动 import,且默认实现无法被优雅替换 后果:接入成本高、行为漂移、升级困难 ``` Petclinic 当前审计逻辑分散在多个模块,每个模块对“请求耗时”“用户标识”“业务动作”的定义都不一样。 这导致指标看板上的数据无法横向对比,排障时只能逐个服务翻日志。 更麻烦的是,如果我把审计逻辑写成一个普通 @Configuration,业务应用必须手动 @Import 或包扫描才能生效。 一旦默认实现需要升级,所有业务方都要改代码;如果业务方想替换默认审计器,又容易因为 Bean 冲突而失败。 可验证的验收标准是:业务应用只增加一个 Maven 依赖,不改任何代码,启动后自动具备请求审计能力;同时业务方可以自定义一个 Bean 替换默认实现,且启动不冲突。 > 旧时配置散,各自埋点忙。 > 一朝口径变,全局费周章。 > 若只加依赖,自动把活扛。 > 替换亦有道,方称好组件。 # 03、原理 ```text 核心机制:AutoConfiguration 条件装配 + 配置属性绑定 + Bean 覆盖策略 ``` Spring Boot 的自动配置不是魔法,它依赖 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.i mports 文件声明候选配置类。 启动时,Spring Boot 会加载这些候选类,并根据 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等条 件决定是否生效。 @EnableConfigurationProperties 可以把外部配置绑定到强类型 POJO,再注入到自动配置类中。 @ConditionalOnMissingBean 是实现“用户可替换默认实现”的关键:只有当容器中不存在用户自定义 Bean 时,才创建默认 Bean。 一个反直觉判断是:自动配置类本身不应该被业务包扫描到,否则会绕过条件装配,导致用户无法覆盖默认实现。 另一个反直觉判断是:Starter 的依赖边界要分“必需依赖”和“可选依赖”,如果把所有依赖都写成 compile,会污染业务应用的类路径。 > 自动非魔法,条件定去留。 > 缺豆才补位,有主便低头。 > 配置绑强型,元数据可求。 > 边界若不清,类路径成愁。 # 04、架构 ```text 输入:HTTP 请求、业务方法调用、用户自定义配置 本模块:audit-spring-boot-starter(自动配置、审计器、指标收集器、配置属性) 输出:统一审计日志、业务指标、可替换 Bean 扩展点 边界:不依赖 Petclinic 具体业务,不强制数据库存储 ``` 本模块分为四层:配置属性层、自动配置层、核心能力层、扩展点层。 配置属性层用 @ConfigurationProperties 绑定前缀,例如 petclinic.audit。 自动配置层用 @AutoConfiguration 声明,配合条件注解控制装配时机。 核心能力层提供默认审计器、指标收集器和请求过滤器。 扩展点层通过 @ConditionalOnMissingBean 暴露可替换接口,业务方只需提供一个同类型 Bean 即可覆盖。 收益是业务应用零代码接入,默认行为统一,升级只需升 Starter 版本。 代价是 Starter 内部必须保持向后兼容,配置项变更要有迁移说明。 适用条件是业务应用基于 Spring Boot 3 或 4,且类路径中存在 Spring MVC 或 WebFlux。 > 四层分职责,边界画得清。 > 属性先绑定,条件再点兵。 > 核心给默认,扩展留空名。 > 若问何所益,接入一身轻。 # 05、第一版实战 ```text 环境:JDK 17 + Spring Boot 3.3.x + Maven 3.9.x 配置:新建 audit-spring-boot-starter 模块,先写普通 @Configuration 代码:AuditProperties、AuditService、AuditFilter、AuditAutoConfiguration 启动:Petclinic 手动 @Import 该配置类 验证:请求 /owners 观察审计日志 V1:能记录,但需要手动 import,且替换默认实现容易冲突 ``` 先创建独立 Maven 模块 audit-spring-boot-starter,pom.xml 如下: ```xml org.springframework.boot spring-boot-autoconfigure org.springframework.boot spring-boot-configuration-processor true ``` 写配置属性类: ```java @ConfigurationProperties(prefix = "petclinic.audit") public class AuditProperties { private boolean enabled = true; private String logPrefix = "[AUDIT]"; private List ignorePaths = List.of("/actuator/**"); // getter/setter 省略 } ``` 写默认审计服务: ```java public interface AuditService { void record(String method, String path, long costMs, String user); } public class DefaultAuditService implements AuditService { private static final Logger log = LoggerFactory.getLogger(DefaultAuditService.class); private final AuditProperties properties; public DefaultAuditService(AuditProperties properties) { this.properties = properties; } @Override public void record(String method, String path, long costMs, String user) { log.info("{} {} {} {}ms user={}", properties.getLogPrefix(), method, path, costMs, user); } } ``` 写请求过滤器: ```java public class AuditFilter extends OncePerRequestFilter { private final AuditService auditService; private final AuditProperties properties; public AuditFilter(AuditService auditService, AuditProperties properties) { this.auditService = auditService; this.properties = properties; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { long start = System.currentTimeMillis(); try { chain.doFilter(request, response); } finally { if (properties.isEnabled() && !shouldIgnore(request.getRequestURI())) { auditService.record(request.getMethod(), request.getRequestURI(), System.currentTimeMillis() - start, request.getRemoteUser()); } } } private boolean shouldIgnore(String uri) { return properties.getIgnorePaths().stream().anyMatch(uri::startsWith); } } ``` 写普通配置类: ```java @Configuration @EnableConfigurationProperties(AuditProperties.class) public class AuditAutoConfiguration { @Bean @ConditionalOnMissingBean public AuditService auditService(AuditProperties properties) { return new DefaultAuditService(properties); } @Bean @ConditionalOnMissingBean public AuditFilter auditFilter(AuditService auditService, AuditProperties properties) { return new AuditFilter(auditService, properties); } } ``` 在 Petclinic 主类上手动导入: ```java @SpringBootApplication @Import(AuditAutoConfiguration.class) public class PetClinicApplication { public static void main(String[] args) { SpringApplication.run(PetClinicApplication.class, args); } } ``` 启动并请求: ```bash mvn spring-boot:run curl -s http://localhost:8080/owners | head -n 5 ``` 示例输出: ```text [AUDIT] GET /owners 23ms user=null ``` V1 能记录请求,但业务方必须手动 @Import,且如果业务方自己定义了 AuditService,可能因为包扫描顺序或配置类加载顺序导致 Bean 冲突。 > 初版虽能记,手动导入烦。 > 替换若不当,冲突起波澜。 > 条件虽已写,装配未成环。 > 欲求真自动,还需再深钻。 # 06、排查 ```text 诊断树: 现象:业务方自定义 AuditService 后启动报 BeanDefinitionOverrideException 怀疑:@Import 的配置类与业务包扫描重复注册 检查:查看 Bean 定义来源和注册顺序 证据:业务方 @Service 注解的 AuditServiceImpl 与 @Bean 方法同时存在 根因:@Import 的配置类被业务包扫描到,导致条件装配失效 修复:把自动配置类移出业务包扫描路径,改用 AutoConfiguration.imports 声明 ``` 第一个问题是业务方自定义 AuditService 后启动报错。 怀疑是 @Import 的配置类被业务包扫描重复加载。 检查 Bean 定义来源: ```bash mvn spring-boot:run -Dspring-boot.run.arguments=--debug ``` 示例输出: ```text AuditService defined in com.example.audit.AuditAutoConfiguration AuditService defined in com.example.petclinic.service.AuditServiceImpl ``` 证据显示同一个类型有两个 Bean 定义,且 @ConditionalOnMissingBean 没有生效。 根因是业务方把自定义 AuditServiceImpl 放在 com.example.petclinic 包下,而 AuditAutoConfiguration 也在该包下,业务包扫描把自动配置类也扫 了进去。 修复方法是把自动配置类移到独立包,例如 com.example.audit.autoconfigure,并通过 AutoConfiguration.imports 声明。 第二个问题是配置属性没有 IDE 提示。 怀疑是缺少配置元数据。 检查 target/classes/META-INF 目录: ```bash ls target/classes/META-INF/ ``` 示例输出: ```text spring-configuration-metadata.json 不存在 ``` 根因是 configuration-processor 没有正确生成元数据,需要确认注解处理器是否启用。 **错误尝试:** 我一开始把 @ConditionalOnMissingBean 放在 @Bean 方法上,但配置类本身被业务包扫描到,导致条件判断在错误时机执行,仍然出现 Bean 冲突。 > 现象是冲突,怀疑在扫描。 > 证据看来源,根因在包号。 > 修复移路径,声明换新招。 > 错误曾尝试,条件未生效。 # 07、优化 ```text V1:普通 @Configuration + 手动 @Import 证据:业务包扫描导致条件装配失效,配置元数据缺失 V2:真正 AutoConfiguration + AutoConfiguration.imports + 配置元数据 ``` 把自动配置类移到独立包 com.example.audit.autoconfigure,并加上 @AutoConfiguration 注解: ```java @AutoConfiguration @EnableConfigurationProperties(AuditProperties.class) public class AuditAutoConfiguration { @Bean @ConditionalOnMissingBean public AuditService auditService(AuditProperties properties) { return new DefaultAuditService(properties); } @Bean @ConditionalOnMissingBean public AuditFilter auditFilter(AuditService auditService, AuditProperties properties) { return new AuditFilter(auditService, properties); } } ``` 在 resources/META-INF/spring/ 下创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports: ```text com.example.audit.autoconfigure.AuditAutoConfiguration ``` 在 pom.xml 中确保 configuration-processor 生效: ```xml org.apache.maven.plugins maven-compiler-plugin org.springframework.boot spring-boot-configuration-processor ``` 在 Petclinic 中移除 @Import,只保留 Maven 依赖: ```xml com.example audit-spring-boot-starter 1.0.0 ``` 重新构建并启动,确认自动装配生效。 > 移包避扫描,注解换新装。 > 声明入文件,元数据成双。 > 业务去导入,依赖自担当。 > 替换有缺豆,冲突不再狂。 # 08、演进 ```text V2:真正 AutoConfiguration + 条件装配 + 配置元数据 能力:业务应用零代码接入,用户可自定义 Bean 替换默认实现 遗留:配置项较少,未覆盖异步请求和响应体审计 ``` 用 V1 同一组请求验证: ```bash curl -s http://localhost:8080/owners | head -n 5 ``` 示例输出: ```text [AUDIT] GET /owners 19ms user=null ``` 再验证用户自定义替换: ```java @Service public class CustomAuditService implements AuditService { @Override public void record(String method, String path, long costMs, String user) { System.out.println("CUSTOM: " + method + " " + path + " " + costMs + "ms"); } } ``` 启动后请求,示例输出: ```text CUSTOM: GET /owners 21ms ``` 收益是业务应用只加一个依赖就自动拥有审计能力,默认实现统一,升级只需升 Starter 版本。 代价是 Starter 内部必须保持向后兼容,配置项变更要有迁移说明。 适用条件是业务应用基于 Spring Boot 3 或 4,且类路径中存在 Spring MVC。 遗留问题是当前只覆盖同步 HTTP 请求,异步请求和响应体审计尚未实现。 > 同组请求验,自动已生效。 > 自定义替换,冲突不再生。 > 收益在零码,代价在兼容。 > 遗留异步事,留待后续攻。 # 09、洞见 ```text 从现象到判断:自动装配的本质是“约定优于配置”的工程化落地 ``` ## 自动配置类必须躲开业务包扫描 ```text 业务包扫描 → 自动配置类被提前加载 → 条件注解失效 → Bean 冲突 ``` 自动配置类如果放在业务包扫描路径下,会被当成普通配置类提前加载,@ConditionalOnMissingBean 的判断时机就会错乱。 正确做法是把自动配置类放在独立包,并只通过 AutoConfiguration.imports 声明。 ## 反直觉判断:@ConditionalOnMissingBean 不是万能保险 ```text 条件注解只在自动配置阶段生效 → 业务包扫描会绕过条件 → 冲突仍会发生 ``` 很多人以为加了 @ConditionalOnMissingBean 就一定能避免冲突,但如果配置类被业务包扫描到,条件判断就失效了。 条件装配的前提是配置类必须由自动配置机制加载,而不是业务扫描加载。 ## 反直觉判断:Starter 的依赖边界比功能更重要 ```text compile 依赖 → 污染业务类路径 → 版本冲突 → Starter 被弃用 ``` Starter 如果把所有依赖都写成 compile,会把不必要的库强加给业务应用,导致类路径冲突。 正确做法是把核心依赖写成 compile,把可选依赖写成 optional 或 provided。 ## 配置元数据是 Starter 的“用户界面” ```text 无元数据 → IDE 不提示 → 用户乱猜配置 → 接入成本上升 ``` 配置元数据不仅让 IDE 有提示,还能在编译期发现拼写错误。 这是 Starter 易用性的关键,很多团队却忽略这一步。 > 现象看冲突,判断在时机。 > 条件非万能,扫描破玄机。 > 边界定生死,元数据可依。 > 洞见藏深处,实战方可知。 # 10、系统落地 ```text 入口:HTTP 请求 → 服务:AuditFilter → 本模块:AuditService/DefaultAuditService → 存储:日志/指标收集器 → 未实现:异步审计、响应体审计 ``` 原来 Petclinic 的审计逻辑散落在控制器、服务层和过滤器中,没有统一入口和口径。 本篇增加了 audit-spring-boot-starter 模块,业务应用只加一个依赖就自动拥有请求审计能力。 现在能做什么:统一记录请求方法、路径、耗时、用户,支持配置忽略路径,支持自定义替换默认审计器。 下一篇缺什么:异步请求审计、响应体脱敏审计、审计数据持久化到数据库或消息队列。 官方文档可参考 Spring Boot 官方文档首页:https://docs.spring.io/spring-boot/ 。 > 入口在请求,服务在过滤。 > 模块补能力,存储待扩充。 > 本篇成闭环,异步尚留空。 > 落地有章法,演进自从容。 # 11、小结 ```text 三问三答: Starter 怎么做?→ 独立模块 + AutoConfiguration.imports + 条件装配 怎么自动拥有?→ 业务应用只加 Maven 依赖,零代码接入 怎么替换默认?→ @ConditionalOnMissingBean + 用户自定义 Bean ``` 企业里大量 xxx-spring-boot-starter 的核心做法是:独立模块、自动配置声明、条件装配、配置属性绑定、配置元数据。 业务应用只增加一个 Maven 依赖就自动拥有能力,是因为 Spring Boot 启动时会加载 AutoConfiguration.imports 中声明的配置类。 用户仍可通过自定义 Bean 替换默认实现,是因为 @ConditionalOnMissingBean 在容器中不存在用户 Bean 时才创建默认 Bean。 当前状态:Petclinic 已具备统一请求审计能力,且该能力以 Starter 形式独立存在,可复用到其他业务应用。 > 一问怎么做,独立加声明。 > 二问何自动,依赖即启程。 > 三问怎替换,缺豆才补成。 > 三答皆落地,闭环已打通。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:@ConditionalOnMissingBean 在什么情况下会失效? 当自动配置类被业务包扫描提前加载时,条件注解的判断时机错乱,@ConditionalOnMissingBean 会失效,导致 Bean 冲突。 ## 实战题:给 audit-spring-boot-starter 增加一个配置项,支持自定义日志级别。 在 AuditProperties 中增加 logLevel 字段,默认 INFO; 在 DefaultAuditService 中根据 logLevel 选择日志输出级别; 重新构建后,在业务应用配置文件中设置 petclinic.audit.log-level=DEBUG 验证生效。 ## 排障题:业务应用加了 Starter 依赖后,启动时提示找不到 AuditFilter 类。 检查 Starter 的 pom.xml 是否把 spring-web 依赖写成了 optional 或 provided; 如果是,业务应用需要显式引入 spring-web 依赖,或者把 Starter 中的 spring-web 改为 compile 依赖。 ## 架构判断题:如果多个 Starter 都提供了同一个类型的默认 Bean,应该怎么避免冲突? 应该使用 @ConditionalOnMissingBean 并配合 @ConditionalOnClass 限定生效条件; 如果确实存在多个 Starter 竞争同一个 Bean,需要明确优先级或让业务方显式定义 Bean 来覆盖所有默认实现。 > 理解问条件,失效在扫描。 > 实战增配置,日志可调参。 > 排障查依赖,边界要看清。 > 架构判冲突,缺豆定去留。 # 13、升华 ```text 工程结论:自定义 Starter 的本质是把“默认配置”变成“可替换自动装配” ``` 从普通 @Configuration 到真正 AutoConfiguration,不是简单加个注解,而是把配置类的加载时机从业务扫描切换到自动配置机制。 这一切换带来了三个能力:业务应用零代码接入、默认实现统一升级、用户自定义 Bean 优雅替换。 有依据的结论是:Spring Boot 的自动配置体系是一套完整的扩展协议,掌握它就能把团队内部的最佳实践沉淀为可复用的 Starter。 下一篇将把审计数据从日志升级为结构化指标,并接入 Actuator 和 Micrometer,让审计能力从“可记录”走向“可观测”。 > 配置非终点,装配是桥梁。 > 默认可替换,升级不慌张。 > 协议掌握后,实践自沉淀。 > 审计入指标,观测路更长。 **系列项目:** Spring 官方 Petclinic 演进为生产级 Spring Boot 4 Java 应用 **已完成:** 环境搭建、启动机制、依赖与自动配置、配置体系、Web/安全/数据/事务、远程调用/数据库迁移/缓存/并发、日志/测试/Testcontainers/Docker Compose、Actuator/可观测 **本篇:** 从零手写 Petclinic 审计 Starter,完成自定义自动配置闭环 **下一篇:** 审计指标接入 Micrometer 与 Actuator,实现可观测增强