news 2026/9/17 3:54:59

Cucumber自动化测试实战:从BDD到Gherkin的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cucumber自动化测试实战:从BDD到Gherkin的完整指南

我第一次用 Cucumber,是在一个购物网站自动化改造项目里。当时团队已经维护了一套 Selenium 脚本,用例数量不少,但产品经理和项目负责人每次验收都要另开一场会,逐条解释“这个脚本到底验证了什么”。直到我们把用例全部改成 Gherkin 格式写进 Feature 文件,产品经理第一次自己读懂了回归报告。那一刻我才真正确认:Cucumber 的价值不是让程序员少写代码,而是把“需求规格”变成“可执行文档”。

这篇文章就是一份我自己的 Cucumber 参考笔记。我会从 BDD 思路讲起,把 Gherkin 语法、项目搭建、步骤定义写法、参数绑定、Hooks、数据表、并行执行、常见报错这些东西串成一条完整链路。内容适合刚接触 Cucumber 的测试开发,也适合用了几个月但一直靠复制粘贴维护 Step Definition 的同学。看完你应该能独立搭一个 Cucumber 项目,并且知道遇到问题去哪里排查。

1. 先弄懂:Cucumber 到底解决什么问题

1.1 从一份所有角色都能看懂的需求单说起

传统的自动化测试脚本,写出来是这样的:打开浏览器、定位元素、输入用户名、输入密码、点击登录、断言 URL 发生了变化。代码本身没问题,但它天然是技术语言。研发能看,测试能维护,唯独业务方看不懂。更麻烦的是,测试脚本里的每一步,到底对应需求文档里的哪一条验收标准,往往没人说得清楚。

Cucumber 走的是另一条路。它要求你先用 Gherkin 语法写一个 Feature 文件,比如:

功能:用户登录 场景:使用正确的用户名和密码登录 假如我在登录页面 当我输入正确的用户名和密码 并且我点击登录按钮 那么我应该看到用户中心页面

这段文字,产品经理能看懂,研发能看懂,测试也能看懂。然后 Cucumber 会把这些自然语言和 Step Definition(步骤定义)代码做绑定,把“当我输入正确的用户名和密码”映射到一段真正的测试代码去执行。所以 Cucumber 不是测试框架的替代品,它更像是“需求描述”和“自动化执行”之间的一座桥。

1.2 BDD 的核心:把沟通成本变成规范成本

BDD,行为驱动开发,它的核心不是工具,而是协作方式。传统流程里,需求经过产品经理转述、研发理解、测试拆解,每一步都有信息损耗。BDD 要求大家坐下来,用一种共享语法把“行为”写下来,形成一份所有角色都认可的规范。

Cucumber 就是 BDD 最常见的落地工具。它在技术实现上其实很简单:Gherkin 解析器把 Feature 文件转成抽象语法树,场景里的每一步句子都会和 Step Definition 方法上声明的正则表达式或 Cucumber Expression 做匹配,匹配成功就调用对应方法。底层再交给 JUnit 或 TestNG 去驱动执行。理解这个机制很重要,后面遇到“步骤匹配不上”“参数解析失败”这类报错,就不会手忙脚乱。

1.3 什么项目适合用 Cucumber

适合用的场景有这几个:业务规则复杂,比如支付、优惠券、审批流;团队里有需要对齐口径的产品和测试;回归频率高,希望需求变更能在 Feature 文件里留下痕迹;以及跨端场景多,一套 Gherkin 描述可以在 Web、App、接口层复用。

不太适合的场景也有。如果你的用例全是简单接口探测,没有业务故事驱动;或者团队没人愿意维护“第二份语言”,Feature 文件写完后很快失真;又或者流程很短,产品经理和研发天天坐在一起,沟通成本本来就低。这种情况下硬上 Cucumber,往往只会多一层维护负担。

2. 把 Gherkin 语法吃透,Feature 文件才有参考价值

2.1 基础关键字:Feature、Scenario、Given、When、Then

Gherkin 的关键字不多,但每个都有固定含义。Feature 是文件级描述,写的是被测功能的整体名称;Scenario 是具体场景,一条场景就是一条测试用例;Given 描述前置条件,When 描述触发动作,Then 描述预期结果。

我见过很多团队把 Given 当测试步骤用,写成“当我打开浏览器”“当我点击按钮”,严格说这不对。Given 是“系统处于某个状态”,When 才是“用户做了某个动作”。区分它们不是因为语法洁癖,而是因为 Cucumber 报告是按照 Given/When/Then 分块的,混在一起看报告时很难快速定位是环境问题还是断言问题。

还有 And 和 But,它们用来连接同类步骤。

功能:订单结算 场景:使用优惠券后金额正确 假如我已经登录 并且我的购物车里有 2 件商品 并且我有一张满 100 减 20 的优惠券 当我进入结算页面 并且我选择使用该优惠券 那么结算金额应该是 180 元

And 减少重复的前缀,让场景读起来更像自然语言。But 则表达“转折”,比如:

但是优惠券不能叠加使用

2.2 Background、Scenario Outline 和 Examples

一个 Feature 文件里通常有多个场景,它们可能共享同一组前置条件。与其在每个场景里重复写,不如用 Background 提取。

功能:用户中心 背景: 假如我已经登录系统 并且我访问用户中心首页 场景:查看个人信息 那么我应该看到我的昵称和头像 场景:修改手机号 当我点击修改手机号 并且输入新的手机号码 那么系统应该发送验证码

Background 适合放那些“所有场景都不变”的公共前置。一旦它开始出现“有些场景需要、有些不需要”的情况,记得不要硬塞进去,改用 Tag 配合 Hooks 来处理。

Scenario Outline 是参数化利器。同样的场景流程,数据不同,结果不同。

场景大纲:不同会员等级享受不同折扣 假如我的会员等级是 <等级> 当我要购买一件 <价格> 元的商品 那么应付金额应该是 <折扣价> 元 示例: | 等级 | 价格 | 折扣价 | | 普通 | 100 | 100 | | 金牌 | 100 | 90 | | 钻石 | 100 | 80 |

用 <变量名> 做占位符,由 Examples 表格里的列名填充。一条大纲可以派生出几十条用例,维护成本却只有一份。

2.3 写 Feature 时一定要守住的三个规矩

第一,场景名称别写测试动作。比如“验证登录功能”,这是个主题描述,不是行为描述。更好的是“使用正确密码登录成功”“使用错误密码登录失败”。到了报告里,场景名称就是测试用例名,写得含糊,失败后根本看不出哪个业务片段出了问题。

第二,避免魔法值满天飞。比如“当我输入 123456”,123456 到底是什么,是手机号还是验证码?用 <占位符> 或者语义化文案替代,比如“当我输入验证码 123456”。这样报告和日志的可读性会高很多。

第三,保持场景独立性。一个场景不要依赖另一个场景运行后的状态,更不要依赖执行顺序。Cucumber 虽然支持执行顺序,但业务上不应当依赖前一个场景留在系统里的脏数据。这跟单元测试的独立性是一个道理。

3. 从零搭一个 Cucumber-JVM 工程

3.1 Maven 依赖和插件配置

现在 Java 技术栈里最主流的是 Cucumber-JVM,配合 JUnit 运行。我用 Maven 举例子,Gradle 的依赖坐标也一样。

<properties> <cucumber.version>7.15.0</cucumber.version> <junit.version>5.10.0</junit.version> </properties> <dependencies> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-java</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit-platform-engine</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies>

Cucumber 7 默认基于 JUnit Platform,现在基本推荐这套配置。早期 Cucumber 6 用cucumber-junit@RunWith(Cucumber.class),也能跑,但和 JUnit 5 的生态兼容不如 JUnit Platform 自然。如果你的工程已经很老,保持老配置没问题;新项目建议直接用 JUnit Platform。

3.2 标准目录结构

一般团队约定把 Feature 文件放在src/test/resources/features,Step Definition 放在src/test/java下按包分层。我的习惯是这样:

src/test/ ├── java/ │ └── com/example/test/ │ ├── steps/ │ │ ├── LoginSteps.java │ │ └── OrderSteps.java │ └── RunCucumberTest.java └── resources/ └── features/ ├── login/ │ └── login.feature └── order/ └── order.feature

Feature 文件按业务模块分目录,Steps 按页面或领域对象分文件。不要一个文件装下所有场景,也不要一个 Step Definition 类装下所有步骤。

3.3 JUnit Runner 的写法

如果用 JUnit Platform,启动类可以很简单:

package com.example.test; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.test") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber-report.html") public class RunCucumberTest { }

关键参数就两个:@SelectClasspathResource指定 Feature 所在的 classpath 路径;GLUE_PROPERTY_NAME指定 Step Definition 的包名。这两个对不上,就会报告上装不到步骤定义。

3.4 第一个能跑的 Feature 和 Step Definition

写一个最基础的例子。

功能:计算器 场景:两数相加 假如我有数字 3 并且我有数字 5 当我将它们相加 那么结果应该是 8

对应步骤定义:

package com.example.test; import io.cucumber.java.en.Given; import io.cucumber.java.en.When; import io.cucumber.java.en.Then; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorSteps { private int a; private int b; private int result; @Given("我有数字 {int}") public void iHaveNumber(int number) { if (a == 0) { a = number; } else { b = number; } } @When("我将它们相加") public void iAddThem() { result = a + b; } @Then("结果应该是 {int}") public void resultShouldBe(int expected) { assertEquals(expected, result); } }

跑 RunCucumberTest,Cucumber 会自动扫描 features 目录,解析 Gherkin,匹配 Step Definition 并执行。{int}是 Cucumber Expression 的整数参数类型,它不只是正则,还包含类型转换。

IDE 方面,IntelliJ IDEA 安装 Cucumber for Java 插件,可以直接右键 Feature 文件运行,还支持从 Feature 步骤跳转到 Step Definition。这对调试帮助很大。

4. 步骤定义里的几个进阶玩法

4.1 参数绑定:正则表达式和 Cucumber Expression

Cucumber 匹配步骤有两种方式:一种是老式正则,比如@"我有数字 (\\d+)";另一种是 Cucumber Expression,比如"我有数字 {int}"。Cucumber 7 默认推荐 Expression,它可读性更好,类型转换也更直接。

常用的内置参数类型包括:

参数类型匹配范围转换结果
{int}整数Integer
{float}浮点数Double
{word}不带空格的单字String
{string}被双引号包起来的内容String
{bigdecimal}高精度数字BigDecimal

{word}时要注意,它不能匹配带空格的内容。如果步骤里要传“张三 李四”这种带空格的字符串,就用{string}并配合引号:当我输入用户名 "张三 李四" 时

4.2 DataTable 和 DocString:处理多行数据

有些场景的前置条件或断言数据不止一行,比如批量创建用户、比对订单清单。这时候用 DataTable 很合适。

场景:批量创建用户 当管理员批量创建以下用户 | 用户名 | 手机号 | 角色 | | zhangsan | 13800000001 | 普通用户 | | lisi | 13800000002 | 管理员 | 那么系统应该创建成功 2 个用户

步骤定义里可以这样收:

import io.cucumber.datatable.DataTable; @When("管理员批量创建以下用户") public void adminCreatesUsers(DataTable dataTable) { List<Map<String, String>> rows = dataTable.asMaps(String.class, String.class); for (Map<String, String> row : rows) { String username = row.get("用户名"); String phone = row.get("手机号"); String role = row.get("角色"); // 调用接口或操作页面 } }

asMaps返回按表头组织的 Map 列表,代码阅读直观。也可以直接转成List<List<String>>,适合不关心表头的场景。

DocString 则适合传一段较长文本,比如 JSON 请求体。用三引号写出来:

场景:创建订单 当用户提交以下订单数据 """ {"userId": 1, "items": [{"sku": "A100", "count": 2}]} """ 那么系统应该返回订单号

步骤定义里,DocString 形参会自动作为 String 传入。它的好处是避免在 Feature 文件里写一长串难看的转义字符串。

4.3 Tags 和 Hooks:控制执行范围与生命周期

Tags 的作用是在一个维度的用例集合上打标记。比如冒烟测试、回归测试、依赖环境的用例。

@smoke @login 功能:用户登录 场景:正确密码登录成功 ...

配置运行时只跑@smoke标签:

cucumber.filter.tags=@smoke

多个标签组合条件也可以,比如@smoke and not (@slow)这种表达式是支持的。

Hooks 是另一个高频能力。@Before在每个场景之前执行,@After在每个场景之后执行。可以配合 Tag 做精细控制,比如:

import io.cucumber.java.Before; import io.cucumber.java.After; public class Hooks { @Before("@smoke") public void setupSmokeData() { // 准备冒烟测试的干净数据 } @After public void cleanup(Scenario scenario) { if (scenario.isFailed()) { // 截图,写日志,清理脏数据 } } }

注意 Hooks 不属于某个步骤,它是全局或按 Tag 生效的。不要在@Before里放跟具体场景业务强相关的准备逻辑,那样会让场景依赖隐形的外部配置,降低可读性。

5. 一个完整实战场景:登录加下单接口链路

5.1 场景描述:跨模块的业务语序

纸上谈兵不好讲,我们用一个登录加下单的接口链路例子走一遍。假设系统有用户服务和订单服务,测试要覆盖:登录拿 Token、用 Token 下单、校验订单创建结果。

@api 功能:用户下单链路 场景:登录后成功创建订单 当用户通过手机号 13800000001 和密码 123456 进行登录 那么登录接口应该返回成功 并且返回的令牌应该不为空 当用户携带该令牌创建一个 A100 商品订单 那么订单接口应该返回创建成功 并且订单状态应该是待支付

注意我在这里用了两个“当”,这在语义上是允许的,代表两个连续的用户动作。Cucumber 语法并不限制 When 只能出现一次。

5.2 步骤定义实现:状态传递是重点

现在要写步骤定义。链路场景里有状态传递,Token 从登录步骤拿到的,要在下单步骤里使用。我不能用静态变量满天飞,但简单的场景下,可以用一个共享 Context 来存。

package com.example.test.steps; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; public class OrderSteps { private final TestContext context; public OrderSteps(TestContext context) { this.context = context; } @When("用户通过手机号 {string} 和密码 {string} 进行登录") public void login(String phone, String password) { // 实际调用登录接口 context.setToken("mock-token-value"); } @Then("登录接口应该返回成功") public void loginShouldSucceed() { // 断言登录返回状态 } @Then("返回的令牌应该不为空") public void tokenShouldNotBeEmpty() { if (context.getToken() == null || context.getToken().isEmpty()) { throw new AssertionError("token 为空"); } } @When("用户携带该令牌创建一个 A100 商品订单") public void createOrderWithToken() { String token = context.getToken(); // 调用下单接口,携带 Token } @Then("订单接口应该返回创建成功") public void orderShouldSucceed() { } @Then("订单状态应该是待支付") public void orderStatusShouldBePending() { } }

这里必须用TestContext这个结构,它其实是一个共享状态的容器。我习惯把每个线程需要的临时数据都放在里面,这样在多线程并行执行时不会数据串味。共享静态变量是初学者经常踩的坑,后面会专门讲。

5.3 运行报告和失败定位

执行完成后,Cucumber 默认生成的 HTML 报告会列出每个场景的执行时间、状态、失败步骤、错误堆栈。查看时我一般先看场景名称,再看失败步骤,最后看错误堆栈。如果错误是Undefined Step,说明 Gherkin 里的这句话没人认领,先去 Step Definition 里搜文案。如果是AssertionError,说明业务断言没通过,再去查接口返回或页面状态。

报告里加上时间统计也有用,可以在@After里记录Scenario对象的总执行耗时,帮助发现哪些场景越来越慢。

6. 长期维护时最重要的四件事

6.1 Step Definition 不要堆成上帝类

我见过一个项目里,所有步骤全堆在CommonSteps.java里,几千行。看起来省事,实际上谁也不愿意改,因为改一个方法可能影响几十个场景。我的做法是按业务域拆类:LoginSteps、OrderSteps、PaymentSteps、UserSteps。就算暂时有些重复,也值得。

另外,Step Definition 方法名要跟步骤文案互相呼应,不要写method1()。当 IDE 搜索时,一个清晰的命名瞬间就能定位。

6.2 “正则脑”和“场景文档”都要防腐化

Feature 文件一旦写成,就要当成需求文档一样持续维护。业务变了,Feature 文本先改,Step Definition 跟着改。最怕的是研发直接改 Step Definition 去适配新代码,Feature 文件却原封不动。这样报告虽然绿了,Cucumber 作为沟通桥梁的价值就没了。

我建议每次 MR 评审时,如果涉及业务逻辑变更,检查清单里一定要有一条:Feature 文件是否描述了新行为。这个习惯在团队里能省很多后期解释成本。

6.3 并行执行与线程安全

用例多了以后,串行执行会很痛苦。Cucumber 7 支持并行执行,JUnit Platform 配置里可以设置:

cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4

这样会在多个线程里同时跑场景。但代价是,你的代码必须线程安全。我前面提到 TestContext 按线程隔离,就是为了应对并行。如果你在 Step Definition 里用 static 变量存数据,并行时一定互相污染。

另外,数据库里的共享数据也要注意。多个场景同时跑,可能同时占用同一个测试用户、同一张优惠券,导致断言失败。我在项目里会把测试数据随机化,比如用户手机号带时间戳后缀。

6.4 失败重试不要乱加

测试不稳定是常态,但一失败就重试,很容易掩盖真实问题。Cucumber 本身没有内置失败重试,需要额外实现或引入第三方插件。我的建议是:只在偶发环境问题(网络抖动、外部服务超时)的用例上做重试,并且重试次数限制在 1-2 次,同时把重试结果单独标记。不要对整个回归套件无脑重试。

7. 高频问题排查速查表

问题出现原因解决办法
Undefined StepFeatures 里某句话没有对应 Step Definition在 Step Definition 中补上@Given/@When/@Then方法,注意副本文案与功能文案格式一致
Ambiguous Step多个 Step Definition 匹配同一句话检查两个方法的表达式,缩小匹配范围,或改用{word}而非(.*)
参数解析失败{int}遇上非数字内容把参数类型改成{string},或修正文本里的参数值
中文乱码文件编码不是 UTF-8把 Maven 的project.build.sourceEncoding设置为 UTF-8,IDEA 的文件编码也要改
场景执行顺序影响结果场景间共享了数据或者使用了静态状态清理测试数据,携带 context,避免依赖执行顺序
在 IDEA 里跑不了 Feature没有安装 Cucumber for Java 插件或版本不匹配安装插件,并确认项目依赖是同一大版本
并行执行出现随机失败测试数据或状态跨线程共享使用线程隔离的 TestContext,随机化测试数据

7.1 遇到 Undefined Step 时怎么看才快

打开报告或者控制台,它会直接列出找不到的那句话。比如:

Undefined Step: 当我输入正确的用户名和密码

最快的做法是把这句话原样复制到代码库里搜索,看看有没有方法已经写了但正则没匹配上,或者直接新建一个方法。注意中英文标点和空格,When 我输入When我输入是完全不同的两句话。

7.2 正则匹配不上的经典坑

比较常见的是“过度使用(.*)”。(.*)是贪婪匹配,可能一下子吞掉后面所有字符,导致后面的参数解析错位。如果你只是匹配一个单词,用{word};匹配一个带引号的字符串,用{string};只要整数,用{int}。范围越小越不容易出歧义。

还有一个坑是转义字符。正则里括号、点号都是特殊字符,要匹配字面字符必须转义。Cucumber Expression 里很多场景不需要写正则,宁愿放弃一点灵活性也别找麻烦。

7.3 报告出现乱码时急救

Cucumber HTML 报告出现乱码,我遇到的大多数原因是源文件编码问题。IDEA 默认 UTF-8,但 Windows 环境下个别文件可能被存成了 GBK。处理时统一转换编码,并在 pom.xml 里强制源文件和资源文件的编码:

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

乱码不在代码里,在文件存储层面,转换之前最好先确认所有文件的 Bytes,不要只改 IDEA 显示编码。

7.4 并行下的共享状态排查思路

如果你开了并行,随机偶发失败,并且一一执行全部通过,基本就是共享状态问题。排查顺序是:先查 Step Definition 里有多少 static 变量;再检查 TestContext 是不是 ThreadLocal;然后看测试数据表里的手机号、优惠券、订单号有没有重复;最后看有没有对同一个文件或数据库记录做并发写。前两个是代码问题,后两个是数据问题,都要逐一排除。

8. 一点真实的个人体会

维护了几年 Cucumber 项目,我最大的体会是:Cucumber 的技术门槛真的不高,高的是持续维护的纪律性。Feature 文件不是写一次就完了,每次业务调整、每个边界条件补充,都应该先改 Feature 再改代码。如果你能做到所有角色都愿意打开 Feature 文件对话,Cucumber 的威力才真正发挥出来。如果你的团队只是把它当成一个脚本翻译器,那它会比普通自动化脚本更让人头大。

最后分享一个小技巧:我习惯在每个 Feature 文件开头加一段简单的“业务背景”注释,说明这条链路属于哪个业务模块、关联了哪些系统、数据依赖是什么。几个月后你自己回来维护,就靠这段注释快速捡起上下文。这个习惯帮我避免了很多次“这场景到底在验什么”的尴尬。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 3:54:54

SQL中count(1)、count(*)与count(列名)的区别及性能优化

年初我帮团队复盘一个慢SQL问题&#xff0c;优化完发现执行计划里count(1)被优化器和count(*)处理成了完全一样的东西。但到了count(列名)&#xff0c;情况突然不一样了。群里当时吵了一轮&#xff1a;有人说 count(1) 比 count(*) 快&#xff0c;有人说 count(列名) 最快&…

作者头像 李华
网站建设 2026/9/17 3:52:10

AI测试工具兴起,2027年非AI驱动工具淘汰,测试工程师如何转型

最近测试圈子里传得最凶的一件事&#xff0c;就是微软内部文件提到2027年要淘汰所有非AI驱动的测试工具。很多朋友跑来问我&#xff0c;说这是不是意味着我们这帮写脚本、点页面的测试工程师要集体失业了。我的看法比较直接&#xff1a;这份文件更像是一个行业风向标&#xff0…

作者头像 李华
网站建设 2026/9/17 3:51:58

把 CC-Switch 的上游 Key 换成 TaoToken 后,WSL2 里也能跑通 Claude Code

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 3:51:08

Linux卡在emergency mode?fstab的UUID错配是主因

周六早上我远程连家里那台Linux NAS&#xff0c;结果怎么都ping不通。过了一会儿家人拍来一张照片&#xff0c;屏幕停在黑底白字的启动界面&#xff0c;上面明晃晃一行字&#xff1a;“Welcome to emergency mode!”看到这行字我反而松了口气&#xff0c;因为这类故障我处理过太…

作者头像 李华
网站建设 2026/9/17 3:49:03

Spring三级缓存机制:循环依赖与AOP代理全解析

最近有个同事跑来问我&#xff0c;说在智谱清言里搜“三级缓存具体是什么”&#xff0c;AI给讲了一堆Spring源码。他看完还是懵的&#xff0c;就跑来让我用人话再讲一遍。这个题目确实经典&#xff0c;面试问烂了&#xff0c;网上文章也一大把&#xff0c;但能把“为什么非得是…

作者头像 李华