首页 文章 精选 留言 我的

精选列表

搜索[SpringBoot],共4561篇文章
优秀的个人博客,低调大师

SpringBoot开发案例之整合定时任务(Scheduled)

来来来小伙伴们,基于上篇的邮件服务,定时任务就不单独分项目了,天然整合进了邮件服务中。 不知道,大家在工作之中,经常会用到那些定时任务去执行特定的业务,这里列举一下我在工作中曾经使用到的几种实现。 任务介绍 Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。Timer的优点在于简单易用;缺点是Timer的所有任务都是由同一个线程调度的,因此所有任务都是串行执行的。同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务,不过这种实现已经被项目所废弃。 开源集群任务框架Quartz,这是一个功能比较强大的的调度器,适合做任务集群,解决单点故障,目前项目中在使用。 Spring家族自带的Scheduled,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多,适用于简单的任务,微服务使用很方便。 项目应用 创建任务 代码中,可以发现,sendMail方法上注解被注释掉了,目前我们采用的是xml配置实现的。 import org.springframework.stereotype.Component; /** * 统计失败邮件定时重新发送 * 创建者 科帮网 * 创建时间 2017年7月21日 * */ @Component("sendMail") public class SendMail { //@Scheduled(cron = "0/5 * * * * ?") public void sendMail() { System.out.println("统计失败邮件定时重新发送开始"); } } 配置文件 <!-- 配置任务线性池 --> <task:executor id="executor" pool-size="5" /> <task:scheduler id="scheduler" pool-size="5"/> <!-- 启用注解驱动的定时任务 --> <task:annotation-driven executor="executor" scheduler="scheduler" proxy-target-class="true"/> <task:scheduled-tasks scheduler="scheduler"> <!-- 统计失败邮件定时重新发送 --> <task:scheduled ref="sendMail" method="sendMail" cron="0/5 * * * * ?"/> </task:scheduled-tasks> 启动项目 /** * 启动类 * 创建者 科帮网 * 创建时间 2017年7月19日 * */ @EnableAutoConfiguration @ComponentScan(basePackages={"com.itstyle.main"}) @ImportResource({"classpath:spring-context-dubbo.xml","classpath:spring-context-task.xml"}) public class Application { private static final Logger logger = Logger.getLogger(Application.class); public static void main(String[] args) throws InterruptedException { SpringApplication.run(Application.class, args); logger.info("项目启动 "); } } 启动后,控制台会每5s打印"统计失败邮件定时重新发送开始"。当然Scheduled的功能不仅仅如此,我们查找源码Scheduled类,可以发现还有一些注解属性,这里就不一一为大家介绍了。总之,要养成查看源码API的习惯。 @Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(Schedules.class) public @interface Scheduled { public abstract String cron(); public abstract String zone(); public abstract long fixedDelay(); public abstract String fixedDelayString(); public abstract long fixedRate(); public abstract String fixedRateString(); public abstract long initialDelay(); public abstract String initialDelayString(); } 项目:http://git.oschina.net/52itstyle/spring-boot-mail 作者: 小柒 出处: https://blog.52itstyle.com 本文版权归作者和云栖社区所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 如有问题, 可邮件(345849402@qq.com)咨询。

优秀的个人博客,低调大师

《SpringBoot4进阶实战》第5招 自动配置不是魔法

# 为什么加一个 Starter,容器里就多出几十个 Bean? ```text 旧方案:手动 new Bean → 冲突:依赖一多就失控 → 后果:配置爆炸、启动失败 ``` 我是老李,一个在深圳摸爬滚打多年的 Java 架构师。 这些年我带过不少团队,见过太多 Spring Boot 项目,明明只加了一个 Starter,启动时容器里却突然多出几十个 Bean,没人说得清它们从哪来。 更头疼的是,有时候你定义了一个自己的 Bean,Spring Boot 却悄悄退让,不给你自动配置,导致功能静默失效。 冲突问句:为什么只加一个依赖,容器就多出这么多 Bean? 选择问句:当自动配置和你的 Bean 冲突时,Boot 凭什么替你做决定? 选错后果问句:如果不懂自动配置的匹配规则,排查启动问题要浪费多少小时? # 01、故事 ```text 凌晨 1 点 → 深圳某创业公司机房 → 任务是排查 Petclinic 启动后数据源失效 → 冲突:加了 web starter 后 JPA 仓库全部报错 ``` 凌晨一点,机房空调嗡嗡作响,我盯着屏幕上的异常堆栈,头皮发麻。 任务是给 Petclinic 加上 Spring MVC 和 Jackson 支持,结果一启动,所有 JPA 仓库都报找不到 DataSource。 明明之前用 spring-boot-starter-data-jpa 还好好的,怎么加了个 web starter 就全乱了? 我翻遍 application.properties,确认数据源配置没丢,可容器里就是没有 DataSource 的 Bean。 同事在旁边催:老李,客户明天早上要看演示,今晚必须搞定。 我深吸一口气,打开 Spring Boot 的 --debug 开关,决定从自动配置的匹配报告里找真相。 > 加个依赖几十 Bean, > 数据源却悄悄不见。 > 深夜排查心焦灼, > 条件报告见真颜。 # 02、问题 ```text 需求变化:从单一 JPA 项目升级为 Web + JPA 完整应用 → 冲突:自动配置互相干扰 → 后果:DataSource 缺失,仓库全部失效 ``` Petclinic 原本只依赖 spring-boot-starter-data-jpa,一切正常。 为了提供 REST API,我加了 spring-boot-starter-web,结果启动时 Hibernate 初始化失败,提示找不到 DataSource。 原因不是配置丢了,而是自动配置的匹配条件变了——web starter 引入了新的类,导致某些条件评估结果翻转。 影响是灾难性的:所有 OwnerRepository、PetRepository 全部无法注入,应用直接启动失败。 可验证的验收标准:启动应用后,日志中必须出现 DataSource 相关的自动配置匹配记录,且仓库能正常注入。 > 需求一变配置乱, > 数据源失联仓库瘫。 > 条件评估藏玄机, > 报告一出真相显。 # 03、原理 ```text 核心机制:@EnableAutoConfiguration 导入 AutoConfigurationImportSelector,按条件评估每个 AutoConfiguration 类 ``` Spring Boot 的自动配置核心是 @EnableAutoConfiguration 注解,它通过 AutoConfigurationImportSelector 加载所有 META-INF/s pring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件里声明的配置类。 每个 AutoConfiguration 类上都有 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解,只有条件全部满足时,这个配置类才会被处理,里 面的 Bean 才会被注册。 反直觉判断:条件评估的顺序不是随机的,而是按 AutoConfiguration 类的声明顺序,且 @ConditionalOnMissingBean 只在当前容器中查找,不会考虑父容器。 另一个反直觉点:@ConditionalOnClass 只检查类是否存在,不关心类是否可加载,所以即使类在但依赖缺失,条件也可能误判为 true。 实战中,你只需要理解:classpath 上的类决定了哪些自动配置候选,你的配置和 Bean 决定了哪些条件被满足,最终 Boot 替你组装好默认的 Bean 图。 > 条件注解层层筛, > 类路径上定乾坤。 > 用户 Bean 优先权, > 回退机制保平安。 # 04、架构 ```text 输入:classpath 类、配置属性、用户 Bean → 本模块:AutoConfiguration 条件评估 → 输出:匹配的 Bean 定义 ``` 自动配置架构分三层:候选层、条件层、注册层。 候选层由 AutoConfiguration.imports 文件提供,列出所有可能的配置类。 条件层用各种 Conditional 注解过滤,只有条件全过才进入注册层。 注册层通过 @Bean 方法定义 Bean,并利用 @ConditionalOnMissingBean 给用户留出覆盖空间。 收益是巨大的:开发者只需加一个 Starter,Boot 自动装配好数据源、JPA、MVC 等几十个 Bean,省去大量样板配置。 代价是黑盒效应:一旦条件不满足,你很难直觉判断为什么某个 Bean 没被创建。 适用条件是:项目遵循 Boot 的约定,classpath 干净,不随意排除自动配置。 > 候选条件注册层, > 层层过滤定乾坤。 > 收益巨大代价隐, > 约定优于配置深。 # 05、第一版实战 ```text 环境:Spring Boot 3.2 + Java 17 + Maven → 配置:添加 web starter → 代码:无改动 → 启动:--debug 查看报告 → 验证:检查 DataSource Bean ``` 我用的版本是 Spring Boot 3.2.5,Java 17,Maven 构建。 首先,在 Petclinic 的 pom.xml 中添加 web starter: ```xml org.springframework.boot spring-boot-starter-web ``` 然后,保持原有 JPA starter 不变,启动应用并开启 debug 日志: ```bash mvn spring-boot:run -Dspring-boot.run.arguments=--debug ``` 启动后,控制台会输出自动配置报告,我重定向到文件方便分析: ```bash mvn spring-boot:run -Dspring-boot.run.arguments=--debug > debug.log 2>&1 ``` 在 debug.log 中搜索 DataSource 相关段落,看到如下内容(示例输出): ```text DataSourceAutoConfiguration matched: - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition) - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition) DataSourceAutoConfiguration#dataSource matched: - @ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition) ``` 但奇怪的是,JPA 相关的 HibernateJpaAutoConfiguration 却显示 negative: ```text HibernateJpaAutoConfiguration matched: - @ConditionalOnClass found required classes 'org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean', 'jakarta.persistence.EntityManager', 'org.hibernate.engine.spi.SessionFactoryImplementor' (OnClassCondition) - @ConditionalOnBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition) ``` 注意最后一行:HibernateJpaAutoConfiguration 需要 DataSource Bean,但报告显示没找到。 可 DataSourceAutoConfiguration 明明 matched 了,为什么 DataSource Bean 没被创建? 我继续往下看,发现 DataSourceAutoConfiguration 中的 dataSource 方法显示 matched,但实际 Bean 却没注册。 这让我怀疑是 Bean 定义冲突,或者条件评估顺序问题。 成功标准是:启动后容器中必须有 DataSource Bean,且 JPA 仓库能正常注入。 但当前显然没达到,我需要深入排查。 > 加个 web 依赖简单, > 启动报告却打脸。 > DataSource 匹配了, > 实际 Bean 却不见。 # 06、排查 ```text 诊断树:现象→怀疑→检查→证据→根因→修复 ``` **问题一:DataSource Bean 缺失** 现象:启动失败,提示找不到 DataSource。 怀疑:自动配置条件不满足,或者被用户 Bean 覆盖。 检查:查看完整自动配置报告,搜索 DataSource 相关段落。 ```bash grep -n "DataSource" debug.log | head -50 ``` 证据:报告显示 DataSourceAutoConfiguration matched,但 dataSource 方法没有输出 matched 或 unmatched。 根因:我注意到报告中有个 negative 条件:@ConditionalOnMissingBean (types: javax.sql.DataSource) 显示 did not find any beans,但 dataSource 方法本身没有匹配记录,说明它可能被 @ConditionalOnProperty 等条件拦截。 修复:检查 application.properties,发现我设置了 spring.datasource.url 但没设置 driver-class-name,导致 DataSource 创建失败。 **错误尝试:** 我一开始怀疑是 web starter 引入了嵌入式数据库,导致自动配置选择了 EmbeddedDataSource,于是尝试排除 EmbeddedDatabase 类,但没用。 **问题二:JPA 自动配置不生效** 现象:HibernateJpaAutoConfiguration 显示 unmatched。 怀疑:缺少 DataSource Bean。 检查:查看 HibernateJpaAutoConfiguration 的条件。 ```bash grep -A 10 "HibernateJpaAutoConfiguration" debug.log ``` 证据:条件 @ConditionalOnBean (types: javax.sql.DataSource) 显示 did not find any beans。 根因:DataSource Bean 没创建,所以 JPA 配置自然不生效。 修复:补全数据源配置,确保 DataSource Bean 能创建。 > 报告显示匹配了, > 实际 Bean 却缺席。 > 条件嵌套层层锁, > 配置补全才解围。 # 07、优化 ```text{V1:手动配置数据源 → 证据:报告显示条件不满足 → V2:利用自动配置 + 自定义 Bean 观察回退} ``` V1 中我手动在 application.properties 里配置了数据源,但漏了 driver-class-name,导致自动配置失败。 优化方案是:完全依赖自动配置,只提供必要属性,并定义一个自定义 DataSource Bean 来观察 Boot 的回退行为。 首先,修正配置,只保留 URL 和账号密码: ```properties spring.datasource.url=jdbc:h2:mem:petclinic;DB_CLOSE_DELAY=-1 spring.datasource.username=sa spring.datasource.password= ``` 然后,在 Petclinic 中定义一个配置类,手动创建 DataSource: ```java @Configuration public class CustomDataSourceConfig { @Bean public DataSource dataSource() { return DataSourceBuilder.create() .url("jdbc:h2:mem:custom;DB_CLOSE_DELAY=-1") .username("sa") .password("") .build(); } } ``` 重新启动并查看报告,会发现 DataSourceAutoConfiguration 中的 dataSource 方法显示 unmatched: ```text DataSourceAutoConfiguration#dataSource matched: - @ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) found beans of type 'javax.sql.DataSource' (OnBeanCondition) ``` 注意:这里显示 found beans,说明 Boot 检测到用户自定义的 DataSource,于是自动回退,不再创建默认的。 这就是 @ConditionalOnMissingBean 的威力:用户 Bean 优先,Boot 自动让路。 > 手动配置易出错, > 自动回退显智慧。 > 用户 Bean 优先权, > 条件注解来守卫。 # 08、演进 ```text{V2:自定义 Bean 触发回退 → 能力:理解 Boot 决策 → 遗留:如何精确控制自动配置} ``` 通过 V2,我验证了 Boot 的回退机制:当用户定义了 DataSource Bean,自动配置就不再创建默认的。 收益是:开发者可以完全控制关键 Bean,而不必担心 Boot 覆盖。 代价是:如果用户 Bean 定义有误,Boot 不会帮你兜底,错误会直接暴露。 适用场景:当你需要定制数据源(如连接池、多数据源)时,自定义 Bean 是正确做法。 遗留问题:如果我想让 Boot 的自动配置和我的 Bean 共存,该怎么办? 用同一组请求验证:启动应用后,访问 /owners 列表,发现数据来自自定义的 H2 库,而不是默认的 petclinic 库,证明自定义 Bean 生效。 > 自定义 Bean 显身手, > Boot 回退不抢戏。 > 共存需求仍存在, > 条件控制待深究。 # 09、洞见 ```text 从现象到判断:自动配置不是魔法,而是条件决策的工程实现 ``` ## 自动配置是条件决策树 ```text classpath → 条件评估 → 匹配/回退 → Bean 注册 ``` Boot 的自动配置本质是一棵决策树,每个节点是一个条件注解。 理解这棵树,你就能预测哪些 Bean 会出现,哪些不会。 反直觉判断:条件评估结果可能因 classpath 顺序而变,因为类加载顺序影响 @ConditionalOnClass 的判断。 ## 用户 Bean 是最高优先级 ```text 用户 Bean → @ConditionalOnMissingBean → Boot 回退 ``` 当你定义了一个 Bean,Boot 默认尊重你的选择,不再创建同类型 Bean。 这是约定优于配置的体现,但也意味着你必须有足够能力保证 Bean 正确。 反直觉判断:@ConditionalOnMissingBean 只检查当前容器,不检查父容器,所以在多上下文环境下可能失效。 ## 条件报告是排障第一工具 ```text --debug → 报告 → matched/unmatched → 根因 ``` 遇到自动配置问题,第一件事就是看条件报告,而不是猜。 报告会告诉你每个条件为什么匹配或不匹配,直接定位根因。 ## 自动配置是工程决策的自动化 ```text 依赖 → 默认决策 → 可覆盖 → 可扩展 ``` Boot 最核心的能力,是根据 classpath、配置和用户 Bean 自动做默认工程决策。 这大大降低了集成成本,但也要求你理解决策规则。 > 条件树中藏玄机, > 用户 Bean 定乾坤。 > 报告一出真相显, > 决策自动化成真。 # 10、系统落地 ```text 入口:PetclinicApplication → 服务:OwnerController → 本模块:自动配置 → 存储:H2 → 未实现:自定义 Starter ``` Petclinic 原本只有 JPA 自动配置,现在增加了 Web 自动配置,并验证了自定义 Bean 的回退。 原来能做什么:启动后自动配置数据源和 JPA,仓库可用。 本篇增加什么:理解自动配置的匹配与回退,能通过 --debug 报告排障。 现在能做什么:当自动配置异常时,能快速定位是哪个条件不满足,并决定是补配置还是自定义 Bean。 下一篇缺什么:如何自定义一个自动配置模块,让团队内部组件也能享受 Starter 的便利。 官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developin g-auto-configuration > 自动配置已掌握, > 排障能力渐增强。 > 自定义模块待开发, > 团队复用更顺畅。 # 11、小结 ```text 三问三答:为什么多 Bean?因为自动配置按条件批量注册;冲突时谁决定?用户 Bean 优先;选错后果?启动失败或静默失效 ``` 第一问:为什么加一个 Starter 就多几十个 Bean?因为 Boot 根据 classpath 加载所有匹配的 AutoConfiguration,每个配置类可能注册多个 Bean。 第二问:当自动配置和用户 Bean 冲突时,Boot 如何选择?通过 @ConditionalOnMissingBean,用户 Bean 优先,Boot 自动回退。 第三问:如果不懂这些规则,选错后果是什么?轻则启动失败,重则功能静默失效,排查耗时数小时。 当前状态:Petclinic 已能正常启动,数据源和 JPA 工作正常,我掌握了用条件报告排障的方法。 > 三问三答心明了, > 自动配置不再玄。 > 条件报告手中剑, > 排障效率翻倍升。 # 12、作业 ```text 理解→实战→排障→架构判断 ``` ## 理解题:为什么 @ConditionalOnMissingBean 能防止自动配置覆盖用户 Bean? 因为条件评估发生在 Bean 注册前,如果容器中已有同类型 Bean,条件不满足,自动配置的 @Bean 方法就不会执行。 ## 实战题:在 Petclinic 中自定义一个 RestTemplate Bean,观察自动配置是否回退。 定义 Bean 后,启动应用,用 --debug 查看 RestTemplateAutoConfiguration 的报告,会发现其 @ConditionalOnMissingBean 条件显示 fo und beans。 ## 排障题:如果自动配置报告显示 DataSourceAutoConfiguration unmatched,你会如何排查? 先看 unmatched 的条件是什么,如果是 @ConditionalOnClass,检查 classpath 是否缺少对应类;如果是 @ConditionalOnProperty,检查配置属性。 ## 架构判断题:自动配置机制是否适合所有场景?请给出你的判断。 不适合。对于需要精细控制或非标准场景,自动配置可能成为黑盒,此时应手动配置或排除自动配置。 > 作业四题练真功, > 理解实战加排障。 > 架构判断需谨慎, > 自动配置非万能。 # 13、升华 ```text 工程结论:自动配置是 Boot 的默认决策引擎,理解条件评估是驾驭它的关键 ``` 通过这次实战,我深刻体会到:Spring Boot 的自动配置不是魔法,而是一套基于条件的决策框架。 它把常见的工程决策(如数据源类型、JSON 解析器、JPA 实现)自动化,让开发者专注于业务。 但自动化也意味着你需要理解决策规则,否则一旦条件不满足,你只能面对黑盒。 掌握 --debug 报告和条件注解,你就能从被动排障转为主动设计。 下一篇,我们将深入自定义自动配置,打造自己的 Starter,让团队组件也能享受这种便利。 > 自动配置决策引擎, > 条件评估定输赢。 > 理解规则方驾驭, > 工程智慧永流传。 **系列项目:** Spring Petclinic,一个经典的宠物诊所管理系统,用于演示 Spring Boot 生产级实践。 **已完成:** 环境搭建、启动机制、依赖与自动配置的初步理解。 **本篇:** 深入自动配置条件评估,掌握 --debug 报告排障,理解用户 Bean 回退机制。 **下一篇:** 自定义自动配置与 Starter 开发,让团队组件也能即插即用。

优秀的个人博客,低调大师

《SpringBoot4进阶实战》第12招 远程HTTP怎么调用

# Petclinic 宠物保险报价:RestClient 超时、异常映射与故障传播的工程实战 ```text 单体自足 → 首个远程依赖 → 线程阻塞与故障蔓延 ``` Petclinic 一直是个自给自足的单体应用,自己读写数据库,自己渲染页面,所有故障都局限在进程内部。 我是老李,负责把官方教学项目演进成生产级系统。今天要给 Petclinic 增加宠物保险报价服务,第一次引入远程 HTTP 调用。 为什么系统一旦拥有第一个远程依赖,就开始需要认真考虑 Timeout、线程占用和故障传播? 选择同步 RestClient 还是响应式 WebClient,依据是什么? 选错超时策略或选错调用模型,会带来哪些线上事故级别的后果? # 01、故事 ```text 周四 14:20 → 深圳科兴科学园 3 栋 11 层 → 任务是接保险报价 → 产品经理站在身后催 ``` 周四下午两点二十分,深圳科兴科学园 3 栋 11 层,窗外是灰蒙蒙的天。 任务是给 Petclinic 的宠物详情页接一个保险报价卡片,数据来自外部保险公司的 HTTP 接口。 产品经理老周站在我身后,手指敲着工位隔板:下午四点要和保险公司联调,你这边到底能不能通? 我打开 Petclinic 的代码仓库,发现项目里连一个 RestTemplate 都没用过,更别说 RestClient。 搜索框里敲下 RestClient,官方文档说它是 RestTemplate 的现代替代品,但我连它的 Builder 长什么样都没见过。 老周又催了一句:别查了,先跑通一个 Hello World 行不行? > 远程调用初登场, > 超时线程两茫茫。 > 故障传播如潮水, > 一腔孤勇对苍黄。 # 02、问题 ```text 需求变化:从本地数据到远程报价 → 冲突:外部服务不可控 → 后果:线程耗尽与雪崩 ``` Petclinic 原本所有数据都来自本地数据库,事务边界清晰,故障范围可控。 引入保险报价后,一次页面请求变成两次网络往返,Petclinic 的线程池开始被外部服务的响应时间绑架。 如果保险公司接口响应慢,Tomcat 的工作线程就会一直占着等响应,积累到一定程度,整个 Petclinic 的线程池被耗尽,连本地数据库查询都进不来。 更隐蔽的是,如果保险公司接口返回 500 或一段无法解析的 JSON,RestClient 默认抛出的异常会让 Petclinic 的全局异常处理器措手不及。 用户看到的不是友好的降级提示,而是 Whitelabel Error Page,甚至连接被重置。 可验证的验收标准是:保险公司接口 5 秒不响应,Petclinic 页面必须在 2 秒内返回降级内容,且 Tomcat 活跃线程数不持续增长。 > 远程一呼百应难, > 线程池中暗波澜。 > 超时未设终成患, > 雪崩来时悔已晚。 # 03、原理 ```text 同步 HTTP 调用的核心机制:线程占用、超时控制、异常映射 ``` RestClient 是同步阻塞模型,发请求的线程会一直等到响应或超时,期间该线程不能做任何其他事。 Tomcat 默认工作线程池是 200,每个线程同时只能处理一个请求,这是理解故障传播的起点。 Connection Timeout 是建立 TCP 连接的最大等待时间,Read Timeout 是连接建立后等待响应体的最大时间,两者独立配置,缺一不可。 反直觉判断一:Read Timeout 不是越大越好,它决定了单个线程被占用的最长时间,也决定了系统在故障时的恢复速度。 反直觉判断二:RestClient 抛出的 ResourceAccessException 和 HttpClientErrorException 需要分别处理,前者是网络层故障,后者是业务层故障,混为 一谈会让降级策略失效。 异常映射的本质是把远程调用的三类失败——连接失败、超时、非 2xx 响应——翻译成 Petclinic 领域内可理解的业务异常。 > 线程阻塞似枷锁, > 超时设置定死活。 > 异常映射分层次, > 降级兜底不背锅。 # 04、架构 ```text Petclinic Controller → InsuranceQuoteService → RestClient → 保险公司 HTTP API ``` 这一层架构的核心是引入 InsuranceQuoteClient 作为防腐层,隔离外部系统的变化。 Controller 只依赖 InsuranceQuoteService 接口,Service 内部持有 InsuranceQuoteClient,Client 封装 RestClient 的所有细节 。 边界在于:Petclinic 内部永远不出现 RestClient 的异常类型,Client 层负责把远程异常翻译成 InsuranceQuoteUnavailableException。 收益是 Petclinic 的 Controller 和 Service 层完全不知道远程调用的存在,替换保险公司或切换调用模型都不影响上层。 代价是多了一层类和一个 DTO 映射,对于只有两三个接口的集成场景,这个成本可以接受。 适用条件是远程依赖数量少但不可控,需要统一降级策略的场景。 ```text +----------------+ +---------------------+ +------------------+ +------------------+ | Petclinic | --> | InsuranceQuote | --> | InsuranceQuote | --> | 保险公司 | | Controller | | Service | | Client | | HTTP API | +----------------+ +---------------------+ +------------------+ +------------------+ | 超时/异常映射 v +------------------+ | 降级策略 | | 返回友好提示 | +------------------+ ``` > 防腐层中隔内外, > 异常翻译不越界。 > 降级策略藏深处, > 架构清晰福自来。 # 05、第一版实战 ```text 环境:Spring Boot 3.2 + Java 17 → 配置:Base URL 与超时 → 代码:Client 与 Service → 启动 → 验证 → V1 ``` 当前 Petclinic 基于 Spring Boot 3.2,Java 17,需按当前官方文档核验 RestClient 的包路径。 第一步,在 pom.xml 增加依赖,Spring Web 已包含 RestClient,无需额外引入。 ```xml org.springframework.boot spring-boot-starter-web ``` 第二步,在 application.properties 配置保险报价服务的 Base URL 和超时时间。 ```properties insurance.quote.base-url=http://localhost:9090 insurance.quote.connection-timeout=1000 insurance.quote.read-timeout=2000 ``` 第三步,创建 InsuranceQuoteClient,使用 RestClient.Builder 构建。 ```java @Component public class InsuranceQuoteClient { private final RestClient restClient; public InsuranceQuoteClient(RestClient.Builder builder, @Value("${insurance.quote.base-url}") String baseUrl, @Value("${insurance.quote.connection-timeout}") int connectTimeout, @Value("${insurance.quote.read-timeout}") int readTimeout) { this.restClient = builder .baseUrl(baseUrl) .requestFactory(new ClientHttpRequestFactorySettings() .withConnectTimeout(Duration.ofMillis(connectTimeout)) .withReadTimeout(Duration.ofMillis(readTimeout))) .build(); } public InsuranceQuote fetchQuote(Long petId) { return restClient.get() .uri("/quotes/{petId}", petId) .retrieve() .body(InsuranceQuote.class); } } ``` 第四步,创建 InsuranceQuoteService,处理降级逻辑。 ```java @Service public class InsuranceQuoteService { private final InsuranceQuoteClient client; public InsuranceQuoteService(InsuranceQuoteClient client) { this.client = client; } public InsuranceQuote getQuote(Long petId) { try { return client.fetchQuote(petId); } catch (ResourceAccessException e) { return InsuranceQuote.unavailable("报价服务暂时不可用"); } catch (HttpClientErrorException | HttpServerErrorException e) { return InsuranceQuote.unavailable("报价服务返回错误"); } } } ``` 第五步,启动 Petclinic,访问宠物详情页,观察报价卡片是否显示。 ```bash mvn spring-boot:run curl http://localhost:8080/owners/1/pets/1 ``` 未在当前环境实测,以下为预期结果:保险公司接口正常时,页面显示报价金额;接口不可达时,页面显示降级文案。 示例输出: ```text HTTP/1.1 200 OK 页面包含:"报价服务暂时不可用" ``` V1 成功标准:本地启动无报错,正常路径能拿到报价,接口关闭时页面不白屏。 > 第一版中代码忙, > 超时配置两行藏。 > 降级逻辑初成形, > 页面不再白茫茫。 # 06、排查 ```text 诊断树:慢响应、HTTP 500、错误 JSON、网络断开 ``` 问题一:保险公司接口模拟慢响应 5 秒,Petclinic 页面卡住 5 秒才返回。 现象:页面转圈 5 秒,用户流失。 怀疑:Read Timeout 配置没生效。 检查:查看启动日志中 RestClient 的配置。 ```bash curl -X POST http://localhost:9090/slow-quote -d '{"delay":5000}' ``` 证据:请求发出后,Tomcat 线程被占用 5 秒,Read Timeout 设为 2000ms 却未触发。 根因:ClientHttpRequestFactorySettings 的配置方式在 Spring Boot 3.2 中需要配合 JdkClientHttpRequestFactory 使用,默认的 SimpleClientHttpRequestFactory 不读取该设置。 修复:显式指定 RequestFactory。 ```java this.restClient = builder .baseUrl(baseUrl) .requestFactory(new JdkClientHttpRequestFactory( new ClientHttpRequestFactorySettings() .withConnectTimeout(Duration.ofMillis(connectTimeout)) .withReadTimeout(Duration.ofMillis(readTimeout)))) .build(); ``` 问题二:保险公司接口返回 HTTP 500,Petclinic 抛出 HttpServerErrorException,页面显示 500 错误。 现象:用户看到 Whitelabel Error Page。 怀疑:Service 层没有捕获 HttpServerErrorException。 检查:查看 Petclinic 日志中的异常堆栈。 ```bash tail -f logs/petclinic.log | grep HttpServerErrorException ``` 证据:日志显示 HttpServerErrorException 未被捕获,直接抛到全局异常处理器。 根因:Service 层只捕获了 ResourceAccessException,遗漏了 HttpServerErrorException。 修复:在 Service 层增加对 HttpServerErrorException 的捕获。 **错误尝试:** 最初尝试在 Controller 层用 @ExceptionHandler 捕获所有 RestClient 异常,结果导致 Controller 层与远程调用细节耦合,后续替换保险 公司时改动面过大,回滚重写。 > 慢响应来线程占, > 超时未生效心寒。 > 五百错误漏捕获, > 层层排查见真章。 # 07、优化 ```text V1 证据:超时未生效、500 未捕获 → V2:显式 RequestFactory + 统一异常映射 + 调用日志 ``` V1 暴露了两个问题:超时配置未生效,异常捕获不完整。 V2 的改动集中在 InsuranceQuoteClient 内部,对外接口不变。 第一,显式指定 JdkClientHttpRequestFactory,确保超时生效。 第二,引入 RestClient 的异常映射器,把非 2xx 响应统一转换为自定义异常。 ```java this.restClient = builder .baseUrl(baseUrl) .requestFactory(new JdkClientHttpRequestFactory( new ClientHttpRequestFactorySettings() .withConnectTimeout(Duration.ofMillis(connectTimeout)) .withReadTimeout(Duration.ofMillis(readTimeout)))) .defaultStatusHandler( HttpStatusCode::isError, (request, response) -> { throw new InsuranceQuoteRemoteException( "保险公司接口返回错误: " + response.getStatusCode()); }) .build(); ``` 第三,在 Client 层增加调用日志,记录请求 URL、耗时和状态。 ```java public InsuranceQuote fetchQuote(Long petId) { long start = System.currentTimeMillis(); try { InsuranceQuote quote = restClient.get() .uri("/quotes/{petId}", petId) .retrieve() .body(InsuranceQuote.class); log.info("Fetch quote for pet {} succeeded in {} ms", petId, System.currentTimeMillis() - start); return quote; } catch (Exception e) { log.warn("Fetch quote for pet {} failed in {} ms: {}", petId, System.currentTimeMillis() - start, e.getMessage()); throw e; } } ``` Service 层只需捕获 InsuranceQuoteRemoteException 和 ResourceAccessException 两种类型。 > 超时配置重调整, > 异常映射一网擒。 > 调用日志留痕迹, > 故障排查有迹寻。 # 08、演进 ```text V2 能力:超时可控、异常可映射、日志可观测 → 遗留:错误 JSON 解析与线程池隔离 ``` V2 之后,用 V1 同一组请求验证效果。 慢响应请求:保险公司接口延迟 5 秒,Petclinic 在 2 秒时触发 Read Timeout,返回降级文案。 ```text HTTP/1.1 200 OK 页面包含:"报价服务暂时不可用" 耗时:2003ms ``` HTTP 500 请求:保险公司接口返回 500,Petclinic 捕获 InsuranceQuoteRemoteException,返回降级文案。 ```text HTTP/1.1 200 OK 页面包含:"报价服务暂时不可用" 耗时:45ms ``` 网络断开请求:关闭保险公司服务,Petclinic 在 1 秒时触发 Connection Timeout,返回降级文案。 ```text HTTP/1.1 200 OK 页面包含:"报价服务暂时不可用" 耗时:1002ms ``` 收益是 Petclinic 的页面不再被远程故障拖垮,降级策略统一且可观测。 代价是引入了自定义异常类型和日志埋点,代码量略有增加。 遗留问题是错误 JSON 的解析:如果保险公司返回 200 但 JSON 结构不符合预期,Jackson 抛出的异常尚未被捕获。 另一个遗留是线程池隔离:目前所有远程调用共享 Tomcat 工作线程,极端情况下仍可能互相影响。 > 同一请求再验证, > 超时降级两相宜。 > 五百断网皆可挡, > 遗留问题待后续。 # 09、洞见 ```text 从现象到判断:远程依赖改变了故障模型 ``` ## 超时是系统恢复能力的上限 ```text 故障恢复时间 = 超时时间 + 降级处理时间 ``` 超时时间决定了系统在故障状态下能多快恢复,它不是越大越好,而是要与业务容忍度匹配。 ## 反直觉判断:同步阻塞不是原罪,失控的同步阻塞才是 ```text 同步调用 + 合理超时 = 可控的阻塞 同步调用 + 无超时 = 线程池耗尽 ``` Tomcat 200 个线程,每个线程等 30 秒,系统在 6 秒内就失去响应能力。 ## 反直觉判断:异常类型是故障分类的钥匙 ```text ResourceAccessException = 网络层故障 HttpServerErrorException = 服务端故障 HttpClientErrorException = 客户端故障 ``` 把三类异常混在一起捕获,等于放弃了精确的降级策略。 ## 故障传播的根源是共享资源 ```text 远程调用占用线程 → 线程耗尽 → 本地请求被拒 ``` 第一个远程依赖出现时,Petclinic 的故障模型从单点变成了链式传播。 > 远程依赖改模型, > 超时恢复定上限。 > 异常分类是钥匙, > 共享资源酿祸端。 # 10、系统落地 ```text 入口:宠物详情页 → 服务:InsuranceQuoteService → 本模块:RestClient 调用层 → 存储:无 → 未实现:错误 JSON 处理 ``` Petclinic 原本只有本地数据库访问,所有查询都在进程内完成,故障范围清晰。 本篇增加了保险报价调用层,包括 Base URL 配置、超时控制、异常映射和调用日志。 现在 Petclinic 能优雅地处理远程服务的慢响应、HTTP 500 和网络断开,页面始终返回友好内容。 下一篇缺的是错误 JSON 的解析容错,以及更细粒度的线程池隔离方案。 官方文档首页:https://docs.spring.io/spring-boot/index.html > 本地数据本无忧, > 远程调用添新愁。 > 超时异常皆可挡, > 错误 JSON 待回头。 # 11、小结 ```text 三问三答:超时、选型、后果 ``` 第一个问题:为什么第一个远程依赖就要考虑超时、线程和故障传播? 因为远程调用把 Petclinic 的线程从本地计算资源变成了外部服务的等待队列,外部服务的不可控性直接转化为内部线程的失控占用。 第二个问题:同步 RestClient 还是响应式 WebClient? Petclinic 是典型的 MVC 应用,请求量可控,同步模型配合合理超时足够应对,引入 WebClient 意味着全链路响应式改造,收益远小于成本。 第三个问题:选错超时或选错模型的后果是什么? 选错超时导致线程池耗尽,系统雪崩;选错模型导致架构复杂度飙升,团队维护成本翻倍。 当前状态:Petclinic 已具备基本的远程调用容错能力,降级策略统一,日志可观测。 > 远程依赖需敬畏, > 超时线程两相随。 > 同步够用不盲从, > 故障传播有壁垒。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:Connection Timeout 和 Read Timeout 的区别是什么? Connection Timeout 是建立 TCP 连接的最大等待时间,Read Timeout 是连接建立后等待响应数据的最大时间。前者在网络不可达时触发,后者在服务端接受连接但不返回数据时触发。 ## 实战题:给 InsuranceQuoteClient 增加错误 JSON 的容错处理 在 fetchQuote 方法中捕获 RestClientResponseException 和 ResourceAccessException,对 JSON 解析错误返回默认报价对象。 ```java public InsuranceQuote fetchQuote(Long petId) { try { return restClient.get() .uri("/quotes/{petId}", petId) .retrieve() .body(InsuranceQuote.class); } catch (RestClientResponseException e) { log.warn("Remote error for pet {}: {}", petId, e.getStatusCode()); return InsuranceQuote.unavailable("报价服务返回错误"); } catch (ResourceAccessException e) { log.warn("Network error for pet {}: {}", petId, e.getMessage()); return InsuranceQuote.unavailable("报价服务暂时不可用"); } } ``` ## 排障题:保险公司接口返回 200 但 JSON 字段缺失,页面报错,如何排查? 先看 Petclinic 日志中是否有 HttpMessageNotReadableException,确认是 Jackson 反序列化失败。 再检查 InsuranceQuote 类的字段注解,确认是否有 @JsonProperty 映射缺失。 最后在 Client 层增加对 HttpMessageNotReadableException 的捕获,返回降级对象。 ## 架构判断题:什么场景下 Petclinic 才需要从 RestClient 切换到 WebClient? 当 Petclinic 需要同时发起多个远程调用且要求并发聚合时,或者需要流式处理响应时,才考虑 WebClient。 如果只是单个同步调用,WebClient 的响应式编程模型会让代码复杂度显著上升,收益不明显。 > 作业四题层层深, > 理解实战两不分。 > 排障架构皆历练, > 功夫到家自然成。 # 13、升华 ```text 工程结论:第一个远程依赖是架构分水岭 ``` Petclinic 从单体自足到引入第一个远程依赖,表面上是加了一个 HTTP 调用,实际上是故障模型的根本转变。 超时不是配置项,而是系统恢复能力的上限;异常映射不是代码技巧,而是故障分类的领域语言;线程占用不是性能指标,而是故障传播的物理通道。 同步 RestClient 配合合理超时,是 MVC 应用应对远程依赖的默认选择,WebClient 是特定场景的升级选项,不是技术时髦的装饰品。 下一篇将深入错误 JSON 的容错处理和线程池隔离,让 Petclinic 的远程调用层更加健壮。 > 首个依赖是分水, > 故障模型从此变。 > 超时异常线程占, > 三管齐下保平安。 **系列项目:** 以 Spring 官方 Petclinic 为唯一贯穿项目,24 篇持续演进同一个代码仓库,覆盖 Spring Boot 4 生产级应用全生命周期。 **已完成:** 环境搭建、启动机制、依赖与自动配置、配置体系、Web 层与安全机制、数据访问与事务管理。 **本篇:** 宠物保险报价服务的 RestClient 调用层,含 Base URL、超时控制、异常映射与调用日志。 **下一篇:** 错误 JSON 容错处理与远程调用线程池隔离方案。

优秀的个人博客,低调大师

《SpringBoot4进阶实战》第16招 日志要能还原现场

# 日志不是越多越好:用 MDC 让 Petclinic 一次请求从 Controller 到 Redis 全程可追踪 ```text 旧方案:默认 Logback,日志只有时间、级别、消息 → 冲突:线上偶发 500,日志里全是孤立片段,无法串联一次请求 → 后果:排障靠猜,定位以小时计 ``` 我是老李,负责把 Spring 官方 Petclinic 演进成生产级应用。今天要解决的是日志可追踪性问题。 线上出现偶发 500,如果没有请求上下文,再多日志是不是也只是更多噪声? 是给每个类加日志,还是设计一套贯穿请求的标识体系? 选错后果是什么?日志泄露敏感信息,或排障时面对海量碎片束手无策。 # 01、故事 ```text 凌晨 1:30 → 深圳某银行机房 → 任务是排查 Petclinic 偶发 500 → 冲突:日志刷屏却找不到同一请求的完整路径 ``` 凌晨 1:30,深圳某银行机房,空调嗡嗡作响。 任务是排查 Petclinic 线上偶发 500,用户报告“保存宠物时偶尔失败”。 我打开日志平台,搜索“ERROR”,瞬间刷出几百条,但每条都是孤立的。 没有 requestId,没有调用链,只有零散的方法名和异常堆栈。 我试着按时间戳拼凑,发现同一时刻有多个请求并发,根本无法区分谁是谁。 那一刻我意识到:日志不是越多越好,没有上下文的日志只是噪声。 > 日志如海量碎片, > 请求无痕难追寻。 > 偶发故障藏深处, > 唯有标识定乾坤。 # 02、问题 ```text 需求变化:从单机调试到分布式排障 → 冲突:默认 Logback 无请求上下文 → 后果:偶发 500 无法定位,日志成噪声 ``` Petclinic 默认使用 Logback,但配置极简,只有控制台输出,没有文件、没有滚动策略、没有格式定制。 更关键的是,日志中没有请求标识,一次 HTTP 请求经过 Controller、Service、Repository,产生的日志无法关联。 当线上出现偶发 500,比如数据库连接池超时或 Redis 抖动,你只能看到“Internal Server Error”,却不知道是哪个用户、哪个操作、经过了哪些服务。 影响是排障效率极低,甚至无法复现。可验证的验收标准是:一次请求的所有日志都带有相同 requestId,且能通过 requestId 过滤出完整调用链。 > 日志无标识, > 排障如盲行。 > 偶发五百错, > 何处觅真凶。 # 03、原理 ```text 核心机制:Logger Level 控制输出量,Appender 决定目的地,MDC 提供线程级上下文,Pattern 定义格式 ``` Logback 三大组件:Logger、Appender、Layout。Logger 负责分级,Appender 负责输出,Layout 负责格式化。 Logger Level 从 TRACE 到 ERROR,级别越低输出越多。生产环境通常设为 INFO,避免 DEBUG 刷屏。 MDC(Mapped Diagnostic Context)是 Logback 提供的线程级上下文,用 Map 存储键值对,Pattern 中通过 %X{key} 输出。 反直觉判断:MDC 不是自动填充的,需要你在入口处手动 put,在出口处 remove,否则线程池复用会导致上下文串扰。 另一个反直觉判断:日志级别不是越高越好,ERROR 太多会掩盖真正的问题,合理使用 WARN 和 INFO 才能平衡可观测性与噪声。 > 级别定多少, > 输出有门道。 > MDC 藏上下文, > 线程需自保。 # 04、架构 ```text 输入:HTTP 请求 → 本模块:Filter 生成 requestId 并写入 MDC → 输出:结构化日志,含 requestId、service、method、耗时 ``` 日志架构分三层:采集层(Logback 配置)、上下文层(MDC 填充)、输出层(结构化格式)。 采集层定义 Appender 和 Pattern,上下文层在 Filter 中生成 requestId,输出层将 MDC 值嵌入日志。 边界:MDC 只存在于当前线程,跨线程需手动传递;敏感信息如 password、token 绝不能写入 MDC 或日志。 收益是排障效率提升,代价是每个请求多一次 UUID 生成和 MDC 操作,但开销可忽略。 适用条件是所有请求都经过统一入口,比如 Spring MVC 的 Filter 或拦截器。 > 请求入网关, > 标识即生成。 > 日志带上下文, > 排障如神助。 # 05、第一版实战 ```text 环境:Spring Boot 3.2 + Logback 1.4 → 配置:logback-spring.xml → 代码:MDC Filter → 启动:验证 requestId 贯穿 ``` 先看 Petclinic 默认的 logback 配置,只有控制台输出,没有文件。 我们创建 `src/main/resources/logback-spring.xml`,配置控制台和文件两个 Appender,并加入 requestId 模式。 ```xml ${LOG_PATTERN} logs/petclinic.log logs/petclinic.%d{yyyy-MM-dd}.log 30 ${LOG_PATTERN} ``` 然后写一个 Filter,在请求入口生成 requestId 并放入 MDC。 ```java @Component public class RequestIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String requestId = UUID.randomUUID().toString().replace("-", ""); MDC.put("requestId", requestId); try { chain.doFilter(request, response); } finally { MDC.remove("requestId"); } } } ``` 启动 Petclinic,访问 `http://localhost:8080/owners/new`,控制台输出如下(示例输出): ```text 2025-01-15 10:23:45.123 INFO [http-nio-8080-exec-1] [a1b2c3d4e5f6] o.s.s.p.web.OwnerController - Creating new owner 2025-01-15 10:23:45.456 INFO [http-nio-8080-exec-1] [a1b2c3d4e5f6] o.s.s.p.service.OwnerService - Saving owner: John 2025-01-15 10:23:45.789 INFO [http-nio-8080-exec-1] [a1b2c3d4e5f6] o.s.s.p.repository.OwnerRepository - Insert owner into DB ``` 成功标准:同一请求的所有日志都带相同 requestId,且能通过 requestId 过滤。 未在当前环境实测,以上为预期结果。 > 配置加代码, > 标识入日志。 > 请求有踪迹, > 排障第一步。 # 06、排查 ```text 诊断树:问题1:MDC 值丢失 → 问题2:线程池复用导致串扰 → 问题3:敏感信息泄露 ``` **问题1:MDC 值丢失** 现象:部分日志没有 requestId。 怀疑:Filter 未生效或 MDC 被清除。 检查:查看 Filter 是否被 Spring 扫描,以及是否在异步线程中丢失。 证据:日志中部分行有 requestId,部分没有。 根因:Petclinic 使用 `@Async` 发送邮件,异步线程不继承 MDC。 修复:在异步任务中手动传递 MDC,或使用 `TaskDecorator`。 ```java @Bean public TaskDecorator mdcTaskDecorator() { return task -> { Map contextMap = MDC.getCopyOfContextMap(); return () -> { MDC.setContextMap(contextMap); try { task.run(); } finally { MDC.clear(); } }; }; } ``` **问题2:线程池复用导致串扰** 现象:不同请求的日志出现相同 requestId。 怀疑:MDC 未在 finally 中清除。 检查:查看 Filter 代码,确认是否在 finally 中 remove。 证据:两个不同用户的请求日志中 requestId 相同。 根因:线程池复用线程,MDC 残留导致串扰。 修复:确保在 finally 中 remove,并在线程池任务中清理。 **错误尝试:** 一开始我尝试在 Controller 中手动 put requestId,但发现 Service 层日志没有,因为 Controller 方法执行完后 MDC 被清除。 后来才意识到必须在 Filter 中统一处理。 > 排查如剥笋, > 层层见真因。 > 线程与异步, > 皆需谨慎行。 # 07、优化 ```text V1:只有 requestId → 证据:异步线程丢失、敏感信息风险 → V2:结构化日志 + 敏感信息过滤 ``` V1 实现了 requestId 贯穿,但日志仍是文本格式,难以机器解析。 而且,日志中可能包含用户输入,比如宠物名字,如果用户输入了密码或证件号,就会泄露。 V2 改为 JSON 结构化日志,并添加敏感信息过滤。 首先,在 logback-spring.xml 中配置 JSON 编码器,需要引入依赖。 ```xml net.logstash.logback logstash-logback-encoder 7.4 ``` 然后修改 Appender 的 encoder: ```xml true {"app":"petclinic"} ``` 同时,在代码中避免记录敏感字段,比如在 Owner 实体中,不打印 password 字段。 ```java public class Owner { private String password; // 禁止在日志中输出 @Override public String toString() { return "Owner{id=" + id + ", name='" + name + "'}"; // 不包含 password } } ``` > 文本转 JSON, > 结构更清晰。 > 敏感信息藏, > 安全有保障。 # 08、演进 ```text V2:结构化日志 + 敏感过滤 → 能力:可机器解析、可追踪 → 遗留:跨服务 TraceId 未实现 ``` V2 后,日志变成 JSON 格式,包含 requestId、timestamp、level、logger、message 等字段。 用同一组请求验证:访问 `/owners/new`,日志输出如下(示例输出): ```json {"timestamp":"2025-01-15T10:23:45.123Z","level":"INFO","thread":"http-nio-8080-exec-1","requestId":"a1b2c3d4","logger":"o.s.s.p.web.OwnerController","message":"Creating new owner"} {"timestamp":"2025-01-15T10:23:45.456Z","level":"INFO","thread":"http-nio-8080-exec-1","requestId":"a1b2c3d4","logger":"o.s.s.p.service.OwnerService","message":"Saving owner: John"} ``` 现在可以通过 requestId 在日志平台中一键过滤,排障效率大幅提升。 但遗留问题是:如果 Petclinic 调用远程 HTTP 服务,比如调用外部 API,requestId 无法传递到下游,需要引入 TraceId 机制。 收益是日志可读性和可搜索性增强,代价是 JSON 格式比文本略大,但可接受。 > 日志结构化, > 检索如飞驰。 > 跨服仍缺链, > 留待下回解。 # 09、洞见 ```text 从现象到判断:日志不是越多越好,而是越有上下文越好 ``` ## 日志级别是成本控制,不是越多越好 ```text 级别越高,输出越少,但信息越关键 ``` 生产环境用 INFO,DEBUG 只用于开发。如果线上开 DEBUG,日志量会爆炸,反而掩盖问题。 ## MDC 是线程级,不是请求级 ```text 线程池复用会导致 MDC 串扰 ``` 反直觉判断:MDC 不会自动清理,必须在 finally 中 remove,否则下一个请求会读到上一个请求的 requestId。 ## 敏感信息是日志的雷区 ```text password、token、cookie、证件号绝不能进日志 ``` 反直觉判断:即使你不在代码中打印,异常堆栈也可能包含参数值,比如 SQL 异常会打印完整 SQL,其中可能含用户输入。 ## 结构化日志是机器可读的基础 ```text JSON 格式让日志平台能自动索引和聚合 ``` 文本日志只能靠人眼搜索,JSON 日志可以按字段过滤,是构建可观测性的前提。 > 日志非越多, > 上下文为贵。 > 级别需权衡, > 安全不可废。 # 10、系统落地 ```text 入口:HTTP 请求 → 服务:Petclinic 应用 → 本模块:日志系统 → 存储:日志文件 → 未实现:跨服务 TraceId ``` 原来 Petclinic 只有默认 Logback,无文件输出,无 requestId。 本篇增加了 logback-spring.xml 配置、RequestIdFilter、结构化日志、敏感信息过滤。 现在能做什么:一次请求从 Controller 到 Repository 的日志都带 requestId,可通过日志平台快速定位。 下一篇缺什么:当 Petclinic 调用远程 HTTP 服务时,requestId 无法传递,需要引入 TraceId 和跨服务传播。 官方文档:Spring Boot Logging 文档(https://docs.spring.io/spring-boot/docs/current/reference/html/features. html#features.logging) > 日志系统立, > 请求有标识。 > 跨服待完善, > 演进不停息。 # 11、小结 ```text 三问三答:冲突是日志无上下文,选择是 MDC 方案,选错后果是排障低效或泄露 ``` 冲突问:线上偶发 500,没有请求上下文,日志再多也是噪声?答:是的,没有 requestId 的日志无法串联,排障如大海捞针。 选择问:是加日志还是设计标识体系?答:设计标识体系,用 MDC 贯穿请求,比盲目加日志更有效。 选错后果问:选错会怎样?答:要么日志泄露敏感信息,要么排障时面对海量碎片束手无策。 当前状态:Petclinic 已具备请求级日志追踪能力,日志结构化且安全。 > 三问已作答, > 日志有章法。 > 请求可追踪, > 排障不再怕。 # 12、作业 ```text 理解→实战→排障→架构判断 ``` ## 理解题:为什么 MDC 需要手动清理? 因为 MDC 是线程级变量,线程池复用线程时,如果不清理,下一个任务会读到上一个任务的上下文,导致日志串扰。 ## 实战题:在 Petclinic 中为异步邮件发送添加 MDC 传递 使用 TaskDecorator 在提交任务时复制 MDC,并在任务执行后清理。 ## 排障题:日志中出现其他请求的 requestId,如何排查? 检查 Filter 是否在 finally 中 remove,检查异步线程是否继承 MDC,检查线程池配置。 ## 架构判断题:是否所有日志都应记录 requestId? 不是。启动日志、定时任务日志等非请求上下文日志不应强制带 requestId,否则会误导排障。 > 作业四题明, > 实践出真知。 > 日志有边界, > 架构需权衡。 # 13、升华 ```text 工程结论:日志可观测性的核心是上下文,而非数量 ``` 通过 MDC 和 requestId,Petclinic 的日志从噪声变成了可追踪的证据。 但单机日志只是起点,下一篇将引入远程调用追踪,让 requestId 跨服务传播,形成完整的 TraceId 链路。 > 日志有上下文, > 排障如神助。 > 跨服待扩展, > 可观测更进。 **系列项目:** Spring 官方 Petclinic,一个宠物诊所管理系统,服务对象是诊所员工和客户。 **已完成:** 环境搭建、启动机制、依赖与自动配置、配置体系、Web 层开发、安全认证、数据访问、事务管理。 **本篇:** 日志系统升级,实现请求级 MDC 追踪、结构化日志和敏感信息过滤。 **下一篇:** 远程调用追踪,引入 TraceId 跨服务传播,让日志链路贯穿分布式系统。

优秀的个人博客,低调大师

《SpringBoot4进阶实战》第19招 DockerCompose改善开发体验

# Petclinic 开发环境工程化:用 Compose 消灭“我的机器能运行” ```text 旧方案:每个开发者本地分别安装 MySQL、Redis、模拟保险服务 冲突:版本不一致、端口冲突、初始化脚本漂移、新成员上手慢 后果:团队规模越大,“我的机器能运行”越昂贵,联调与排障成本指数上升 ``` Petclinic 从单体教学项目开始,逐渐接入 MySQL、Redis,还新增了一个模拟保险服务。 我是老李,最近团队从 3 个人扩到 9 个人,新同事第一天往往卡在装 MySQL、导数据、起 Redis 这些事上。 为什么团队规模变大以后,“我的机器能运行”会变成越来越昂贵的问题? 是继续让每个人手工安装组件,还是把开发依赖写成 Compose 基础设施即代码? 如果选错,会不会让新开发者 onboarding 从半天变成三天,甚至让联调环境永远无法复现? # 01、故事 ```text 周一 09:40 → 新同事工位 → 任务是跑起 Petclinic 开发环境 → 冲突:MySQL 版本不对、Redis 没装、保险服务起不来 ``` 周一早上,新同事小周入职,任务是 clone Petclinic 后把应用跑起来。 他先装 MySQL 8.4,又发现本地 3306 被旧项目占用,改端口后应用连不上。 接着装 Redis,结果测试用例要求 Redis 7,他装的是 6。 最后卡在模拟保险服务,那个服务没有安装包,只有一段 Python 脚本和一堆环境变量。 > 新机初装路九弯, > 版本参差端口残。 > 一人能跑非交付, > 团队复现始知难。 # 02、问题 ```text 需求变化:团队从 3 人扩到 9 人,开发依赖从 H2 变成 MySQL + Redis + 模拟保险服务 冲突:本地手工安装无法保证版本、端口、初始化脚本一致 后果:新成员 onboarding 超过一天,联调问题无法复现,交付节奏被拖慢 ``` 原来的 Petclinic 用 H2 内存库,开发者 clone 后直接 Run Application 就能启动。 现在为了贴近生产,数据层切到 MySQL,缓存用 Redis,还接入了模拟保险服务。 这些依赖如果继续靠口头文档和手工安装,每个人机器上的版本、配置、初始化数据都会漂移。 可验证的验收标准是:新开发者完成 git clone、启动 Docker、Run Application 三步后,应用能在 10 分钟内正常启动并通过健康检查。 > 依赖渐多如藤蔓, > 手工安装各不同。 > 一人能跑非标准, > 团队协作必成空。 # 03、原理 ```text Spring Boot Development-time Services 检测 compose.yml → 自动启动容器 → 通过 Service Connection 注入连接信息 ``` Spring Boot 3.1 起支持 Development-time Services,应用启动时如果检测到 classpath 下有 Docker Compose 模块,会读取根目录的 comp ose.yml。 它会在应用启动前自动执行 docker compose up,把 MySQL、Redis 等依赖容器拉起来。 应用停止时根据配置决定是否执行 docker compose down,这就是 Compose Lifecycle 管理。 Service Connection 会自动从容器元数据中提取主机、端口、用户名、密码,生成 ConnectionDetails,覆盖配置文件里的连接信息。 反直觉的是:compose.yml 里定义的端口映射并不是给应用用的,应用在容器网络内部直连服务名,只有你需要从宿主机调试时才需要 ports。 > 容器编排藏暗线, > 服务连接自动连。 > 端口映射非正道, > 网络内部自相牵。 # 04、架构 ```text 输入:git clone 后的源码 + Docker Desktop → 本模块:compose.yml + Spring Boot Dev Services → 输出:可运行的应用 + 一致的依赖环境 ``` 这一层位于开发环境基础设施层,不进入生产部署路径。 compose.yml 定义开发依赖:MySQL 8.4、Redis 7、模拟保险服务。 Spring Boot 通过 spring-boot-docker-compose 依赖读取 compose.yml,在应用启动前拉起容器。 Service Connection 负责把容器连接信息注入 DataSource、RedisConnectionFactory 以及自定义的保险服务客户端。 收益是环境一致性,代价是要求每个开发者安装 Docker Desktop 或等效容器运行时。 适用条件是开发与联调环境,不适用于生产 Kubernetes,因为生产需要滚动升级、资源配额、故障自愈等能力。 > 开发环境求一致, > 生产部署另有纲。 > 边界清晰方不乱, > 各司其职稳如桩。 # 05、第一版实战 ```text 环境:macOS + Docker Desktop 4.30 + JDK 21 + Spring Boot 3.3 → 配置:pom.xml + compose.yml → 代码:启动类 → 启动:Run Application → 验证:HTTP 请求 → V1 ``` 先确认版本:Spring Boot 3.3.0,JDK 21,Docker Desktop 4.30。 在 pom.xml 加入 docker-compose 依赖: ```xml org.springframework.boot spring-boot-docker-compose true ``` 在项目根目录创建 compose.yml: ```yaml services: mysql: image: mysql:8.4 environment: MYSQL_DATABASE: petclinic MYSQL_USER: petclinic MYSQL_PASSWORD: petclinic MYSQL_ROOT_PASSWORD: root ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:7.2 ports: - "6379:6379" insurance-service: image: petclinic/insurance-mock:1.0.0 environment: INSURANCE_PORT: 8081 ports: - "8081:8081" volumes: mysql-data: ``` 在 application.yml 中保留生产配置,但开发时让 Service Connection 覆盖: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/petclinic username: petclinic password: petclinic data: redis: host: localhost port: 6379 ``` 启动应用: ```bash ./mvnw spring-boot:run ``` 示例输出: ```text Using Docker Compose file 'compose.yml' Starting Docker Compose services... Container petclinic-mysql-1 started Container petclinic-redis-1 started Container petclinic-insurance-service-1 started ``` 验证请求: ```bash curl http://localhost:8080/owners curl http://localhost:8080/actuator/health ``` V1 成功标准:应用启动时自动拉起三个容器,健康检查返回 UP,owners 页面返回 200。 > 一键启动三容器, > 服务连接自动成。 > 新兵上手指日待, > 环境一致初显形。 # 06、排查 ```text 诊断树:应用启动失败 → 是容器未启动?是连接信息错误?是端口冲突?→ 逐层检查 ``` 第一个问题:应用启动时提示 Cannot connect to MySQL。 怀疑是容器没起来,检查容器状态: ```bash docker compose ps ``` 证据显示 mysql 容器处于 Restarting 状态。 查看日志: ```bash docker compose logs mysql ``` 根因是本地 3306 端口被旧 MySQL 占用,容器内 MySQL 无法绑定端口。 修复:把宿主机映射端口改为 3307,应用仍通过容器网络连接 3306。 第二个问题:Redis 连接被拒绝。 怀疑是 Service Connection 没有生效,检查启动日志: ```bash ./mvnw spring-boot:run | grep -i redis ``` 证据显示日志里没有 Redis ConnectionDetails 注入信息。 根因是 pom.xml 中 docker-compose 依赖被标记为 optional,但当前 IDE 运行配置没有包含该依赖。 修复:在 IDE 运行配置中勾选包含 optional 依赖,或改用命令行启动。 **错误尝试:** 一开始手动在 application.yml 里写死容器 IP,结果每次容器重启 IP 变化,连接又失败。 > 端口冲突寻常事, > 容器日志见真章。 > 连接注入需依赖, > 写死地址必遭殃。 # 07、优化 ```text V1:compose.yml 端口写死 3306 → 证据:宿主机端口冲突导致容器重启 → V2:宿主机端口改为 3307,应用走容器网络 ``` 把 compose.yml 中 MySQL 的宿主机端口改为 3307,避免和宿主机已有服务冲突: ```yaml services: mysql: image: mysql:8.4 environment: MYSQL_DATABASE: petclinic MYSQL_USER: petclinic MYSQL_PASSWORD: petclinic MYSQL_ROOT_PASSWORD: root ports: - "3307:3306" volumes: - mysql-data:/var/lib/mysql ``` 同时给模拟保险服务加上健康检查,确保 Service Connection 能正确等待服务就绪: ```yaml insurance-service: image: petclinic/insurance-mock:1.0.0 environment: INSURANCE_PORT: 8081 ports: - "8081:8081" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8081/health"] interval: 5s timeout: 3s retries: 10 ``` 在 application.yml 中明确关闭生产环境不需要的 Compose 生命周期管理: ```yaml spring: docker: compose: lifecycle-management: start-and-stop enabled: true ``` 这样开发时应用停止会自动执行 docker compose down,避免残留容器占用资源。 > 端口冲突改映射, > 健康检查候就绪。 > 生命周期明起止, > 残留容器不再余。 # 08、演进 ```text V2:compose.yml 端口隔离 + 健康检查 + 生命周期管理 → 能力:新开发者三步启动 → 遗留:生产环境仍需 Kubernetes ``` 用 V1 同一组请求验证: ```bash curl http://localhost:8080/owners curl http://localhost:8080/actuator/health ``` 现在新开发者流程变成: ```bash git clone https://github.com/spring-projects/spring-petclinic.git cd spring-petclinic docker compose up -d ./mvnw spring-boot:run ``` 收益是 onboarding 从一天缩短到 10 分钟,环境漂移问题消失。 代价是每个开发者必须安装 Docker Desktop,且首次拉取镜像需要网络带宽。 适用场景是开发与联调,不适用于生产,因为 Compose 缺少滚动升级、资源配额、故障自愈等生产级能力。 遗留问题是模拟保险服务的镜像还没有纳入 CI 构建流程,目前是手动构建后推送到本地镜像仓库。 > 三步启动成新标, > 环境漂移自此消。 > 生产仍需 K8s 管, > 开发生产各一条。 # 09、洞见 ```text 从“我的机器能运行”到“团队环境一致”的工程判断 ``` ## 反直觉判断:端口映射不是给应用用的 ```text compose.yml ports → 宿主机调试用 应用连接 → 容器网络服务名 ``` 很多开发者以为应用必须通过 localhost:3306 连接 MySQL,实际上 Spring Boot Dev Services 在容器网络内部直连服务名 mysql,端口映射只是为了方便你在宿主 机用数据库工具查看数据。 ## 反直觉判断:开发环境越接近生产,反而越需要自动化 ```text 依赖越多 → 手工配置越不可靠 → 自动化收益越大 ``` 当依赖只有 H2 时,手工配置成本几乎为零;当依赖变成 MySQL、Redis、模拟服务时,手工配置的出错概率和沟通成本急剧上升,自动化反而成为唯一可持续的方案。 ## 判断:Compose 是开发契约,不是部署方案 ```text compose.yml → 开发环境契约 生产部署 → Kubernetes Manifest ``` 把 compose.yml 当作团队内部的开发环境契约,它定义了依赖的版本、配置、初始化方式; 生产部署应该由独立的 Kubernetes Manifest 或 Helm Chart 管理,两者边界清晰才能避免“开发环境能跑,生产环境跑不起来”的困境。 > 端口映射非正道, > 容器网络自相连。 > 依赖越多越自动, > 开发生产各一边。 # 10、系统落地 ```text 入口:git clone + Docker Desktop → 服务:Spring Boot Dev Services → 本模块:compose.yml + Service Connection → 存储:MySQL + Redis + 模拟保险服务 → 未实现:生产 Kubernetes 部署 ``` 原来 Petclinic 只有 H2 内存库,开发者 clone 后直接启动。 本篇增加了 compose.yml 定义 MySQL、Redis、模拟保险服务,通过 Spring Boot Development-time Services 自动拉起容器,通过 Service C onnection 注入连接信息。 现在新开发者完成 git clone、Docker、Run Application 三步就能开始工作,不再需要手工配置中间件。 下一篇缺的是把这些开发依赖的初始化数据、版本升级策略、CI 集成纳入自动化流程,以及为生产环境编写独立的 Kubernetes 部署清单。 官方链接:https://spring.io/projects/spring-boot > 开发环境已成形, > 三步启动告功成。 > 生产部署尚缺位, > 下篇再续 K8s 程。 # 11、小结 ```text 三问三答:为什么昂贵?怎么选?选错后果? ``` 为什么团队规模变大后“我的机器能运行”越来越昂贵?因为每个开发者的环境漂移导致联调问题无法复现,沟通和排障成本随人数指数上升。 怎么选?选择把开发依赖写成 compose.yml,用 Spring Boot Dev Services 自动管理容器生命周期和连接注入。 选错后果?如果继续手工安装,新开发者 onboarding 会从 10 分钟变成一整天,团队交付节奏被拖慢,最终失去工程化能力。 当前状态:Petclinic 开发环境已经实现基础设施即代码,新成员可以三步启动,环境一致性问题基本解决。 > 三问三答明方向, > 环境一致是良方。 > 工程化路初起步, > 生产部署待加强。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:为什么应用连接 MySQL 时不用 localhost:3306,而用服务名 mysql? 因为 Spring Boot Dev Services 在 Docker Compose 创建的容器网络内部运行,应用容器和 MySQL 容器在同一个网络中,直接通过服务名 mysql 解析到容器 I P,端口是容器内部的 3306。 localhost:3306 是宿主机视角,只有当你从宿主机用数据库工具连接时才需要。 ## 实战题:给 compose.yml 增加一个 Kafka 依赖,并让 Spring Boot 应用通过 Service Connection 连接它。 在 compose.yml 中加入 kafka 服务,使用 bitnami/kafka 镜像,配置 KAFKA_CFG_LISTENERS 等环境变量。 在 pom.xml 中加入 spring-kafka 依赖,Spring Boot 会自动检测 Kafka 容器并注入 KafkaConnectionDetails。 启动应用后,在日志中确认 Kafka ConnectionDetails 被注入,然后发送一条测试消息验证。 ## 排障题:应用启动时提示 Cannot connect to Redis,但 docker compose ps 显示 Redis 容器正在运行,如何排查? 先检查应用日志中是否有 Redis ConnectionDetails 注入信息,确认 spring-boot-docker-compose 依赖是否在运行 classpath 中。 然后检查 Redis 容器日志,确认是否已就绪。 最后检查 compose.yml 中 Redis 服务名是否与 Service Connection 期望的一致,以及是否有端口冲突导致容器内 Redis 未监听 6379。 ## 架构判断题:团队领导要求把 compose.yml 直接部署到生产环境,理由是“开发环境能跑,生产环境也能跑”,你是否同意? 不同意。compose.yml 是开发环境契约,缺少生产环境需要的滚动升级、资源配额、故障自愈、多副本调度等能力。 生产环境应该使用 Kubernetes Manifest 或 Helm Chart,由独立的部署流水线管理。 开发 Compose 与生产 Kubernetes 的边界必须清晰,否则会导致生产环境不可控。 > 四题作业层层进, > 理解实战排障明。 > 架构判断最关键, > 开发生产界限清。 # 13、升华 ```text 工程结论:开发环境的一致性必须通过基础设施即代码来保证,而不是依赖个人自觉 ``` “我的机器能运行”在团队规模变大后必然失效,因为环境漂移带来的沟通和排障成本会吞噬所有效率收益。 把开发依赖写入 compose.yml,用 Spring Boot Development-time Services 自动管理容器生命周期和连接注入,是当前 Spring Boot 生态下最务实的方 案。 开发 Compose 与生产 Kubernetes 的边界必须清晰:前者是开发契约,后者是部署方案,两者不能混用。 下一篇将把这些开发依赖的初始化数据、版本升级策略、CI 集成纳入自动化流程,并开始编写生产环境的 Kubernetes 部署清单。 > 环境一致靠代码, > 个人自觉难久长。 > 开发生产分两界, > 工程化路始成章。 **系列项目:** Spring 官方 Petclinic 教学项目,服务对象为宠物医院诊疗与客户管理系统。 **已完成:** 环境搭建、启动机制、依赖与自动配置、配置体系、Web 层、安全层、数据层、事务管理、远程调用、数据库迁移、缓存、并发控制、日志、测试、Testcontainers、Docker Compose 开发环境。 **本篇:** 将 MySQL、Redis、模拟保险服务统一写入 compose.yml,实现 Development-time Services、Compose Lifecycle 与 Service Connection,让新开发者三步启动。 **下一篇:** 将开发依赖的初始化数据、版本升级策略、CI 集成纳入自动化流程,并编写生产 Kubernetes 部署清单。

优秀的个人博客,低调大师

《SpringBoot4进阶实战》第1招 搭好Java开发环境

# 为什么正式教程应先验证完整工程环境,而不是从复制第一段 Java 代码开始 ```text 旧方案:复制第一段 Java 代码 → 本地缺 JDK/依赖/数据库 → 编译失败、连接失败、调试失败 → 学习者以为框架难用 ``` 很多 Spring 教程一上来就贴 Controller、Service、Repository,默认读者已经装好 JDK、IDE、Maven、数据库和调试工具。 但真实工程里,环境不一致才是第一道墙。 我是老李,带团队接手过不少“代码能跑、环境跑不起来”的项目。 一个正式教程应该先验证完整工程环境,还是先复制第一段 Java 代码? 应该把 JDK、Maven、IDE、Docker、MySQL、Redis、HTTP 调试和 JVM 诊断一次打通,还是只保证单文件编译通过? 如果选错,会不会让后续每一章都在修环境,而不是学 Spring Boot? # 01、故事 ```text 周一 09:40 → 新同事工位 → 任务是跑起 Petclinic → 本地 JDK 版本不对、MySQL 没装、Debug 不会看线程 → 项目启动失败 ``` 周一早上,新同事被要求把 Spring 官方 Petclinic 跑起来。 任务是“先让项目在本机启动,能访问首页”。 他打开 IDE,导入项目,Maven 一直下载失败。 旁边同事催着要联调,他只能反复重启 IDE。 搜到的答案一会儿说改镜像,一会儿说装 JDK 17,一会儿又说要本地 MySQL。 他卡了两个小时,连 main 方法都没进去。 > 晨会催得急, > 环境未通先写码。 > 报错层层叠, > 不知根在哪。 # 02、问题 ```text 需求变化:从“能编译”到“能开发、能调试、能连库、能观测” → 旧环境只验证 javac → 数据库、容器、调试、诊断全部失效 ``` 只验证 javac 或 IDE 里能打开文件,并不能证明工程环境可用。 Petclinic 需要 Maven Wrapper 下载依赖,需要 Docker 启动 MySQL 和 Redis,需要 IDE 连接源码和运行进程。 如果这些链路不通,后续数据访问、缓存、事务、测试、容器化章节都会反复失败。 可验证的验收标准是:执行 ./mvnw test 通过,./mvnw spring-boot:run 能启动,HTTP 请求能访问首页,Debug 能停在断点,MySQL 和 Redis 能通过客户端 验证。 > 编译非终点, > 链路未通皆虚妄。 > 库连不上时, > 代码再美也空转。 # 03、原理 ```text 开发链 = JDK → Maven Wrapper → IDE → Docker → MySQL/Redis → Spring Boot → HTTP → Debug/JVM 诊断 ``` Spring Boot 不是只靠 Java 文件运行。 它启动时要解析依赖、读取配置、连接数据源、暴露端口、注册 Bean。 Maven Wrapper 固定 Maven 版本,避免本机 Maven 差异。 Docker 统一 MySQL 和 Redis 版本,避免“我本地能跑,你本地不行”。 IDE 的 Debug 能力依赖源码与运行进程的对应关系。 JVM 诊断工具依赖 JDK 自带命令与运行用户权限。 反直觉的是:环境验证越早,业务代码写得越慢,但整体交付越快。 > 慢即是快时, > 先铺管道后引水。 > 若先砌高墙, > 水到墙前也白费。 # 04、架构 ```text 输入:官方 Petclinic 源码 + 本机开发机 → 环境层:JDK/IDE/Maven/Docker/MySQL/Redis/HTTP/JVM 工具 → 输出:可运行、可调试、可观测的工程基线 ``` 这一篇不新增业务模块,而是建立工程基线。 基线分四层:语言运行时、构建工具、外部依赖、观测调试。 语言运行时是 JDK 21,构建工具是 Maven Wrapper,外部依赖是 Docker 中的 MySQL 和 Redis,观测调试是 IDE Debug 与 JDK 诊断命令。 收益是后续每章都能在一致环境上验证。 代价是首次搭建时间较长。 适用条件是所有需要长期演进同一个代码仓库的正式项目。 > 基线如地基, > 不显眼却承千钧。 > 工具链贯通, > 后续方可行远路。 # 05、第一版实战 ```text 环境:macOS/Linux/Windows + JDK 21 + IntelliJ IDEA + Docker Desktop → 配置:JAVA_HOME、Maven Wrapper、MySQL、Redis → 代码:官方 Petclinic → 启动:./mvnw spring-boot:run → 验证:HTTP 200、Debug 断点、redis-cli PING → V1:全链路首次跑通 ``` 第一步安装 JDK 21。 以 macOS 为例: ```bash brew install openjdk@21 ``` 配置 JAVA_HOME: ```bash export JAVA_HOME=$(/usr/libexec/java_home -v 21) export PATH="$JAVA_HOME/bin:$PATH" ``` 验证: ```bash java -version javac -version ``` 示例输出: ```text openjdk version "21.0.2" 2024-01-16 OpenJDK Runtime Environment (build 21.0.2+13-58) OpenJDK 64-Bit Server VM (build 21.0.2+13-58, mixed mode, sharing) javac 21.0.2 ``` 第二步安装 IntelliJ IDEA,打开 Petclinic 后设置 Project SDK 为 JDK 21。 在 IDEA 中确认 Maven Wrapper 使用项目自带 .mvn/wrapper。 第三步安装 Docker Desktop 并启动。 启动 MySQL: ```bash docker run -d --name petclinic-mysql \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=petclinic \ -e MYSQL_USER=petclinic \ -e MYSQL_PASSWORD=petclinic \ -p 3306:3306 \ mysql:8.4 ``` 启动 Redis: ```bash docker run -d --name petclinic-redis \ -p 6379:6379 \ redis:7.4 ``` 验证 Redis: ```bash docker exec -it petclinic-redis redis-cli PING ``` 示例输出: ```text PONG ``` 第四步克隆官方项目: ```bash git clone https://github.com/spring-projects/spring-petclinic.git cd spring-petclinic ``` 执行测试: ```bash ./mvnw test ``` 启动应用: ```bash ./mvnw spring-boot:run ``` 访问: ```bash curl -I http://localhost:8080 ``` 示例输出: ```text HTTP/1.1 200 ``` 第五步在 IDEA 中给 PetClinicController 的 showOwner 方法打断点,用 Debug 启动,浏览器访问 /owners/find,确认断点命中。 再练习条件断点、异常断点、Evaluate Expression 和线程查看。 第六步验证 JVM 诊断工具: ```bash jcmd jstack jmap -histo ``` V1 完成标志:JDK、Maven、IDE、Docker、MySQL、Redis、Spring Boot、HTTP、Debug 全部跑通。 > 一链通到底, > 从编译到断点停。 > 库中表可见, > 缓存 PONG 回应声。 # 06、排查 ```text 诊断树:启动失败 → 是 JDK 版本?是 Maven 依赖?是数据库连接?是端口占用?是 Docker 未启动? ``` 问题一:./mvnw 报 Unsupported class file major version 65。 怀疑是 Maven 使用的 JDK 版本低于 21。 检查: ```bash ./mvnw -version ``` 证据显示 Maven 运行在 JDK 17 上。 根因是 JAVA_HOME 未生效或 IDE 终端继承了旧环境变量。 修复:重新 export JAVA_HOME 并重启终端。 问题二:应用启动报 Access denied for user 'petclinic'@'localhost'。 怀疑是 MySQL 账号权限或密码不匹配。 检查: ```bash docker exec -it petclinic-mysql mysql -upetclinic -ppetclinic -e "SELECT 1" ``` 证据显示账号无法登录。 根因是容器创建时 MYSQL_USER 与 MYSQL_PASSWORD 未同时生效,或旧容器残留。 修复:删除旧容器和 volume,重新创建。 **错误尝试:** 一开始只改 application.properties 里的密码,没有重建容器,导致反复报同样的错。 > 报错如迷雾, > 先查版本再查权。 > 莫急改配置, > 证据未齐勿下判。 # 07、优化 ```text V1:手动 export JAVA_HOME、手动 docker run → 证据:换终端失效、容器参数易漏 → V2:固定 JDK 配置、用 Docker Compose 管理 MySQL 和 Redis ``` V1 的问题在于环境配置散落在多个终端和命令历史里。 换一个终端窗口,JAVA_HOME 就失效。 MySQL 和 Redis 每次手动 docker run,参数容易漏。 V2 把 JDK 配置写入 shell 配置文件: ```bash echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 21)' >> ~/.zshrc source ~/.zshrc ``` 再用 Docker Compose 管理 MySQL 和 Redis: ```yaml services: mysql: image: mysql:8.4 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: petclinic MYSQL_USER: petclinic MYSQL_PASSWORD: petclinic ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:7.4 ports: - "6379:6379" volumes: mysql-data: ``` 启动: ```bash docker compose up -d ``` 这样环境可重复、可版本化。 > 散落终成患, > 配置入文件方安。 > 容器归编排, > 一键起落不慌乱。 # 08、演进 ```text V2:JDK 配置持久化 + Docker Compose 管理依赖 → 能力:环境可重复、可重建、可交接 → 遗留:IDE 配置和 JVM 诊断脚本尚未标准化 ``` V2 之后,新同事拿到项目,只需安装 JDK 21、IDEA、Docker Desktop,然后执行 docker compose up -d 和 ./mvnw spring-boot:run。 用 V1 同一组请求验证:./mvnw test 通过,curl -I http://localhost:8080 返回 200,redis-cli PING 返回 PONG,Debug 断点命中。 收益是环境搭建从两小时降到二十分钟。 代价是首次需要理解 Docker Compose 文件。 适用条件是团队统一使用容器化外部依赖。 遗留问题是 IDE 的 Run Configuration 和 JVM 诊断命令还没有沉淀成团队文档。 > 从散到有序, > 环境不再靠口传。 > 新人接手工, > 二十分钟可开战。 # 09、洞见 ```text 从“先写代码”到“先验证环境”,本质是从个人英雄主义转向工程基线思维 ``` ## 环境是代码的运行时契约 ```text 代码 → 依赖 → 配置 → 外部服务 → 端口 → 观测 ``` 代码只是链路的一环。 没有 JDK、Maven、数据库和调试工具,代码无法被验证。 正式教程先验证环境,是在确认运行时契约成立。 ## 反直觉判断:先慢后快 ```text 首日搭环境 2 小时 → 后续每章省 30 分钟 → 24 章省 12 小时 ``` 很多人觉得搭环境浪费时间。 但环境问题不会消失,只会推迟到最不方便的时候爆发。 ## 反直觉判断:容器不是运维的事 ```text 开发库也进容器 → 版本一致 → 减少“我本地能跑” ``` 开发阶段就用容器,能提前暴露配置、网络和权限问题。 等到上线才容器化,往往已经积累了大量环境假设。 ## 调试能力是工程素养 ```text 断点 → 条件 → 异常 → 表达式 → 线程 → JVM 命令 ``` 只会普通断点的人,遇到偶发问题只能加日志重启。 条件断点、异常断点和线程查看能大幅缩短定位时间。 > 契约先立定, > 慢行反是快行舟。 > 容器非运维, > 调试素养不可丢。 # 10、系统落地 ```text 入口:开发者本机 → 服务:IDE + Maven Wrapper → 本模块:环境基线 → 存储:Docker MySQL/Redis → 未实现:业务代码尚未改动 ``` 原来只有官方 Petclinic 源码,没有统一的本地环境说明。 本篇增加了 JDK 21、Maven Wrapper、IDEA Debug、Docker Compose、MySQL、Redis、HTTP 调试和 JVM 诊断工具的环境基线。 现在能做什么:任何新成员可以在二十分钟内跑起 Petclinic,并具备调试和观测能力。 下一篇缺什么:还没有深入 Spring Boot 启动机制,需要解释 main 方法如何拉起内嵌 Tomcat 和 Spring 容器。 官方链接:https://github.com/spring-projects/spring-petclinic > 基线已铺就, > 开发调试皆可依。 > 下一程启程, > 深入启动探玄机。 # 11、小结 ```text 三问三答:先验证环境 → 先打通全链路 → 选错会反复修环境 ``` 第一问:正式教程应该先验证完整工程环境,因为环境是代码的运行时契约。 第二问:应该把 JDK、Maven、IDE、Docker、MySQL、Redis、HTTP 调试和 JVM 诊断一次打通,因为任何一环缺失都会阻塞后续章节。 第三问:如果选错,后续每一章都会在修环境,而不是学 Spring Boot,最终失去对框架本身的信心。 当前状态:Petclinic 已在本机稳定运行,测试通过,数据库和缓存可验证,Debug 和 JVM 诊断可用。 > 三问皆已答, > 环境先行是正解。 > 链路全贯通, > 后续章节少波折。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:为什么 Maven Wrapper 比本机全局 Maven 更适合正式教程? Maven Wrapper 把 Maven 版本固定在项目里,所有开发者执行 ./mvnw 时使用同一版本,避免本机 Maven 版本差异导致的构建不一致。 ## 实战题:用 Docker Compose 启动 MySQL 和 Redis,并分别验证连接、建表、SET/GET 和 TTL。 执行 docker compose up -d 后,用 docker exec -it petclinic-mysql mysql -upetclinic -ppetclinic petclinic 连接,执行 SHOW TABLES; 用 docker exec -it petclinic-redis redis-cli 执行 SET k v、GET k、TTL k。 ## 排障题:应用启动报 Communications link failure,但 MySQL 容器在运行,如何定位? 先检查端口映射:docker port petclinic-mysql。 再检查应用配置的数据库地址是否为 localhost:3306。 再检查 MySQL 是否已接受连接:docker logs petclinic-mysql。 最后检查防火墙或 Docker 网络是否隔离。 ## 架构判断题:开发环境是否应该完全依赖本地安装的 MySQL,而不是容器? 不应该。 本地安装的 MySQL 版本、配置和权限难以统一,容器能保证开发、测试、生产环境的一致性,减少“我本地能跑”的问题。 > 四题如四关, > 理解实战排障判。 > 环境既已通, > 后续深入不迷航。 # 13、升华 ```text 工程结论:先验证完整环境,再写业务代码,是正式教程的第一原则 ``` 有依据的结论是:环境问题不会因为忽略而消失,只会推迟到更贵的阶段爆发。 Petclinic 作为长期演进项目,必须先把开发、运行、调试、观测的基线打牢。 下一篇将进入 Spring Boot 启动机制,解释 main 方法如何触发自动配置、内嵌容器和 Bean 装配。 > 地基既已深, > 高楼方可层层起。 > 环境为序章, > 启动机制是正题。 **系列项目:** Spring 官方 Petclinic 教学项目,服务兽医诊所的宠物与主人管理。 **已完成:** JDK 21、Maven Wrapper、IntelliJ IDEA、Docker Desktop、MySQL、Redis、HTTP 调试、JVM 诊断工具与 Petclinic 首次运行。 **本篇:** 建立可重复、可调试、可观测的完整工程环境基线。 **下一篇:** 深入 Spring Boot 启动机制,理解 main 方法、自动配置与内嵌容器。

优秀的个人博客,低调大师

《SpringBoot4进阶实战》第2招 跑透官方Petclinic

# Spring Boot 替 Petclinic 完成了哪些基础设施 ```text 旧方案:手工搭 Tomcat、Spring 容器、JPA、事务、数据源、静态资源、日志、安全 冲突:教学项目 Petclinic 开箱即跑,开发者却说不清哪些是框架代劳 后果:后续演进没有基线,改一处配置就牵出隐藏依赖,排障靠猜 ``` Spring 官方 Petclinic 是很多人第一次见到的“完整 Spring 应用”。它看起来不大,却同时包含 Web 层、数据层、校验、安全、测试和可观测入口。 我是老李,这次不新建 Hello World,而是把 Petclinic 作为后面 22 招持续演进的代码母本。 如果连“Spring Boot 到底替我们做了什么”都说不清,后续加缓存、加迁移、加容器化时,凭什么判断改动边界? 是继续把 Petclinic 当黑盒跑一遍,还是从启动、请求、数据三层把它拆成可解释的基线? 选错会怎样?选错后,团队只能背诵配置,无法在故障时定位是容器、框架还是业务代码的问题。 # 01、故事 ```text 周二 09:40 → 工位 → 任务是跑通 Petclinic 并追一条真实请求 → 冲突是能启动但说不清启动过程 ``` 周二上午,项目组要求把官方 Petclinic 作为新一版内部实验项目的起点。 任务是先跑起来,再执行“查询主人→新增宠物→登记就诊”,最后从浏览器请求追到数据库。 我打开官方仓库,依赖下载完成后应用确实起来了,页面也能点。 但同事问了一句:“除了写 Controller 和 Repository,Spring Boot 还帮我们干了什么?” 我盯着启动日志,只能说出“内嵌 Tomcat”和“自动配置”,再往下就空了。 > 页面已开跑,链路却成谜, > 框架代劳处,无人说得齐。 > 若把黑盒当基线, > 后招皆是空中梯。 # 02、问题 ```text 需求变化:从“能跑”变成“能解释、能演进” 冲突:Petclinic 的启动、请求、数据链路被自动配置掩盖 后果:无法建立后续实验共用的工程基线 ``` Petclinic 能跑,不代表团队掌握了它。 真正的问题是:当后续要加缓存、迁移、容器化、可观测时,我们不知道现有能力来自哪里。 例如数据源是谁创建的,事务是谁开启的,静态资源是谁映射的,安全过滤链是谁装配的。 这些如果不查清,后续任何一处改动都可能触发隐藏依赖。 可验证的验收标准是:用三种方式启动同一份代码,并解释从 HTTP 请求到数据库返回的完整路径。 > 能跑只是表象, > 能解释才算账。 > 黑盒之上无演进, > 基线之下有文章。 # 03、原理 ```text Spring Boot 核心机制:自动配置 + 条件装配 + 内嵌容器 + 起步依赖 ``` Spring Boot 不是新框架,而是 Spring 的“工程化装配器”。 它通过 `@EnableAutoConfiguration` 扫描 classpath 中的依赖,再按条件装配 DataSource、EntityManagerFactory、TransactionM anager、DispatcherServlet 等基础设施。 Petclinic 只声明了 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`spring-boot-starter-thymele af` 等起步依赖。 这些起步依赖本质是依赖集合,它们把“你需要什么能力”翻译成“引入哪些 jar”。 反直觉的是:Petclinic 里没有一行创建 Tomcat 的代码,但应用却能接收 HTTP 请求。 这是因为 `spring-boot-starter-web` 引入了内嵌 Tomcat,自动配置在启动时把它初始化并绑定到指定端口。 > 不见 Tomcat 影, > 却有请求声。 > 起步依赖藏装配, > 条件注解定死生。 # 04、架构 ```text 输入:HTTP 请求 → 本模块:Controller → 业务层 → Repository → 数据库 → 输出:页面或 JSON ``` Petclinic 采用经典分层:Web 层负责接收和渲染,业务层负责协调,Repository 负责持久化。 Spring Boot 在这条链路之外,提供了容器、配置、数据源、事务、静态资源、错误页、日志等横切能力。 本阶段只关注“启动与请求链路”,不引入新的业务模块。 收益是团队能建立统一解释框架:每一层由谁创建、由谁调用、由谁销毁。 代价是前期要花时间读源码和日志,不能只满足于页面可点。 适用条件是:后续所有实验都基于同一份 Petclinic 代码,不再另起新项目。 > 分层本不奇, > 装配才是基。 > 若问边界何处止, > 先看自动配置里。 # 05、第一版实战 ```text 环境:JDK 17 + Maven + 官方 Petclinic → 配置:默认 application.properties → 代码:不改业务 → 启动:三种方式 → 验证:真实请求 → V1:可跑但未解释 ``` 第一步,克隆官方仓库并创建自己的实验仓库。 ```bash git clone https://github.com/spring-projects/spring-petclinic.git spring-boot-petclinic-advanced-lab cd spring-boot-petclinic-advanced-lab git remote rename origin upstream git branch baseline ``` 第二步,确认关键类。 ```java @SpringBootApplication public class PetClinicApplication { public static void main(String[] args) { SpringApplication.run(PetClinicApplication.class, args); } } ``` 第三步,用 IDEA 启动。 在 IDEA 中打开项目,等待依赖下载完成,直接运行 `PetClinicApplication`。 示例输出: ```text Started PetClinicApplication in 8.2 seconds Tomcat started on port 8080 ``` 第四步,用 Maven 启动。 ```bash ./mvnw spring-boot:run ``` 第五步,构建可执行 Jar 并启动。 ```bash ./mvnw clean package -DskipTests java -jar target/spring-petclinic-*.jar ``` 第六步,验证真实链路。 ```bash curl -s http://localhost:8080/owners?lastName= | head -n 20 ``` 页面返回主人列表。 再执行新增宠物和登记就诊: ```bash curl -s -X POST http://localhost:8080/owners/1/pets/new -d "name=Leo&birthDate=2023-01-01&type=cat" curl -s -X POST http://localhost:8080/owners/1/pets/1/visits/new -d "date=2024-01-01&description=check" ``` 未在当前环境实测,以下为预期结果:新增宠物后跳转主人详情页,登记就诊后显示就诊记录。 V1 成功标准:三种启动方式均可运行,真实请求可到达数据库。 > 三种启动皆可跑, > 一条链路已通到。 > 只是框架代劳处, > 仍似雾里看花早。 # 06、排查 ```text 诊断树:启动慢 → 怀疑依赖下载 → 检查本地仓库 → 证据:首次下载慢 → 根因:冷缓存 请求 404 → 怀疑映射缺失 → 检查 Controller → 证据:路径正确 → 根因:静态资源未刷新 ``` 第一个问题是启动慢。 现象:首次运行 `./mvnw spring-boot:run` 超过两分钟。 怀疑:网络或依赖下载慢。 检查: ```bash ./mvnw -version ls ~/.m2/repository/org/springframework/boot ``` 证据:本地仓库缺少大量依赖,首次启动触发全量下载。 根因:冷缓存,不是代码问题。 修复:等待下载完成,后续启动恢复正常。 第二个问题是页面样式丢失。 现象:首页能打开,但 CSS 不生效。 怀疑:静态资源映射错误。 检查: ```bash curl -I http://localhost:8080/resources/css/petclinic.css ``` 证据:返回 404。 根因:浏览器缓存了旧路径,应用实际静态资源路径已变化。 修复:强制刷新浏览器或清缓存。 **错误尝试:** 我曾以为静态资源丢失是打包问题,重新执行 `clean package` 后仍未解决,最后发现只是浏览器缓存。 > 慢非代码错, > 冷缓存惹祸。 > 样式失踪非打包, > 浏览器旧路多。 # 07、优化 ```text V1:能跑但不可解释 → 证据:启动日志与请求日志未结构化 → V2:增加启动报告与链路说明 ``` V1 已经能跑,但团队仍无法回答“Spring Boot 做了什么”。 优化方向不是改业务,而是把隐式装配显式化。 第一步,开启启动报告。 在 `application.properties` 中增加: ```properties logging.level.org.springframework.boot.autoconfigure=DEBUG ``` 重新启动后,日志会打印自动配置报告。 示例输出: ```text Positive matches: DataSourceAutoConfiguration matched JpaRepositoriesAutoConfiguration matched TransactionAutoConfiguration matched ``` 第二步,增加一个只读接口,用于查看当前生效的关键基础设施。 ```java @RestController @RequestMapping("/lab") public class LabInfoController { @GetMapping("/infra") public Map infra() { return Map.of( "server", "embedded-tomcat", "data", "jpa", "tx", "declarative", "mvc", "dispatcher-servlet" ); } } ``` 第三步,验证: ```bash curl -s http://localhost:8080/lab/infra ``` 预期返回: ```json {"server":"embedded-tomcat","data":"jpa","tx":"declarative","mvc":"dispatcher-servlet"} ``` 这样,团队有了一个可访问的“基础设施清单”。 > 能跑不够用, > 能查才从容。 > 自动配置报告里, > 隐式装配现真容。 # 08、演进 ```text V2:可解释的启动与请求链路 → 能力:三种启动方式 + 自动配置报告 + 基础设施清单 → 遗留:安全、缓存、迁移未展开 ``` V2 相比 V1,核心变化是从“能跑”到“能解释”。 用 V1 同一组请求验证: ```bash curl -s http://localhost:8080/owners?lastName= curl -s http://localhost:8080/lab/infra ``` 两个请求均正常返回,说明新增只读接口没有破坏原有链路。 收益是团队建立了统一解释框架:容器、数据、事务、MVC 都由 Spring Boot 自动装配。 代价是增加了一个实验用接口,后续需要控制其访问范围。 适用条件是:所有后续实验都基于当前仓库的 `baseline` 分支继续演进。 遗留问题是:安全过滤链、缓存抽象、数据库迁移、可观测能力尚未展开。 > 黑盒已开窗, > 链路有方向。 > 后续二十二招, > 皆在此基上。 # 09、洞见 ```text 从现象到判断:启动快慢、请求路径、配置来源三者共同决定工程基线质量 ``` ## 自动配置不是魔法,而是条件装配 ```text classpath 依赖 → 条件注解 → Bean 装配 ``` 很多人以为 Spring Boot 会“猜”你要什么。 实际上它只做一件事:根据 classpath 和显式配置,决定装配哪些 Bean。 反直觉判断:自动配置越少越稳定,而不是越多越强大。 ## 内嵌容器改变了部署模型 ```text 传统:war + 外部 Tomcat Boot:jar + 内嵌 Tomcat ``` Petclinic 的可执行 Jar 同时包含应用和容器。 这让本地开发、测试、部署使用同一种启动方式。 反直觉判断:内嵌容器不是为生产替代外部容器,而是为一致性降低环境差异。 ## 起步依赖是工程化契约 ```text starter-web → 一组依赖 + 自动配置 ``` 起步依赖把“能力”打包成“契约”。 团队引入一个 starter,就等于接受一组默认行为。 因此,引入前必须知道它带来了什么,否则后续排障会非常困难。 ## 基线不是代码快照,而是解释能力 ```text 代码快照 → 只能回滚 解释能力 → 可以演进 ``` 只保存代码分支不够。 真正的基线是团队能说清:谁创建了容器、谁开启了事务、谁映射了请求。 > 装配有条件, > 容器在 Jar 间。 > 起步依赖是契约, > 基线在解释间。 # 10、系统落地 ```text 入口:浏览器/curl → 服务:Petclinic → 本模块:启动与请求链路 → 存储:HSQLDB/MySQL → 未实现:安全、缓存、迁移 ``` 原来有什么:官方 Petclinic 代码,能跑但未被团队解释。 本篇增加:三种启动方式验证、自动配置报告、基础设施清单接口、统一解释框架。 现在能做什么:团队可以回答“Spring Boot 替 Petclinic 完成了哪些基础设施”,并以此为基础继续演进。 下一篇缺什么:还缺对依赖解析和自动配置边界的深入控制,需要进一步理解 Spring Boot 的依赖管理机制。 官方链接: - https://spring.io/projects/spring-boot- https://github.com/spring-projects/spring-petclinic- https ://docs.spring.io/spring-boot/documentation.html > 入口已打通, > 服务有行踪。 > 存储虽可达, > 安全缓存仍空。 # 11、小结 ```text 三问三答:做了什么、怎么选、选错后果 ``` 第一问:Spring Boot 到底替 Petclinic 完成了哪些基础设施? 回答:内嵌 Tomcat、DispatcherServlet、DataSource、EntityManagerFactory、TransactionManager、静态资源映射、错误页、日志等。 第二问:是继续当黑盒,还是拆成可解释基线? 回答:必须拆成可解释基线,否则后续 22 招没有落点。 第三问:选错会怎样? 回答:选错后,团队只能背诵配置,无法定位故障边界,也无法安全演进。 当前状态:已建立 `spring-boot-petclinic-advanced-lab` 仓库,三种启动方式验证通过,真实请求链路可解释。 > 三问皆已答, > 基线初成家。 > 后续每一招, > 同仓再出发。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:为什么 Petclinic 没有创建 Tomcat 的代码,却能接收 HTTP 请求? 因为 `spring-boot-starter-web` 引入了内嵌 Tomcat,`ServletWebServerFactoryAutoConfiguration` 在启动时创建并启动 Tomca t,绑定到 `server.port` 配置的端口。 ## 实战题:在 Petclinic 中新增一个只读接口,返回当前生效的自动配置类数量。 可以注入 `ApplicationContext`,通过 `getBeanDefinitionNames()` 统计名称中包含 `AutoConfiguration` 的 Bean,或读取启动日志中的 自动配置报告。 ## 排障题:启动时出现 `Cannot determine embedded database driver class for database type NONE`,如何定位? 说明 classpath 中没有嵌入式数据库驱动,但数据源自动配置被触发。 检查是否引入了 `spring-boot-starter-data-jpa` 却没有引入 H2 或 HSQLDB,或显式配置了数据源但驱动缺失。 ## 架构判断题:把 Petclinic 改成 war 包部署到外部 Tomcat,是否更符合生产标准? 不一定。内嵌容器和外部容器是两种部署模型,没有绝对优劣。内嵌容器更适合一致性和快速迭代,外部容器更适合传统运维体系。关键不是形式,而是团队能否解释和管理所选模型。 > 理解不靠背, > 实战见真章。 > 排障有路径, > 架构看方向。 # 13、升华 ```text 工程结论:基线不是能跑,而是能解释 ``` Petclinic 的真正价值不是它实现了多少功能,而是它提供了一个足够真实、又足够小的 Spring Boot 应用。 通过三种启动方式和一条真实请求链路,我们证明了:Spring Boot 的核心贡献是工程化装配,而不是业务代码。 后续每一招都将在同一个仓库上继续演进,不再另起项目。 下一篇将深入依赖解析与自动配置边界,把“为什么引入一个 starter 会改变应用行为”讲清楚。 > 能跑非终点, > 能解是起点。 > 同仓二十二招, > 步步见真篇。 **系列项目:** 面向开发、测试、运维一体化的 Spring Boot 生产级演进实验系统 **已完成:** 官方 Petclinic 母本导入、三种启动方式验证、真实请求链路追踪、自动配置报告与基础设施清单 **本篇:** 建立可解释的启动与请求基线,回答 Spring Boot 代劳的基础设施 **下一篇:** 依赖解析与自动配置边界控制,理解 starter 引入的行为变化

优秀的个人博客,低调大师

Zs Admin 基于 springboot+vue 的快速开发脚手架

ZS-Admin 🚀 企业级前后端分离快速开发平台 · 开箱即用 · 安全 · 可扩展 官网 • 文档 • 在线演示 • 后端源码 • 前端源码 ## 🌟 项目简介 ZS-Admin 是一套面向中大型企业场景的现代化管理后台快速开发平台。基于主流技术栈构建,深度融合 RBAC 权限体系、多租户(SaaS)架构 与 低代码开发能力,帮助团队以最小成本快速交付高可用、高安全性的业务系统。 > 💡 适用于:OA 系统、ERP 后台、B2B 平台、SaaS 应用、内部管理系统等。 ### ✨ 核心价值 - 极速启动:初始化即具备完整权限与组织架构,5 分钟进入业务开发 - 降低重复劳动:内置用户、角色、菜单、字典、日志等通用模块 - 架构清晰:前后端完全解耦,模块化设计,便于二次开发与维护 - 生产就绪:支持 Docker 部署、JWT 认证、SQL 监控、操作审计等企业级特性 ## 🔗 官方资源 * 资源类型 * 地址 * * --------------- * -------------------------------------------------- * * 🌐 官方网站 * https://www.zsadmin.top * * 📚 官方文档 * https://www.zsadmin.top * * 🖥️ 在线演示 * http://demo.zsadmin.top * * 👤 演示账号 * admin / 123456(默认租户) zs/ 123456(测试租户) * ## 演示图 ## ⚙️ 技术栈 * 层级 * 技术选型 * * -------- * ----------------------------------------------------------------------- * * 后端 * JDK 17 · Spring Boot 3.x · Spring Security · MyBatis-Plus · JWT · Druid * * 前端 * Vue 3 · Vite · Pinia · Arco Design Vue · TypeScript * * 部署 * 支持 Docker 容器化 * ## 🗂️ 源码仓库 * 平台 * 前端 * 后端 * * --------- * ----------------------------------------------------------------- * --------------------------------------------------------- * * Gitee * zs-ui-acro-design * zs-admin-java * * GitHub * zs-ui-acro-design * zs-admin-java * * Gitcode * zs-ui-acro-design * zs-admin-java * > ⭐ 欢迎 Star、Fork 与 PR!您的支持是我们持续迭代的动力! ## 🧩 功能全景 ### 🏠 首页 - 工作台:聚合待办、快捷入口、数据卡片,打造个性化操作中心。 ### ⚙️ 系统管理 - 用户/部门/岗位:完整的组织人事模型,支持树形结构与多对多关联。 - 角色 & 菜单:基于 RBAC 的细粒度权限控制(菜单 + 按钮级)。 - 字典管理:统一维护枚举值,前端自动映射下拉选项。 - 通知公告:富文本编辑 + 定时发布 + 已读追踪。 ### 🏢 租户管理(SaaS) - 租户管理:独立租户生命周期管理(启用/停用/过期)。 - 租户套餐:定义功能包、数据配额、API 调用限制等。 ### 📬 消息中心 - 站内信:实时消息推送,支持批量操作与状态标记。 - 短信模板:标准化模板管理,无缝对接阿里云、腾讯云等短信服务。 ### 📊 报表管理 - 积木报表:集成 JimuReport,拖拽式设计复杂报表,支持 Excel/PDF 导出。 ### 🔧 系统设置 - 全局配置:动态修改系统 Logo、名称、备案信息、第三方服务密钥等。 - 定时任务:Cron 表达式配置、启停控制、执行日志查看。 ### 📈 系统监控 - SQL 监控:Druid 集成,实时分析慢查询与连接池状态。 - 操作日志:记录关键行为(增删改),支持按用户、IP、时间回溯。 - 在线用户:实时列表(IP、终端、登录时间)。 ### 🛠️ 开发工具 - API 文档:Swagger + Knife4j,自动生成 RESTful 接口文档,支持在线调试。 - 代码生成器:输入表名 → 一键生成前后端 CRUD 代码(含 Vue 页面、Service、Controller 等)。 ## 🌈 项目亮点 * 特性 * 说明 * * ----------------- * ------------------------------------------------- * * ✅ 开箱即用 * 初始化数据库即可运行完整后台,无需从零搭建 * * ✅ 企业级安全 * JWT 无状态认证 + Spring Security + 权限校验拦截器 * * ✅ 多租户支持 * 数据隔离(Schema/字段级)、租户上下文自动注入 * * ✅ 开发者友好 * 内置代码生成、接口调试、日志追踪三大提效利器 * * ✅ 现代化 UI * 基于 Arco Design Vue,简洁美观,响应式布局 * * ✅ 易于扩展 * 模块化结构,新增业务模块只需关注核心逻辑 * ## 🤝 参与共建 - 🐞 反馈问题:提交 Issue - 💡 贡献代码:欢迎 Fork 并提交 PR,共同完善 ZS-Admin! - 💬 加入社区:关注官网底部获取 官方 QQ 群 / 微信公众号 入口

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册