首页
/ ECC 技能解析:Spring Boot 生产级架构与 API 设计模式完整实践指南

ECC 技能解析:Spring Boot 生产级架构与 API 设计模式完整实践指南

2026-09-06 18:27:30作者:沈韬淼Beryl

本指南以开源仓库 ECC 内置技能文档 skills/springboot-patterns/SKILL.md(对应 .kiro 安装副本 .kiro/skills/springboot-patterns/SKILL.md)为骨架,系统讲解 Spring Boot 后端工程从 Controller → Service → Repository 分层、数据访问、缓存、异步、日志、限流到可观测性的全套生产级模式,并对照仓库中的自动装配映射与评审规则给出可执行的落地清单。读完本文将掌握一套"构建与评审"双视角下的 Spring Boot 最佳实践,可直接用于日常后端开发与代码评审。

ECC(agent harness performance optimization system)将本文所讲解的模式沉淀为一个名为 springboot-patterns 的技能:当 Agent 在 Java Spring Boot 后端上构建或评审代码(REST 层、服务层、数据访问、缓存或异步逻辑)时会被自动激活。技能本身是"模式知识库",模式代码示例中的市场(Market)领域模型只是用于演示的参考上下文,真正重要的是把每一层该做什么、边界划在哪里、防御点如何布置一次性讲清楚。

技能定位:何时应当启用 springboot-patterns

技能元数据(frontmatter)给出明确激活条件,凡命中以下场景,Agent 应加载该技能以约束实现风格:

  • 使用 Spring MVC 或 WebFlux 构建 REST API;
  • 组织 controller → service → repository 分层结构;
  • 配置 Spring Data JPA、缓存(Caching)或异步(Async)处理;
  • 引入 Bean Validation 校验、统一异常处理或分页;
  • 为 dev / staging / production 环境设置 Profile;
  • 使用 Spring Events 或 Kafka 实现事件驱动模式。

在 ECC 中该技能不是孤立存在的。查看 config/project-stack-mappings.json 可确认它隶属于 springboot 技术栈的默认技能组合:

技术栈项 内容
栈标识 springboot,名称 "Spring Boot (Java/Kotlin)"
项目识别信号 pom.xmlbuild.gradlebuild.gradle.kts 中包含 spring-boot 关键字
绑定规则 commonjava
绑定技能 springboot-patternsspringboot-tddspringboot-verificationspringboot-securityjava-coding-standardstdd-workflowverification-loop
常用命令 构建 ./mvnw compile / ./gradlew build;测试 ./mvnw test / ./gradlew test;lint ./mvnw checkstyle:check;格式化 ./mvnw spotless:apply;开发 ./mvnw spring-boot:run / ./gradlew bootRun
权限约束 允许 Maven/Gradle/java 执行,禁止 deploypublish 类操作

同时,manifests/install-modules.jsonskills/springboot-patterns 归入 framework-language(框架与语言核心工程技能)模块随包分发。这意味着:当你在一个含 spring-boot 的 Maven/Gradle 项目根目录运行 ECC 的 /project-init 时,上面的技能组合与构建命令会被自动配置好,springboot-patterns 即为 Spring Boot 类工程写作风格与架构约束的基准参照。

REST API 分层结构:controller 只做编排,不做业务

技能强调的第一条纪律是"薄 Controller"。控制器只负责接收 HTTP 请求、做参数绑定与权限级校验,然后立即委托给服务层,禁止在其中写业务逻辑(agents/java-reviewer.md 也将"控制器内的业务逻辑必须立刻下沉到服务层"列为 HIGH 级架构问题)。

技能文档给出一个完整的列表 + 创建双端点示例:

@RestController
@RequestMapping("/api/markets")
@Validated
class MarketController {
  private final MarketService marketService;

  MarketController(MarketService marketService) {
    this.marketService = marketService;
  }

  @GetMapping
  ResponseEntity<Page<MarketResponse>> list(
      @RequestParam(defaultValue = "0") int page,
      @RequestParam(defaultValue = "20") int size) {
    Page<Market> markets = marketService.list(PageRequest.of(page, size));
    return ResponseEntity.ok(markets.map(MarketResponse::from));
  }

  @PostMapping
  ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {
    Market market = marketService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market));
  }
}

关键点逐条拆解:

  • @Validated + @Valid 双保险:类级 @Validated 激活方法级参数校验(如 @RequestParam 约束),@Valid @RequestBody 触发请求体的 Bean Validation。
  • 构造函数注入而非字段注入:字段使用 private final,通过构造函数完成注入。技能在 "Production Defaults" 中明确"优先构造函数注入、避免字段注入",这保证 Bean 一旦构造即处于完整状态、便于单元测试,也消除了循环依赖的隐患。java-reviewer 规则将 @Autowired 字段注入直接标记为 code smell。
  • 返回 ResponseEntity<T> 精确控制状态码:创建成功返回 201 Created,列表成功返回 200 OK。评审规则中"创建接口返回 200 空 body 而不是 201、缺失资源返回 200 而不是 404"属于错误做法。
  • 领域对象不直接出网markets.map(MarketResponse::from) 把内部实体投影为响应 DTO。直接向客户端暴露 JPA 实体(懒加载代理、密码/内部字段泄漏、JSON 循环引用)是评审红线。
  • 响应即分页:返回 Page<T> 而非裸 List<T>,未分页的列表端点同样被评审规则列为 HIGH。

Repository 模式(Spring Data JPA):接口即查询契约

数据访问层使用 Spring Data JPA 的派生查询与 @Query 命名查询,仓库保持"简单",复杂逻辑一律上移到服务层:

public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
  @Query("select m from MarketEntity m where m.status = :status order by m.volume desc")
  List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);
}

要点与纵深:

  • @Query + 参数绑定防注入:JPQL 中通过命名参数 :status@Param("status") 标注)绑定,禁止字符串拼接查询。java-reviewer 将 @QueryJdbcTemplateNamedParameterJdbcTemplate 中的拼接 SQL 列为 CRITICAL 级注入风险。
  • Pageable 作为方法参数:排序与分页由调用方注入,Sort/PageRequest 与 SQL 解耦,天然避免"接口数量爆炸"。
  • N+1 防护意识:集合关联上禁用 FetchType.EAGER,改用 JOIN FETCH@EntityGraph;凡是返回集合的关联查询都要审视是否会逐条触发懒加载。此外,@Query 若包含更新/删除语句,必须加 @Modifying 并配合事务,否则运行时失败。
  • 实体只属于持久化层MarketEntity(带 JPA 注解的实体)与领域对象 Market、响应 MarketResponse 各自独立,这正是评审中"实体直接返回 controller"反模式的规避方式。

Service 层与事务:@Transactional 只出现在这里

服务层承载全部业务编排,是事务的唯一合法宿主。技能示例:

@Service
public class MarketService {
  private final MarketRepository repo;

  public MarketService(MarketRepository repo) {
    this.repo = repo;
  }

  @Transactional
  public Market create(CreateMarketRequest request) {
    MarketEntity entity = MarketEntity.from(request);
    MarketEntity saved = repo.save(entity);
    return Market.from(saved);
  }
}

实践要点:

  • 事务边界 = 服务方法@Transactional 放在 controller 或 repository 上都属于错误分层(java-reviewer 明确列出该问题)。事务跨多个仓库操作时才能保证原子性。
  • 读操作显式声明 readOnly = true:写操作默认读写事务,只读查询声明 readOnly = true 可让 Hibernate 关闭脏检查并允许底层驱动走读优化。这是技能 "Production Defaults" 的硬性要求。
  • repo.save() 的语义save 对已存在主键执行 merge、对新实体执行 persist,属于仓储封装而非"必须调用才落库";理解 @Transactional 下 flush 时机(方法提交时)比盲目依赖 save 返回值更重要。
  • 空值策略:服务层返回 Optional<T> 而不是 null;消费方用 orElseThrow 而不是裸 .get()(java-reviewer 将无保护的 .get() 列为 CRITICAL 错误处理缺陷)。

DTO 与 Bean Validation:record + 声明式校验

从 Java 16 起,DTO 首选 record 承载不可变数据,并用 Bean Validation 注解在边界完成声明式校验:

public record CreateMarketRequest(
    @NotBlank @Size(max = 200) String name,
    @NotBlank @Size(max = 2000) String description,
    @NotNull @FutureOrPresent Instant endDate,
    @NotEmpty List<@NotBlank String> categories) {}

public record MarketResponse(Long id, String name, MarketStatus status) {
  static MarketResponse from(Market market) {
    return new MarketResponse(market.id(), market.name(), market.status());
  }
}

参数语义说明:

注解 作用 典型取值/触发时机
@NotBlank 字符串非空且去空白后长度 > 0 name、description
@Size(max = n) 上限约束,配合 @NotBlank 限制长度 200(短文本)、2000(长文本)
@NotNull 引用不可为空 endDate
@FutureOrPresent 时间必须为当前或未来(面向截止日期类业务) Instant/日期字段
@NotEmpty 容器非空 categories
List<@NotBlank String> 容器元素级校验(type-use 约束) 集合内每个元素均须合法

配套技巧:请求 DTO(入站)与响应 DTO(出站)分离,MarketResponse.from(market) 这类静态工厂统一做领域对象 → DTO 投影,禁止 controller 手写散落的字段拷贝;若存在高度复用的转换,可进一步使用 MapStruct 生成映射代码,但技能示例保持纯手写以最小化依赖。

集中式异常处理:@ControllerAdvice + 语义化状态码

业务异常绝不允许散落在 controller 的 try-catch 中。技能给出统一出口:

@ControllerAdvice
class GlobalExceptionHandler {
  @ExceptionHandler(MethodArgumentNotValidException.class)
  ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
    String message = ex.getBindingResult().getFieldErrors().stream()
        .map(e -> e.getField() + ": " + e.getDefaultMessage())
        .collect(Collectors.joining(", "));
    return ResponseEntity.badRequest().body(ApiError.validation(message));
  }

  @ExceptionHandler(AccessDeniedException.class)
  ResponseEntity<ApiError> handleAccessDenied() {
    return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));
  }

  @ExceptionHandler(Exception.class)
  ResponseEntity<ApiError> handleGeneric(Exception ex) {
    // Log unexpected errors with stack traces
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body(ApiError.of("Internal server error"));
  }
}

要点说明:

  • 按异常类型映射状态码:校验失败 400、权限不足 403、兜底未知异常 500。兜底 handler 不应把异常堆栈回写给客户端,只记录日志(示例注释明确"Log unexpected errors with stack traces")。
  • 校验错误聚合:遍历 BindingResult 的 field errors,拼成 "field: message" 形式返回,前端可直接展示。
  • 统一错误信封 ApiError:让所有失败响应共享同一结构(技能示例中的 ApiError.validation(...)ApiError.of(...) 为错误体构造入口),客户端可稳定解析。若需要字段级明细,可扩展为 {"code","message","fieldErrors":[...]} 结构。
  • Spring Boot 3+ 进阶:开启 spring.mvc.problemdetails.enabled=true 即可获得符合 RFC 7807 的 application/problem+json 标准错误体,与 @ControllerAdvice 方案互补。
  • 一致性要求:若工程内某处异常处理绕过了该 Advice、在 controller 内各自为政,java-reviewer 将直接判为 "No @RestControllerAdvice(异常处理散落)" 缺陷。

缓存:@Cacheable / @CacheEvict 与开启开关

技能文档强调:缓存注解本身并不生效,必须在某个配置类上声明 @EnableCaching,否则 @Cacheable 只是摆设。

@Service
public class MarketCacheService {
  private final MarketRepository repo;

  public MarketCacheService(MarketRepository repo) {
    this.repo = repo;
  }

  @Cacheable(value = "market", key = "#id")
  public Market getById(Long id) {
    return repo.findById(id)
        .map(Market::from)
        .orElseThrow(() -> new EntityNotFoundException("Market not found"));
  }

  @CacheEvict(value = "market", key = "#id")
  public void evict(Long id) {}
}
  • @Cacheable(value = "market", key = "#id"):缓存名 market(对应底层 Cache 区域),键 SpEL #id 使用方法入参。命中缓存时方法体不执行;未命中则执行并将结果入缓存。
  • @CacheEvict:更新/删除场景删除对应键,防止脏读。缓存一致性采用"先写库、后逐出"的策略时注意事务提交顺序,避免提交前逐出导致并发窗口。
  • EntityNotFoundException 抛出语义:读不到数据时的统一业务异常,供前面的全局 Advice 映射 404。
  • 缓存本质上仍要处理并发击穿/穿透/雪崩;技能保持"最小可用 + 显式驱逐"的克制,更复杂的分布式缓存策略属于上层架构决策。

异步处理:@EnableAsync + @Async + 有界线程池

与缓存同理,异步调用需要配置类声明 @EnableAsync,随后在服务方法上加 @Async 即可异步执行:

@Service
public class NotificationService {
  @Async
  public CompletableFuture<Void> sendAsync(Notification notification) {
    // send email/SMS
    return CompletableFuture.completedFuture(null);
  }
}

关键认知:

  • @Async 的代理本质:Spring 通过 AOP 代理切入异步执行。调用必须经由 Spring 管理的 Bean 代理(从另一个 Bean 注入调用),同类内部 this 自调用会绕过代理导致异步失效。
  • 返回类型:返回 void(调用即弃)或 CompletableFuture<T>(可聚合)。不要在 @Async 方法上返回裸 Future 之外的复杂类型。
  • 线程池必须显式配置:java-reviewer 明确指出,Spring 默认的 SimpleAsyncTaskExecutor 每任务新建线程、无界增长;@AsyncCompletableFuture 若不配置自定义 Executor,将产生无界线程风险。生产环境应提供有界、带队列与拒绝策略的 ThreadPoolTaskExecutor Bean。
  • 失败面void 异步方法的异常对调用方不可见,需在 Executor 或方法内捕获上报指标/日志,保证失败可观测。

日志(SLF4J):参数化占位符与包含上下文的错误日志

技能示例使用 SLF4J API,强调"结构化字段 + 不拼接字符串":

@Service
public class ReportService {
  private static final Logger log = LoggerFactory.getLogger(ReportService.class);

  public Report generate(Long marketId) {
    log.info("generate_report marketId={}", marketId);
    try {
      // logic
    } catch (Exception ex) {
      log.error("generate_report_failed marketId={}", marketId, ex);
      throw ex;
    }
    return new Report();
  }
}
  • 参数化日志"marketId={}" 占位符由日志框架延迟求值,避免字符串拼接开销,无谓的拼接在热路径上是真实成本。
  • 事件名风格generate_reportgenerate_report_failed 用下划线命名事件,后续可按事件名聚合统计成功率。
  • 异常日志的完整调用栈log.error(msg, args, ex) 把异常作为最后一个参数传入,保证堆栈完整落盘。
  • 绝不记敏感信息:java-reviewer 在 CRITICAL 安全项中强调,认证附近代码若出现密码、令牌入日志,即为 PII/凭据泄漏事故(Spring 侧重点检查 SLF4J 的 log.info(...))。
  • 生产进阶:通过 Logback JSON encoder 输出结构化日志,便于采集进日志系统做字段检索(与后文 Observability 呼应)。

过滤器(OncePerRequestFilter):请求日志与请求级切面

Servlet 请求日志使用继承 OncePerRequestFilter@Component,保证同一请求在转发场景下只执行一次:

@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
  private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    long start = System.currentTimeMillis();
    try {
      filterChain.doFilter(request, response);
    } finally {
      long duration = System.currentTimeMillis() - start;
      log.info("req method={} uri={} status={} durationMs={}",
          request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
    }
  }
}
  • try/finally 保证必记:无论下游抛异常与否,响应状态与耗时都会被记录(response.getStatus() 在异常被全局 Advice 处理前可能仍为默认 200,若需精确值可在 Advice 中另行补充——这是实现层面需留意的细节)。
  • 声明为 @Component 即自动注册:无需手写 FilterRegistrationBean,Spring Boot 会自动将其加入过滤器链并作用于全部请求。
  • 注意 OncePerRequestFiltershouldNotFilter 可对静态资源、监控端点做白名单,避免日志噪声。

分页与排序:PageRequest 组合 Sort

PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<Market> results = marketService.list(page);
  • PageRequest.of(page, size, sort):从 0 起始的页码 + 页大小 + Sort 排序。
  • Page<Market> 传给 controller 后,响应可包含 totalElementstotalPageshasNext 等元数据;技能示例用 Page.map(MarketResponse::from) 投影,避免实体出网。
  • 白名单排序字段:直接透传用户可控排序字段存在注入与滥用风险,生产实现应对可排序字段做白名单映射后再构造 Sort

面向外部调用的弹性重试:带退避上限的手写实现与生产级替代

技能提供了"零依赖可用"的手写重试工具方法,并明确给出生产建议——若追求健壮性请使用 Resilience4j / Spring Retry(含熔断、指标与可配置策略):

public <T> T withRetry(Supplier<T> supplier, int maxRetries) {
  final long maxBackoffMillis = 10_000L;
  int attempts = 0;
  while (true) {
    try {
      return supplier.get();
    } catch (Exception ex) {
      attempts++;
      if (attempts >= maxRetries) {
        throw ex;
      }
      try {
        long backoff = Math.min((long) Math.pow(2, attempts) * 100L, maxBackoffMillis);
        Thread.sleep(backoff);
      } catch (InterruptedException ie) {
        Thread.currentThread().interrupt();
        throw ex;
      }
    }
  }
}
  • 指数退避 + 上限封顶:每次重试间隔 2^attempts * 100ms,并 Math.min 收敛到 maxBackoffMillis = 10s,防止对下游造成风暴。
  • 中断即放弃并复位中断位:捕获 InterruptedExceptionThread.currentThread().interrupt() 恢复中断状态再抛出原异常,这是 Java 并发下处理中断的标准姿势。
  • 只重试可恢复异常:示例捕获 Exception 属教学简化,生产应只对超时、瞬时网络错误等重试,业务异常直接上抛;同时 java-reviewer 提醒"指数退避不加抖动会导致惊群效应(thundering herd)",大量客户端同时重试会在退避点齐射,生产应叠加随机 jitter。

限流:Bucket4j 过滤器 + 对 X-Forwarded-For 的安全处理

技能用 OncePerRequestFilter + Bucket4j 令牌桶实现按客户端 IP 的限流,核心代码如下:

@Component
public class RateLimitFilter extends OncePerRequestFilter {
  private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    String clientIp = request.getRemoteAddr();

    Bucket bucket = buckets.computeIfAbsent(clientIp,
        k -> Bucket.builder()
            .addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
            .build());

    if (bucket.tryConsume(1)) {
      filterChain.doFilter(request, response);
    } else {
      response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
    }
  }
}

这是全文档安全警示最密集的一节,务必逐条落实:

  1. X-Forwarded-For 默认不可信:客户端可以任意伪造该头。技能明确列出仅在同时满足以下条件时才可依赖转发头:应用位于可信反向代理(nginx、AWS ALB 等)之后;已注册 ForwardedHeaderFilter Bean;已在配置中设置 server.forward-headers-strategy=NATIVEFRAMEWORK;代理配置为覆盖(overwrite)而非追加(append) X-Forwarded-For
  2. 客户端 IP 的取值策略:当 ForwardedHeaderFilter 正确配置后,request.getRemoteAddr() 会自动从转发头解析出真实客户端 IP;未配置时它返回直连 IP(即代理 IP),而这恰恰是唯一可信的值。技能代码注释反复强调:永远不要直接读取 X-Forwarded-For——在缺少可信代理处理时它可被轻松伪造,攻击者可伪造任意 IP 绕过限流或干扰他人额度。
  3. 限流参数:示例为每个 IP 构建一个容量 100、以每分钟 100 令牌速率补充(Refill.greedy 即补满式)的令牌桶,每次请求消耗 1 令牌;桶满则返回 429 Too Many Requests。令牌桶相比固定窗口能容忍突发。
  4. 内存态 Map 的适用边界ConcurrentHashMap 保存桶实例适合单实例场景;多实例部署需把计数状态外置(Redis 等),否则限流总额会被实例数放大。
  5. 可配置项对齐:与 server.tomcat.remoteip.trusted-proxies 或容器对应的远程 IP 信任配置配合,才能让 NATIVE 策略准确识别可信代理列表。

后台任务:@Scheduled 与消息队列

技能给出的方向性约束:使用 Spring 的 @Scheduled 定时执行,或与消息队列(Kafka、SQS、RabbitMQ)集成做异步解耦;处理器必须幂等且可观测

配套要点(与 java-reviewer 规则互相印证):

  • 调度线程勿被长任务阻塞:长时间运行的 @Scheduled 方法不应占用调度线程;同时,调度方法的异常若不捕获会导致后续触发被吞,需要异常处理与监控告警。
  • 分布式锁:多实例部署下同一定时任务会在每个实例执行,需引入分布式锁(如基于数据库/ShedLock/Redis)保证单实例执行。
  • 消费端幂等:事件可能重复投递,处理逻辑必须以业务键去重(如唯一约束 + 幂等表),保证重复消息无副作用。
  • 失败兜底:消息处理失败要有重试队列与死信(DLQ)策略,避免消息无限重投或静默丢失。

可观测性三件套

技能列出生产系统必须具备的观测能力,方向清晰、可直接对标:

  • 结构化日志(JSON):通过 Logback encoder(如 logstash-logback-encoder)输出 JSON 行,字段可被日志平台直接检索、聚合与告警。
  • 指标(Metrics):Micrometer 作为门面,暴露给 Prometheus(/actuator/prometheus)或 OpenTelemetry(OTel)采集,覆盖 HTTP 延迟、错误率、线程池、连接池等核心指标。
  • 链路追踪(Tracing):Micrometer Tracing 接 OpenTelemetry 或 Brave 后端,为分布式请求生成 traceId/spanId,贯穿网关、服务与数据库,使"请求日志 → 指标 → 链路"可交叉关联。

落地最小集建议:引入 spring-boot-starter-actuator 暴露健康与指标端点,配合上述方案即可得到可监控的基座。

生产默认配置清单(Production Defaults)

技能在结尾给出了可直接当"验收清单"使用的生产默认项:

  1. 构造函数注入优先,杜绝字段注入@Autowired 字段注入 = 反模式)。
  2. 开启 RFC 7807 标准错误体:Spring Boot 3+ 设置 spring.mvc.problemdetails.enabled=true
  3. 显式配置 HikariCP 连接池:按工作负载设定最大/最小池大小并配置连接超时(如 spring.datasource.hikari.maximum-pool-sizeconnection-timeout),避免默认值不适配高并发或导致连接耗尽。
  4. 查询型服务方法声明 @Transactional(readOnly = true)
  5. 空安全策略:恰当使用 @NonNull(或 org.springframework.lang.NonNull)注解与 Optional 返回值,消灭隐式 null 传递。

总结口诀(技能原文):让 controller 保持薄、service 保持专注、repository 保持简单、异常集中处理,一切以可维护性与可测试性为优化目标。

在 ECC 中的联动机制:技能如何被"评审者"消费

springboot-patterns 不仅是写作指南,还是 ECC 代码评审管线的模式基准。Java 评审 Agent agents/java-reviewer.md 在 frontmatter 中声明"Expert Java code reviewer for Spring Boot and Quarkus projects",其评审规则与本技能一一呼应,形成"规范 → 评审"闭环:

技能中的约定 java-reviewer 对应检查
构造函数注入优先 @Autowired 字段注入 → code smell(HIGH)
@Transactional 只放 Service 出现在 controller/repository → HIGH 缺陷
实体不出网、DTO 投影 实体直接从 controller 返回 → HIGH 缺陷
集中式异常处理 @RestControllerAdvice → CRITICAL 错误处理缺陷
读取用 Optional 无保护 .get() → CRITICAL
参数化查询 @Query/JdbcTemplate SQL 拼接 → CRITICAL 注入
@Query 更新语句 @Modifying → HIGH(JPA 运行时失败)
分页返回 Page<T> List<T> 无分页 → HIGH
N+1 防护 集合 FetchType.EAGER → HIGH
@Async 需要自定义 Executor 默认无界线程 → MEDIUM
重试需加 jitter 纯指数退避 → MEDIUM(惊群效应)

评审 Agent 还给出配套的测试切面建议:单元测试避免 @SpringBootTest 全量启动,controller 用 @WebMvcTest、repository 用 @DataJpaTest、服务层用 @ExtendWith(MockitoExtension.class) + Mockito;异步断言使用 Awaitility 而不是 Thread.sleep。这些与 skills/springboot-tdd/SKILL.md(Spring Boot TDD 技能)、skills/springboot-security/SKILL.md(安全加固技能)、skills/springboot-verification/SKILL.md(验证技能)共同构成完整的 Spring Boot 技能族,后者在 manifests/install-modules.jsonframework-language 模块中与本文技能同批分发。

一套完整的"对照式"落地建议

把整篇技能落成一个可执行的行动序列:

  1. 开工前(Spring Boot 工程识别):确认 pom.xml/build.gradle(.kts)spring-boot,进入本技能上下文;初始化时同步加载 springboot-tddspringboot-securityspringboot-verification
  2. 建控制器:只写 URL 映射、参数绑定与状态码;用 @Validated + @Valid;返回 DTO 或 Page<DTO>
  3. 建服务:承载事务与业务编排;写方法加 @Transactional,读方法加 readOnly = true;返回 Optional 或用 orElseThrow
  4. 建仓库:继承 JpaRepository,用 @Query 参数绑定或派生查询;避免 EAGER 与 N+1。
  5. 定义 DTO/校验:record + Bean Validation,入站出站分离。
  6. 加全局异常处理:一个 @ControllerAdvice 覆盖校验、鉴权、兜底三类;Spring Boot 3+ 开启 problemdetails。
  7. 按需开启横切能力:缓存/异步分别补齐 @EnableCaching@EnableAsync 与有界线程池;过滤器统一做请求日志与限流,并严格按代理头安全清单配置。
  8. 收尾加固:结构化日志、Micrometer 指标、链路追踪;按 Production Defaults 五项做最终复查。

整个模式体系的设计取向非常清晰:它不追求炫技,而是把每一层的职责、边界与失败处理收敛成可复用、可评审、可测试的最小完备集合——这正是"controllers thin, services focused, repositories simple, errors handled centrally"所代表的、面向长期维护与生产可观测性的工程审美。

登录后查看全文
热门项目推荐
相关项目推荐