第1848篇:技术规范检查自动化——AI审查代码是否符合团队编码标准
2026/4/30大约 8 分钟
第1848篇:技术规范检查自动化——AI审查代码是否符合团队编码标准
每个团队都有一套编码规范。大部分团队也都有一套编码规范执行率接近于零的现实。
原因很好理解:Checkstyle能检查格式,SpotBugs能检查bug模式,但有些规范没法用静态分析工具来检查。比如:
- "所有对外接口必须有明确的错误码定义"
- "Service层不允许直接依赖另一个Service层,必须通过接口抽象"
- "所有异步操作必须有超时设置和降级处理"
- "核心业务方法必须有完整的日志埋点"
这些规范理解起来需要语义,静态分析工具做不到,但AI可以。
今天这篇,讲怎么构建一套AI驱动的技术规范检查系统,把团队的编码标准变成可以自动执行的检查器。
规范检查的分类框架
在构建系统之前,先把要检查的规范分个类。这很重要,不同类型的规范适合不同的检查策略:
我们的AI检查器专注于"需语义理解类"和"架构约束类",和Checkstyle、SonarQube做互补,而不是替代。
规范定义系统
第一个核心设计决策:规范要如何描述。
如果直接写在Prompt里,维护起来很麻烦。我设计了一套YAML格式的规范描述体系:
# coding-standards.yml
version: "1.0"
team: "后端平台团队"
standards:
- id: "LOG-001"
name: "核心业务操作日志规范"
level: "ERROR" # ERROR/WARNING/INFO
description: "所有涉及数据修改的Service方法必须有明确的操作日志"
applies_to:
annotations: ["Service"]
method_patterns: ["create*", "update*", "delete*", "cancel*", "pay*"]
check_points:
- "方法入口必须有日志记录,包含操作类型和关键参数"
- "方法成功后必须有成功日志"
- "异常情况必须有ERROR级别日志,且包含足够的错误上下文"
bad_example: |
public void createOrder(OrderRequest request) {
// 没有任何日志
Order order = orderRepository.save(new Order(request));
}
good_example: |
public void createOrder(OrderRequest request) {
log.info("创建订单, userId={}, amount={}", request.getUserId(), request.getAmount());
try {
Order order = orderRepository.save(new Order(request));
log.info("订单创建成功, orderId={}", order.getId());
} catch (Exception e) {
log.error("订单创建失败, userId={}, reason={}",
request.getUserId(), e.getMessage(), e);
throw e;
}
}
- id: "TIMEOUT-001"
name: "外部调用超时规范"
level: "ERROR"
description: "所有对外部服务(HTTP/RPC)的调用必须设置超时时间"
applies_to:
used_classes: ["RestTemplate", "FeignClient", "WebClient", "HttpClient"]
check_points:
- "调用外部服务时必须有显式的超时配置"
- "超时后必须有明确的降级处理逻辑"
- "超时时间必须是可配置的,不能硬编码"
- id: "ERROR-CODE-001"
name: "接口错误码规范"
level: "WARNING"
description: "Controller接口的异常必须映射到规范的错误码,不能直接返回HTTP 500"
applies_to:
annotations: ["RestController", "Controller"]
check_points:
- "所有可预期的业务异常必须有对应的业务错误码"
- "错误码必须来自系统统一定义的ErrorCode枚举"
- "不允许直接抛出RuntimeException到接口层"
- id: "ARCH-001"
name: "Service层依赖规范"
level: "ERROR"
description: "Service实现类不允许直接依赖另一个具体的Service实现类,必须通过接口依赖"
applies_to:
annotations: ["Service"]
check_points:
- "注入的依赖必须是接口类型,而不是具体实现类"
- "如果需要调用另一个Service,必须注入其接口"
- id: "TRANS-001"
name: "事务边界规范"
level: "WARNING"
description: "跨表操作必须使用事务,且事务边界要合理"
applies_to:
annotations: ["Service"]
check_points:
- "涉及多个表写操作的方法必须有@Transactional注解"
- "事务方法不能有过重的外部调用(HTTP/MQ发送等)"
- "@Transactional方法不应该被同类中的非事务方法调用(会导致事务失效)"AI检查引擎
@Service
@Slf4j
public class CodingStandardsChecker {
private final CodingStandardsLoader standardsLoader;
private final AnthropicClient anthropicClient;
private final JavaCodeAnalyzer codeAnalyzer;
public List<StandardsViolation> checkFile(Path javaFile) throws IOException {
String sourceCode = Files.readString(javaFile);
CodeFileInfo fileInfo = codeAnalyzer.analyze(javaFile);
// 根据文件特征,找出适用的规范
List<CodingStandard> applicableStandards = findApplicableStandards(fileInfo);
if (applicableStandards.isEmpty()) {
return Collections.emptyList();
}
log.debug("对文件 {} 应用 {} 条规范", javaFile.getFileName(),
applicableStandards.size());
// 按批次检查,避免单次Prompt太长
List<List<CodingStandard>> batches = Lists.partition(applicableStandards, 3);
List<StandardsViolation> allViolations = new ArrayList<>();
for (List<CodingStandard> batch : batches) {
List<StandardsViolation> violations = checkBatch(sourceCode, fileInfo, batch);
allViolations.addAll(violations);
}
return allViolations;
}
private List<CodingStandard> findApplicableStandards(CodeFileInfo fileInfo) {
List<CodingStandard> allStandards = standardsLoader.loadAll();
return allStandards.stream().filter(standard -> {
StandardApplicability applicability = standard.getAppliesTo();
// 按注解过滤
if (applicability.getAnnotations() != null &&
!applicability.getAnnotations().isEmpty()) {
boolean hasRequiredAnnotation = applicability.getAnnotations().stream()
.anyMatch(ann -> fileInfo.getClassAnnotations().contains(ann));
if (!hasRequiredAnnotation) return false;
}
// 按使用的类过滤
if (applicability.getUsedClasses() != null &&
!applicability.getUsedClasses().isEmpty()) {
boolean usesRequiredClass = applicability.getUsedClasses().stream()
.anyMatch(cls -> fileInfo.getImportedClasses().contains(cls) ||
fileInfo.getUsedClasses().contains(cls));
if (!usesRequiredClass) return false;
}
return true;
}).collect(Collectors.toList());
}
private List<StandardsViolation> checkBatch(String sourceCode,
CodeFileInfo fileInfo, List<CodingStandard> standards) {
String prompt = buildCheckPrompt(sourceCode, fileInfo, standards);
String response = anthropicClient.complete(prompt);
return parseViolations(response, fileInfo.getFilePath());
}
private String buildCheckPrompt(String sourceCode, CodeFileInfo fileInfo,
List<CodingStandard> standards) {
StringBuilder standardsDesc = new StringBuilder();
for (CodingStandard standard : standards) {
standardsDesc.append(String.format("""
规范ID: %s
名称: %s
严重级别: %s
说明: %s
检查要点:
%s
""",
standard.getId(),
standard.getName(),
standard.getLevel(),
standard.getDescription(),
standard.getCheckPoints().stream()
.map(p -> "- " + p)
.collect(Collectors.joining("\n"))
));
if (standard.getGoodExample() != null) {
standardsDesc.append("正确示例:\n```java\n")
.append(standard.getGoodExample())
.append("\n```\n\n");
}
}
return String.format("""
你是一个严格的代码规范检查工具。请检查以下Java代码是否符合规范要求。
文件信息:
- 文件路径: %s
- 类名: %s
- 类注解: %s
需要检查的规范:
%s
待检查代码:
```java
%s
```
检查规则:
1. 只检查新添加的代码问题,不要纠结历史遗留问题
2. 给出具体的行号引用
3. 如果代码符合规范,不要硬找问题
4. 每个发现必须对应具体的规范ID
输出格式(JSON数组,如无违规则输出空数组[]):
[
{
"standard_id": "规范ID",
"severity": "ERROR或WARNING",
"message": "具体违规说明",
"line_range": "行号范围(如:45-52)",
"suggestion": "修改建议",
"code_snippet": "违规代码片段"
}
]
只输出JSON,不要其他解释。
""",
fileInfo.getFilePath(),
fileInfo.getClassName(),
fileInfo.getClassAnnotations().toString(),
standardsDesc,
truncateCode(sourceCode, 4000)
);
}
private List<StandardsViolation> parseViolations(String response, String filePath) {
try {
// 清理可能的代码块标记
String jsonStr = response.trim();
if (jsonStr.startsWith("```")) {
jsonStr = jsonStr.replaceAll("```json?\\n?", "").replace("```", "");
}
List<Map<String, Object>> rawViolations = new ObjectMapper()
.readValue(jsonStr, new TypeReference<>() {});
return rawViolations.stream().map(v -> StandardsViolation.builder()
.standardId((String) v.get("standard_id"))
.severity(Severity.valueOf((String) v.get("severity")))
.message((String) v.get("message"))
.lineRange((String) v.get("line_range"))
.suggestion((String) v.get("suggestion"))
.codeSnippet((String) v.get("code_snippet"))
.filePath(filePath)
.build()
).collect(Collectors.toList());
} catch (Exception e) {
log.warn("解析违规结果失败: {}", e.getMessage());
return Collections.emptyList();
}
}
private String truncateCode(String code, int maxLength) {
if (code.length() <= maxLength) return code;
return code.substring(0, maxLength) + "\n// ... 代码截断 ...";
}
}与构建流程集成
Maven插件形式集成
@Mojo(name = "check-standards", defaultPhase = LifecyclePhase.VERIFY)
@Slf4j
public class CodingStandardsMojo extends AbstractMojo {
@Parameter(defaultValue = "${project.build.sourceDirectory}")
private File sourceDirectory;
@Parameter(property = "standards.config",
defaultValue = "${project.basedir}/coding-standards.yml")
private File standardsConfig;
@Parameter(property = "standards.failOnError", defaultValue = "true")
private boolean failOnError;
@Parameter(property = "anthropic.api.key")
private String anthropicApiKey;
@Override
public void execute() throws MojoExecutionException {
if (anthropicApiKey == null || anthropicApiKey.isEmpty()) {
getLog().warn("未配置Anthropic API Key,跳过AI规范检查");
return;
}
CodingStandardsChecker checker = new CodingStandardsChecker(
standardsConfig, anthropicApiKey);
List<StandardsViolation> allViolations = new ArrayList<>();
try (Stream<Path> javaFiles = Files.walk(sourceDirectory.toPath())) {
javaFiles.filter(p -> p.toString().endsWith(".java"))
.filter(p -> !p.toString().contains("/test/"))
.forEach(file -> {
try {
List<StandardsViolation> violations = checker.checkFile(file);
allViolations.addAll(violations);
if (!violations.isEmpty()) {
violations.forEach(v ->
getLog().warn(String.format("[%s] %s:%s - %s",
v.getStandardId(),
file.getFileName(),
v.getLineRange(),
v.getMessage()))
);
}
} catch (IOException e) {
getLog().warn("检查文件失败: " + file, e);
}
});
} catch (IOException e) {
throw new MojoExecutionException("无法遍历源码目录", e);
}
// 生成报告
generateReport(allViolations);
// ERROR级别的违规会导致构建失败
long errorCount = allViolations.stream()
.filter(v -> v.getSeverity() == Severity.ERROR)
.count();
if (failOnError && errorCount > 0) {
throw new MojoExecutionException(
String.format("发现 %d 个ERROR级别的规范违规,构建失败!查看报告:target/standards-report.html",
errorCount));
}
if (!allViolations.isEmpty()) {
getLog().warn(String.format("发现 %d 个规范违规(%d ERROR, %d WARNING)",
allViolations.size(), errorCount, allViolations.size() - errorCount));
}
}
private void generateReport(List<StandardsViolation> violations) {
// 生成HTML报告
String reportPath = sourceDirectory.getParent() + "/target/standards-report.html";
// ... 生成HTML报告的逻辑
}
}智能豁免机制
有些情况下,代码看起来违规但实际上是有意为之的。需要一个豁免机制:
// 在代码里用注释标记豁免
public class LegacyOrderService {
// @standards-ignore ARCH-001 历史遗留依赖,计划在Q3迁移重构
@Autowired
private UserServiceImpl userServiceImpl; // 直接依赖实现类
public void processOrder(Long orderId) {
// @standards-ignore LOG-001 这个方法的日志由AOP统一处理
Order order = orderRepository.findById(orderId).orElseThrow();
// ...
}
}@Service
public class ExemptionProcessor {
private static final Pattern EXEMPTION_PATTERN =
Pattern.compile("@standards-ignore\\s+(\\S+)\\s+(.+)");
public Map<String, String> extractExemptions(String sourceCode) {
Map<String, String> exemptions = new HashMap<>();
String[] lines = sourceCode.split("\n");
for (String line : lines) {
Matcher matcher = EXEMPTION_PATTERN.matcher(line);
if (matcher.find()) {
String standardId = matcher.group(1);
String reason = matcher.group(2);
exemptions.put(standardId, reason);
}
}
return exemptions;
}
public List<StandardsViolation> filterExempted(
List<StandardsViolation> violations,
Map<String, String> exemptions) {
return violations.stream()
.filter(v -> !exemptions.containsKey(v.getStandardId()))
.collect(Collectors.toList());
}
}一个真实的踩坑经历
系统上线的第一周,团队里有个同学投诉了一件事:他的代码被标记了5个违规,但他觉得其中3个是误报。
我去看了一下,确实有误报。原因是AI把项目里自定义的@LogOperation注解(用于AOP统一打日志)识别成了"没有日志"——它只认识log.info()这种直接调用,不知道我们用了AOP方案。
这个问题让我意识到:规范检查的prompt里必须包含项目特有的约定,不能只描述通用规则。
解决方法是在coding-standards.yml里加了一个project_context字段:
project_context: |
本项目使用自定义的 @LogOperation 注解进行AOP日志记录。
使用了此注解的方法,视为已满足日志规范,不需要额外的log.info()调用。
本项目的外部服务调用统一通过 BaseRemoteClient 基类处理,
BaseRemoteClient 内部已配置了超时和重试,不需要在业务代码里再设置。加上这段上下文之后,误报率从约30%降到了8%以内。
效果数据
部署三个月的效果统计:
- 共检查PR:847个
- 发现规范违规:1,203处
- 开发者接受并修改的比例:78%
- 因规范检查而提前发现的潜在问题(超时缺失、事务边界错误等):156处
最让我满意的是最后那个数字——156处潜在问题。这些问题如果到了生产环境才暴露,修复成本会高得多。
