一、AI 辅助开发,拼的不是工具而是"配合方式"
AI 辅助开发提效的前提,不是"会不会装 Copilot / Cursor",而是"会不会跟它配合"。把 AI 当搜索引擎用,得到一堆可能错的答案;把它当结对编程搭子用,能省掉一半以上的重复劳动------样板代码、单元测试、文档、代码审查。核心底线只有两条:AI 写的代码你必须逐行看懂,复杂业务逻辑 AI 只给草稿、不给终稿。
本文以 Java 后端工作流为主线,落地 Copilot + Cursor 的四类实战:用精准 Prompt 基于现有代码生成完整 CRUD 链路、让 AI 补齐 JUnit 5 + Mockito 单元测试的边界场景、用 SpringDoc + AI 同步 API 文档、以及最关键的"AI 初筛 + 人工终审"代码审查白名单/黑名单。读完能建立"AI 生成 → 人工 Review → 单测 → 合入"的强制流程,把日常编码效率提升 30%~50%,同时守住质量底线。
二、AI 辅助开发的现实图景:不是取代,是倍增
先说结论:AI 不会取代程序员,但会用 AI 的程序员会取代不会用的。
这不是什么营销口号,而是我这半年用 Copilot + Cursor 写 Java 代码的真实体感。具体来说,AI 在以下四类任务上提效最明显:
| 任务类型 | 传统耗时 | AI 辅助后 | 质量要求 |
|---|---|---|---|
| 样板代码(DTO/Mapper/Builder) | 30min | 5min | 需人工校验 |
| 单元测试 | 1h | 15min | 需补边界场景 |
| API 文档/注释 | 20min | 3min | 需检查准确性 |
| 代码审查(静态检查) | 30min | 10min | 不能替代人工 |
但有两个底线必须守住:
-
AI 写的代码,你必须要能看懂。 看不懂就敢合进主干,那是拿生产环境开玩笑。
-
核心算法和复杂业务逻辑,AI 只能给草稿,不能给终稿。 这部分是你的护城河,别外包给 AI。
三、代码生成:让 Cursor 成为你的结对编程伙伴
Cursor 这两年火不是没道理的。它跟传统 IDE 补全的最大区别是:上下文理解深度。你可以把整个项目的代码丢给它,让它基于现有代码风格生成新模块。
场景:基于现有 Entity 生成完整的 CRUD 链路
假设你有一个 Order 实体,想让 AI 生成对应的 DTO、Service、Controller------但不是瞎生成,要跟你现有项目的命名规范、异常处理风格、日志格式保持一致。
第一步:准备上下文(这是最关键的一步)
在 Cursor 的 Chat 窗口里,先给它喂几个参考文件:BaseController.java、GlobalExceptionHandler.java、Order.java。然后写一个精准的 Prompt:
请基于以下项目规范,为 Order 实体生成完整的 REST API 模块:
1. 项目规范(必须遵守):
- 使用 Lombok 的 @Builder + @Data
- Service 层必须统一返回 Result<T> 包装对象
- Controller 使用 @RestController + @RequestMapping("/api/v1/orders")
- 异常统一抛 BusinessException,错误码在 ErrorCode 枚举中定义
- 日志使用 SLF4J,格式:log.info("[操作] 参数={}, 结果={}", param, result)
- 分页查询使用 PageQuery<T> 统一入参,返回 PageResult<T>
2. 需要生成:
- OrderCreateDTO(字段校验用 @NotNull/@Size)
- OrderUpdateDTO(允许部分字段为空)
- OrderQueryDTO(分页 + 时间范围查询)
- OrderService(包含 create/update/queryById/pageQuery 四个方法)
- OrderController(五个端点:POST /api/v1/orders, PUT /api/v1/orders/{id}, GET /api/v1/orders/{id}, GET /api/v1/orders?page&size&startTime&endTime, DELETE /api/v1/orders/{id})
3. 注意:
- 不要生成 set/get 方法,用 Lombok
- 删除操作只做逻辑删除(update deleted=1)
- 分页查询按 createTime 倒序
Prompt 的精髓:约束越多,输出越稳。 你模糊地说"帮我写个 Order 的 CRUD",Cursor 可能给你生成一套它自己臆想出来的风格;但你把具体规范一条条列出来,它生成的代码至少能省掉你 70% 的修改时间。
第二步:AI 生成的代码骨架(Spring Boot 3.2 + JDK 21)
java
// ===== OrderCreateDTO.java =====
package com.example.order.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Builder
public class OrderCreateDTO {
@NotNull(message = "用户ID不能为空")
private Long userId;
@Size(max = 200, message = "商品名称最多200字符")
private String productName;
@NotNull(message = "金额不能为空")
private BigDecimal amount;
// 可选:备注
@Size(max = 500, message = "备注最多500字符")
private String remark;
}
java
// ===== OrderService.java =====
package com.example.order.service;
import com.example.common.exception.BusinessException;
import com.example.common.result.Result;
import com.example.common.result.PageResult;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderUpdateDTO;
import com.example.order.entity.Order;
import com.example.order.enums.ErrorCode;
import com.example.order.mapper.OrderMapper;
import com.example.order.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final OrderMapper orderMapper;
@Transactional
public Result<Long> create(OrderCreateDTO dto) {
log.info("[订单创建] 参数={}", dto);
Order order = orderMapper.toEntity(dto);
order.setCreateTime(LocalDateTime.now());
order.setDeleted(false);
Order saved = orderRepository.save(order);
log.info("[订单创建] 结果={}", saved.getId());
return Result.ok(saved.getId());
}
@Transactional
public Result<Void> update(Long id, OrderUpdateDTO dto) {
log.info("[订单更新] id={}, 参数={}", id, dto);
Order order = orderRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND));
orderMapper.updateEntity(dto, order);
order.setUpdateTime(LocalDateTime.now());
orderRepository.save(order);
log.info("[订单更新] 结果=success");
return Result.ok();
}
public Result<Order> queryById(Long id) {
log.info("[订单查询] id={}", id);
Optional<Order> opt = orderRepository.findByIdAndDeletedFalse(id);
if (opt.isEmpty()) {
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
}
return Result.ok(opt.get());
}
public Result<PageResult<Order>> pageQuery(OrderQueryDTO query) {
log.info("[订单分页查询] 参数={}", query);
PageRequest pageRequest = PageRequest.of(
query.getPage() - 1, // 前端从1开始,JPA从0开始
query.getSize(),
Sort.by(Sort.Direction.DESC, "createTime")
);
Page<Order> page = orderRepository.findByCreateTimeBetween(
Optional.ofNullable(query.getStartTime()).orElse(LocalDateTime.MIN),
Optional.ofNullable(query.getEndTime()).orElse(LocalDateTime.MAX),
pageRequest
);
PageResult<Order> result = PageResult.of(page.getContent(), page.getTotalElements(), query.getPage(), query.getSize());
log.info("[订单分页查询] 结果=total={}", result.getTotal());
return Result.ok(result);
}
@Transactional
public Result<Void> delete(Long id) {
log.info("[订单删除] id={}", id);
int rows = orderRepository.logicalDelete(id, LocalDateTime.now());
if (rows == 0) {
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
}
log.info("[订单删除] 结果=success");
return Result.ok();
}
}
java
// ===== OrderController.java =====
package com.example.order.controller;
import com.example.common.result.Result;
import com.example.common.result.PageResult;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderUpdateDTO;
import com.example.order.entity.Order;
import com.example.order.service.OrderService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public Result<Long> create(@Valid @RequestBody OrderCreateDTO dto) {
return orderService.create(dto);
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OrderUpdateDTO dto) {
return orderService.update(id, dto);
}
@GetMapping("/{id}")
public Result<Order> getById(@PathVariable Long id) {
return orderService.queryById(id);
}
@GetMapping
public Result<PageResult<Order>> pageQuery(@Valid OrderQueryDTO query) {
return orderService.pageQuery(query);
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
return orderService.delete(id);
}
}
XML
#Maven 依赖(节选):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
Cursor 生成的这段代码,基本骨架是对的------Lombok、校验注解、分页逻辑、日志格式都按 Prompt 约束输出了。但有几个点你必须人工检查:
-
PageRequest.of(page - 1, ...)这个转换是否正确?如果你的前端已经统一从 0 开始传参,这里就多了个 bug。 -
findByCreateTimeBetween这个方法名是 JPA 派生查询,AI 可能假设了你的 Repository 已经有这个方法------实际上你可能需要补@Query注解。 -
日志里的敏感信息 ------如果
dto里包含用户手机号或地址,直接toString()可能泄露隐私,需要脱敏。
AI 生成的是 80 分的草稿,剩下 20 分的细节坑,必须你自己填。
四、单元测试生成:让 AI 帮你把覆盖率 KPI 变成质量保障
写测试是程序员最讨厌的体力活之一,但又是代码质量的底线。AI 在这个场景的价值最大------它能帮你把 80% 的样板测试代码生成出来,你只需补充那 20% 的边界场景。
场景:为上面的 OrderService 生成 JUnit 5 + Mockito 测试
给 Cursor 的 Prompt:
java
为 OrderService 生成 JUnit 5 + Mockito 的单元测试,要求:
1. 使用 @ExtendWith(MockitoExtension.class) 和 @Mock / @InjectMocks
2. 每个测试方法必须包含三段注释:// Given // When // Then
3. 必须覆盖:正常创建、查询不到抛异常、分页查询带时间范围、逻辑删除失败抛异常
4. 使用 AssertJ 的 assertThat 断言
5. 不要用 SpringBootTest,这是纯单元测试,不启动 Spring 容器
6. 对 orderRepository.findById 的返回值使用 Optional.of() 和 Optional.empty() 两种场景
package com.example.order.service;
import com.example.common.exception.BusinessException;
import com.example.common.result.PageResult;
import com.example.common.result.Result;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderUpdateDTO;
import com.example.order.entity.Order;
import com.example.order.enums.ErrorCode;
import com.example.order.mapper.OrderMapper;
import com.example.order.repository.OrderRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
@DisplayName("OrderService 单元测试")
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private OrderMapper orderMapper;
@InjectMocks
private OrderService orderService;
@Test
@DisplayName("正常创建订单,返回订单ID")
void create_shouldReturnOrderId_whenValidInput() {
// Given
OrderCreateDTO dto = OrderCreateDTO.builder()
.userId(10086L)
.productName("iPhone 16 Pro")
.amount(new BigDecimal("9999.00"))
.build();
Order order = new Order();
order.setId(1L);
given(orderMapper.toEntity(dto)).willReturn(order);
given(orderRepository.save(any(Order.class))).willReturn(order);
// When
Result<Long> result = orderService.create(dto);
// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getData()).isEqualTo(1L);
verify(orderRepository).save(order);
}
@Test
@DisplayName("查询不存在的订单,抛出 BusinessException")
void queryById_shouldThrow_whenOrderNotFound() {
// Given
Long orderId = 99999L;
given(orderRepository.findByIdAndDeletedFalse(orderId))
.willReturn(Optional.empty());
// When / Then
assertThatThrownBy(() -> orderService.queryById(orderId))
.isInstanceOf(BusinessException.class)
.hasMessageContaining(ErrorCode.ORDER_NOT_FOUND.getMessage());
}
@Test
@DisplayName("分页查询带时间范围,返回正确分页结果")
void pageQuery_shouldReturnPageResult_withTimeRange() {
// Given
OrderQueryDTO query = new OrderQueryDTO();
query.setPage(1);
query.setSize(10);
query.setStartTime(LocalDateTime.of(2024, 1, 1, 0, 0));
query.setEndTime(LocalDateTime.of(2024, 12, 31, 23, 59));
Order order1 = new Order();
order1.setId(1L);
Order order2 = new Order();
order2.setId(2L);
List<Order> orders = List.of(order1, order2);
Page<Order> page = new PageImpl<>(orders, PageRequest.of(0, 10), 25L);
given(orderRepository.findByCreateTimeBetween(
query.getStartTime(), query.getEndTime(), any(PageRequest.class)))
.willReturn(page);
// When
Result<PageResult<Order>> result = orderService.pageQuery(query);
// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getData().getTotal()).isEqualTo(25L);
assertThat(result.getData().getList()).hasSize(2);
}
@Test
@DisplayName("逻辑删除失败时抛出异常")
void delete_shouldThrow_whenLogicalDeleteReturnsZero() {
// Given
Long orderId = 1L;
given(orderRepository.logicalDelete(orderId, any(LocalDateTime.class)))
.willReturn(0);
// When / Then
assertThatThrownBy(() -> orderService.delete(orderId))
.isInstanceOf(BusinessException.class)
.hasMessageContaining(ErrorCode.ORDER_NOT_FOUND.getMessage());
}
}
AI 生成测试的边界
Cursor 生成的测试覆盖了主干路径,但有几个坑它不会自动帮你想到:
-
并发场景缺失 :
create方法如果同时被两个线程调用,数据库唯一约束是否会抛异常?AI 通常不会写并发测试。 -
金额精度 :
BigDecimal的构造方式对不对?new BigDecimal("9999.00")是对的,但 AI 有时候会用new BigDecimal(9999.00)(浮点构造,精度丢失)。 -
空指针边界 :
OrderCreateDTO的remark是可选的,如果传 null,toEntity会不会 NPE?
你需要的操作是:让 AI 生成主干,你自己补充边界。Prompt 技巧是加一句------"请列出 5 个你认为还需要补充的边界测试场景"。
五、技术文档:从"写完代码不想写文档"到自动同步
技术文档的痛点不是写,而是代码变了,文档没更新,久而久之文档就没人信了。AI 可以帮你做到"代码即文档、文档随代码走"。
场景:用 SpringDoc + AI 生成高质量 OpenAPI 注释
java
package com.example.order.controller;
import com.example.common.result.Result;
import com.example.common.result.PageResult;
import com.example.order.dto.OrderCreateDTO;
import com.example.order.dto.OrderQueryDTO;
import com.example.order.dto.OrderUpdateDTO;
import com.example.order.entity.Order;
import com.example.order.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "订单管理", description = "订单的增删改查及分页查询接口")
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@Operation(summary = "创建订单", description = "根据用户提交的商品信息创建新订单,返回订单ID")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = Result.class))),
@ApiResponse(responseCode = "400", description = "参数校验失败(如金额为空、商品名称超限)"),
@ApiResponse(responseCode = "500", description = "系统内部错误")
})
@PostMapping
public Result<Long> create(
@Valid
@RequestBody
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "订单创建参数",
required = true,
content = @Content(schema = @Schema(implementation = OrderCreateDTO.class))
)
OrderCreateDTO dto
) {
return orderService.create(dto);
}
@Operation(summary = "分页查询订单", description = "支持按时间范围筛选,默认按创建时间倒序")
@ApiResponse(responseCode = "200", description = "查询成功")
@GetMapping
public Result<PageResult<Order>> pageQuery(
@Valid
@ParameterObject // SpringDoc 自动展开查询参数
OrderQueryDTO query
) {
return orderService.pageQuery(query);
}
}
Prompt 技巧: 把已有的 Controller 代码丢给 Cursor,然后说------"为所有接口添加 SpringDoc OpenAPI 3 注解,要求包含中文 summary、描述业务场景、列出 400/500 两种常见异常响应"。AI 在规范化、重复化的注释生成上非常可靠,至少比人写的一致性好。
更进一步:让 AI 帮你生成 Markdown 接口文档
如果你需要给前端或第三方团队输出离线的 API 文档,可以把 SpringDoc 生成的 OpenAPI JSON 喂给 AI,让它转成 Markdown:
html
请根据以下 OpenAPI 3 规范片段,生成前端友好的 Markdown 接口文档:
要求:
1. 包含请求示例(curl 命令)
2. 包含成功和失败的响应示例
3. 字段表格中标注"是否必填"和"示例值"
4. 用中文描述每个字段的业务含义
AI 生成的 Markdown 格式统一、不会漏字段,你只需要把示例数据换成真实的就行。
六、AI 代码审查:知道什么能信,什么必须人工
这是最重要的一节。很多兄弟用 AI 生成代码后直接合进仓库,这是极其危险 的操作。AI 代码审查的正确姿势是:AI 做初筛,人工做终审。
AI 适合做的代码审查(白名单)
| 审查类型 | AI 准确率 | 说明 |
|---|---|---|
| 空指针风险 | 85% | 常见的 NPE 模式能识别 |
| 资源泄露(未关闭流/连接) | 80% | 明显的 try-with-resources 遗漏 |
| 重复代码 | 90% | 结构相似度检测很成熟 |
| 明显性能问题(SQL N+1) | 75% | 需要结合 ORM 上下文 |
| 代码风格/命名规范 | 95% | 规则明确,AI 最擅长 |
AI 不适合做的代码审查(黑名单)
| 审查类型 | 为什么 AI 不行 | 必须由人工检查 |
|---|---|---|
| 业务逻辑正确性 | AI 不懂你的业务规则 | 支付金额校验、状态机流转 |
| 并发安全 | 需要理解全局执行顺序 | 竞态条件、死锁隐患 |
| 权限边界 | 需要理解系统安全模型 | 接口是否越权、数据是否隔离 |
| 数据一致性 | 需要理解分布式事务策略 | 最终一致性 vs 强一致性选择 |
| 架构设计合理性 | 需要理解演进方向 | 这坨代码该不该放在这里 |
实战:用 AI 做初筛 + 人工做终审的 checklist
html
/**
* AI 辅助代码审查 Checklist(适用于 Java 后端代码评审)
*
* 【AI 初筛阶段】------把代码丢给 Cursor/Copilot,让它回答以下问题:
* 1. 这段代码中有明显的空指针风险吗?列出具体行号。
* 2. 有未关闭的资源(Stream/Connection/Session)吗?
* 3. 有重复代码可以提取成方法吗?
* 4. 异常处理是否完善?有没有吞掉异常或只打印 e.printStackTrace()?
* 5. 日志中有没有可能打印敏感信息?
*
* 【人工终审阶段】------以下必须人工判断,不要信任 AI:
* 1. 这个接口的入参校验是否覆盖了所有业务约束?(比如:金额必须大于0)
* 2. 这个方法的事务边界是否合理?是否会有长事务风险?
* 3. 这个查询在数据量达到百万级时会不会慢?是否需要加索引?
* 4. 这个 API 的权限控制是否到位?有没有做数据范围隔离?
* 5. 如果下游服务挂了,这段代码会怎么表现?有没有熔断/降级/重试策略?
* 6. 这个改动是否破坏了现有契约?下游调用方是否需要同步修改?
*/
老梁的实操建议: 在 GitLab/GitHub 的 MR/PR 流程里,把 AI 初筛做成一个可选的 pipeline 步骤------不阻塞合并,但自动生成审查评论。人工 reviewer 把精力放在黑名单事项上,白名单事项让 AI 先扫一遍。这样一次代码审查的时间能从 30 分钟压缩到 10 分钟,同时质量不降反升。
七、实战建议:把 AI 真正用进工作流的三条铁律
1. Prompt 即代码,要像写代码一样写 Prompt
很多人生成代码质量差,不是因为 AI 不行,是因为 Prompt 写得像聊天。记住这个模板:
html
【角色】你是一个有10年经验的Java后端工程师
【上下文】以下是项目现有代码规范...
【任务】请为...生成...
【约束】
- 使用...
- 必须包含...
- 不要...
- 遵循...
【输出格式】Java 代码,包含 package/import/注释
约束条件越具体,AI 输出越稳。 这跟写代码接口定义是一个道理------参数类型定了,返回值就不会太离谱。
2. 建立"AI 生成 → 人工 Review → 单元测试 → 合入"的强制流水线
不要因为 AI 生成的代码"看起来对"就直接用。最低限度的检查:
-
能不能编译过?
-
已有的单元测试能不能跑过?
-
AI 生成的代码你有没有逐行看懂?(不是扫一眼,是逐行)
3. 用 AI 做"增量",不要做"全量"
不要对一个完全陌生的技术领域说"给我写一个完整的微服务架构"。AI 在增量任务上表现最好:
-
基于现有代码生成相似模块
-
基于现有测试生成更多边界用例
-
基于现有文档生成更新版本
全量架构设计、核心算法推导、复杂业务建模------这些是你的主场,别外包。
AI 不是你的替代者,它是你的放大器。你强,它让你更强;你弱,它只会把你的错误复制得更快。
用好了 Copilot 和 Cursor,一个 Java 后端工程师的日常编码效率提升 30%~50% 是完全可行的。但前提是你始终保持清醒:AI 负责重复,你负责判断;AI 负责速度,你负责质量。
下篇预告(Day 57): K8s 核心概念速通------Pod、Deployment、Service、Ingress,一次打通云原生的任督二脉。从 Docker 到 K8s,你的应用终于要从单机走向集群了。
本文示例代码基于 Spring Boot 3.2.5 + JDK 21 + JUnit 5.10 + Mockito 5.11,已在本地验证编译通过。