首页
/ ECC Agent 体系中的 Java 模式实践指南:从记录类型到领域安全的现代 Java 工程规范

ECC Agent 体系中的 Java 模式实践指南:从记录类型到领域安全的现代 Java 工程规范

2026-09-06 18:42:45作者:田桥桑Industrious

本指南以 .kiro/steering/java-patterns.md 这一 Java 方向 steering 文档为主线,系统讲解 ECC(Agent Harness Performance Optimization System)为其所驱动的 Claude Code / Codex / Opencode / Cursor 等 Agent 注入的 Java 工程规范——涵盖不可变性、现代 Java 语言特性、构造器注入、仓储模式、Optional 使用、异常处理、安全与测试。读完本文,你将掌握一套可直接照搬到 Spring Boot / Quarkus / 纯 Java 项目中的编码约定,并理解这些约定在仓库内 java-reviewer Agent、rules 规则集与 skill 技能中的落地方式,便于被 Agent 与 LLM 检索复用。

1. 这份 Java 模式文档在仓库中的角色

.kiro/steering/java-patterns.md 是一份带 frontmatter 的 steering(方向性约束)文档:

---
inclusion: fileMatch
fileMatchPattern: "*.java"
description: Java-specific patterns, Spring Boot, and enterprise best practices.
---
  • fileMatchPattern: "*.java":表示该文档在 Agent 处理 Java 源文件 时自动生效,属于按文件类型触发的规则注入;
  • inclusion: fileMatch:区别于 .kiro/steering/patterns.mdinclusion: auto 的"自动注入"模式,本文件仅在命中的文件上下文中参与;
  • 文档自身声明"This file extends the common patterns with Java specific content",即它是 .kiro/steering/patterns.md(仓储模式、API 响应信封、骨架项目流程)与 .kiro/steering/coding-style.md(不可变性 CRITICAL、文件组织、错误处理、代码质量清单)的 Java 化补充。

仓库中与之一致的整套 Java 知识分层是:steering 文档(方向性总纲)→ rules(按 **/*.java 路径触发的行为规则)→ skills(可调用技能,提供代码示例库)→ agents(java-reviewer 执行审查)

2. 不可变性(Immutability)

2.1 三条核心规则

文档将不可变性列为 Java 模式第一条,具体约束:

  1. 值类型优先用 record(Java 16+)
  2. 字段默认声明为 final,仅在确实需要时使用可变状态;
  3. 对外返回防御性拷贝List.copyOf()Map.copyOf()

文档给出的最小范例:

public record OrderSummary(Long id, String customerName, BigDecimal total) {}

该规则的更深层动机在 .kiro/steering/coding-style.md 中被标记为 CRITICAL

WRONG:  modify(original, field, value) → changes original in-place
CORRECT: update(original, field, value) → returns new copy with change

理由:不可变数据杜绝隐藏副作用、降低调试难度,并能安全支持并发——这与 java-reviewer 审查项中"可变单例字段(singleton 作用域 Bean 中的非 final 实例字段是竞态条件)"的判例互为印证。

2.2 record 与"无 setter"的配合写法

skills/java-coding-standards/SKILL.md 对不可变性做了进一步展示,推荐两种形态:

public record MarketDto(Long id, String name, MarketStatus status) {}

public class Market {
  private final Long id;
  private final String name;
  // 只提供 getter,不提供 setter
}

同时也记录了框架惯例带来的边界:在 Quarkus 的 Panache active-record 模型里,public class Market extends PanacheEntity { public String name; } 使用公有字段是 Quarkus 惯例(访问器在构建期生成),说明不可变性规范需要结合具体框架的领域模型形态来应用。

3. 现代 Java 语言特性(Modern Java Features)

文档明确给出特性与版本对照,便于工程落地时设定编译基线:

特性 最低 Java 版本 典型用途
Records Java 16+ DTO 与值类型
Sealed classes Java 17+ 封闭类型层级
Pattern matching with instanceof Java 16+ 免除显式强转
Switch expressions(arrow 语法) Java 14+ 表达式的多路分支

文档中的封闭类型与记录的组合示例:

public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure {}
record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
record PaymentFailure(String errorCode, String message) implements PaymentResult {}

rules/java/patterns.md 中该模式被推进到穷尽式 switch(Java 21+),编译器可验证所有分支均已覆盖:

String message = switch (result) {
    case PaymentSuccess s -> "Paid: " + s.transactionId();
    case PaymentFailure f -> "Failed: " + f.errorCode();
};

对应到审查侧,.kiro/agents/java-reviewer.md 把"先 instanceof 判断再做显式强转、却未使用 pattern matching"列为 MEDIUM 级代码味道,反向印证了文档要求:Java 16+ 环境下应直接写 if (obj instanceof PaymentSuccess s) { s.transactionId(); }

4. 构造器注入:杜绝字段注入

文档给出正反例对照:

// GOOD
public class NotificationService {
    private final EmailSender emailSender;
    public NotificationService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }
}

// BAD — field injection
@Inject private EmailSender emailSender;

选择构造器注入的理由(在 rules/java/patterns.md 中有完整注释):依赖为 final,与不可变性配合;无反射也能实例化,可测试性最好;依赖关系在构造期即被满足。

仓库内进一步细分了 Spring 与 Quarkus 的差异(见 skills/java-coding-standards/SKILL.md):

// [SPRING] 构造器注入(优于字段 @Autowired)
@Service
public class MarketService {
  private final MarketRepository marketRepository;
  public MarketService(MarketRepository marketRepository) {
    this.marketRepository = marketRepository;
  }
}

// [QUARKUS] 构造器注入
@ApplicationScoped
public class MarketService {
  private final MarketRepository marketRepository;
  @Inject
  public MarketService(MarketRepository marketRepository) { ... }
}

// [QUARKUS] 例外:包私有字段注入在 Quarkus 中可接受(规避代理问题)
// [QUARKUS] 避免 @Singleton 当需要拦截/懒加载时 → 用 @ApplicationScoped(@Singleton 不可代理)

在审查红线层面,.kiro/agents/java-reviewer.md 将 Spring 中 @Autowired 字段注入列为 HIGH(架构) 级问题,并要求 @Transactional 置于 service 层而非 controller/repository 层。

5. 仓储模式(Repository Pattern)

文档的核心接口形态:

public interface OrderRepository {
    Optional<Order> findById(Long id);
    List<Order> findAll();
    Order save(Order order);
    void deleteById(Long id);
}

四个方法的返回值选择本身就有讲究:findById 返回 Optional<Order> 与第 6 节"查询可能无结果时返回 Optional"呼应,save 返回被持久化的对象以支持自动生成主键回填。

底层抽象思路在 .kiro/steering/patterns.md 通用层给出:仓储把数据访问封装在统一接口背后(findAll / findById / create / update / delete),具体实现负责存储细节(数据库、API、文件等),业务逻辑只依赖抽象接口而非存储机制——从而支持数据源切换并用 mock 简化测试。

在 Spring Data JPA 世界(见 skills/springboot-patterns/SKILL.md),仓库接口通常直接继承 JpaRepository 并叠加 @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);
}

审查端则围绕 JPA 仓储给出高频红线:集合上的 FetchType.EAGER(N+1 查询问题,应改 JOIN FETCH@EntityGraph)、无分页的 List<T> 接口、变更型 @Query 缺少 @Modifying@Transactional、以及 CascadeType.ALL + orphanRemoval = true 的危险级联。

6. Optional 使用规范

文档给出的三条纪律:

  1. 查找类方法可能无结果时返回 Optional<T>
  2. 使用 map()flatMap()orElseThrow()——绝不脱离 isPresent() 调用 get()
  3. 永不将 Optional 用作字段类型或方法参数(Optional 是容器语义,不是存储/传参载体)。

service 层的典型落地(skills/java-coding-standards/SKILL.md 中的 PASS 范例):

Optional<Market> market = marketRepository.findBySlug(slug);

return market
    .map(MarketResponse::from)
    .orElseThrow(() -> new EntityNotFoundException("Market not found"));

Quarkus / Panache 下对应的 finder 形态是 Market.find("slug", slug).firstResultOptional()。审查端则把 在未做 isPresent() 检查时调用 .get() 列为 CRITICAL 级错误处理问题,把"service 层返回 null 而非 Optional<T>"列为 MEDIUM 级 Java 惯用法问题。

7. 错误处理:领域异常 + 不泄露内部细节

文档的异常设计原则与示例:

public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(Long id) {
        super("Order not found: id=" + id);
    }
}

要点归纳:

  • 领域错误优先使用非受检异常(unchecked),避免受检异常污染业务方法签名;
  • 为领域错误创建专属异常类,继承 RuntimeException
  • 永不把堆栈追踪暴露在 API 响应中——客户端只能看到安全的通用消息,细节留在服务端日志。

三层纵深做法在仓库规则与技能中均有展开(rules/java/security.mdrules/java/patterns.md):

  • 中心化异常处理:Spring 用 @RestControllerAdvice / @ExceptionHandlerskills/springboot-patterns/SKILL.md 展示了验证异常、AccessDeniedException、兜底 Exception 三类 handler 分别映射 400/403/500);Quarkus 用 @Provider ExceptionMapper 或 RESTEasy Reactive 的 @ServerExceptionMapper
  • 服务端记录细节、客户端返回通用消息catch (Exception ex) { log.error("...", ex); return ApiResponse.error("Internal server error"); }——绝不把 ex.getMessage() 下发给客户端;
  • 统一响应信封rules/java/patterns.md):ApiResponse<T>(boolean success, T data, String error) 提供 ok(data) / error(message) 工厂方法,作为所有 API 的返回容器。

审查端把"空 catch 块 / catch (Exception e) {} 吞异常"与"缺中心化异常处理(无 @RestControllerAdviceExceptionMapper)"列为 CRITICAL,把"200 OK 空 body 当 404 返回"列为 CRITICAL 级 HTTP 语义错误。

8. 安全底线(Security)

文档的安全条目既是编码规范,也可直接作为 Java 安全 checklist:

规范 落实方式
永不硬编码密钥 System.getenv("API_KEY")(生产用 Vault / AWS Secrets Manager)
永远使用参数化查询 PreparedStatement、JPA、JDBC template,杜绝 SQL 字符串拼接
在 DTO 上使用 Bean Validation @NotNull@NotBlank@Size
口令存储 bcrypt 或 Argon2(绝不 MD5 / SHA1)

rules/java/security.md 的正反例可以补充到你的代码评审对照中:

// BAD — SQL 注入:字符串拼接
String sql = "SELECT * FROM orders WHERE name = '" + name + "'";

// GOOD — PreparedStatement 参数化
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
ps.setString(1, name);

// GOOD — JDBC template 参数绑定
jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);

密钥读取配合快速失败:

String apiKey = System.getenv("PAYMENT_API_KEY");
Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");

补充红线(见 .kiro/agents/java-reviewer.md 的 CRITICAL 清单):用户输入进 ProcessBuilder / Runtime.exec()(命令注入)、用户输入直接 new File(...)(路径穿越)、日志输出口令 / token / PII、请求体缺 @Valid 校验、以及 Spring Security 中禁用 CSRF 但未说明理由。

9. 测试:JUnit 5 + AssertJ + Mockito + Testcontainers

文档定义的测试技术栈与目标:

  • JUnit 5 + AssertJ(流式断言);
  • Mockito(依赖模拟);
  • Testcontainers(集成测试);
  • JaCoCo,目标 80%+ 行覆盖率

文档中的单元测试范例(同时体现不可变性:record 构造 + 无 setter 访问 customerName()):

@Test
@DisplayName("findById returns order when exists")
void findById_existingOrder_returnsOrder() {
    var order = new Order(1L, "Alice", BigDecimal.TEN);
    when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
    var result = orderService.findById(1L);
    assertThat(result.customerName()).isEqualTo("Alice");
}

rules/java/testing.md 在此基础上补全了"错误路径也要测试"与验证交互的惯例:

@Test
@DisplayName("findById throws when order not found")
void findById_missingOrder_throws() {
    when(orderRepository.findById(99L)).thenReturn(Optional.empty());
    assertThatThrownBy(() -> orderService.findById(99L))
        .isInstanceOf(OrderNotFoundException.class)
        .hasMessageContaining("99");
}

以及参数化测试、Testcontainers 真实数据库集成测试、测试目录镜像 src/main/java 包结构等惯例。审查端对测试的关注点是"作用域过大的注解":单元测试应使用 @WebMvcTest / @DataJpaTest 切片而非 @SpringBootTest,禁止 Thread.sleep()(改用 Awaitility),弱测试命名应改 should_return_404_when_user_not_found 风格。

10. 在仓库中的落地:steering → agents → skills 联动

文档末尾的 Reference 段表明,Java 方向知识是"多层级协同"而非单文件自治:

当你在自己的 Java 工程中引入这些约定时,无需改动仓库——只需按上述分层把"总纲 + 规则 + 技能 + 审查 Agent"复制进你的工作区(本仓库为只读参考),并在构建配置中锁定 Java 17+(记录、密封类、pattern matching、switch 表达式完整可用)即可落地。

11. 一页速查清单

写作与审查 Java 代码时可快速核对:

  • 值类型与 DTO 用 record;字段默认 final;返回 List.copyOf() / Map.copyOf()
  • 分支收口的领域模型用 sealed interface/class + switch 穷尽匹配;
  • 依赖一律构造器注入,标记 final 字段,不用字段注入;
  • 数据访问收敛在仓储接口后,findById 返回 Optional;勿对集合用 FetchType.EAGER
  • 只在"可能无结果的 finder"返回 Optional,永不把 Optional 当字段/参数;不裸调 get()
  • 领域异常继承 RuntimeException,由 @RestControllerAdvice / ExceptionMapper 中心化映射,API 响应永不带堆栈;
  • 密钥走环境变量/密钥管理;SQL 全参数化;DTO 加 Bean Validation;口令 bcrypt/Argon2;
  • JUnit 5 + AssertJ + Mockito + Testcontainers + JaCoCo(80%+),错误路径与正确路径一起测。

这套体系的核心价值在于:把 Java 工程最佳实践转译成 Agent 可自动注入(fileMatch *.java)、可检索、可执行的机器友好规则,从而让 AI 辅助编码与人工审查遵循同一条标准线。

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