第1794篇:API设计的AI审查——自动检测不一致性和反模式
第1794篇:API设计的AI审查——自动检测不一致性和反模式
API设计这件事,说简单也简单,说复杂也真的很复杂。
简单的地方在于,基本原则就那么几条:资源名用复数、HTTP方法语义要对、错误码要有含义、版本要管理。
复杂的地方在于:一个团队十几个人各自设计API,三年下来积累了几百个接口,里面的风格不统一、命名冲突、行为不一致,已经变成了一个让新人头疼、老人也说不清楚的混乱系统。
我见过一个项目,查询用户信息是 GET /user/info/{id},查询订单列表是 POST /order/list,删除商品用的是 GET /product/delete?id=123。同一个系统里,三个接口三种风格,这还只是冰山一角。
这种问题靠Code Review发现效率很低——Code Review通常聚焦在功能逻辑上,API风格问题很容易被忽略。
API设计的常见问题分类
在讲怎么用AI检测之前,先系统性地梳理API设计的常见问题:
命名不一致类:
- 同一概念多种叫法(userId/user_id/uid 混用)
- 动词乱入资源路径(/getUserInfo vs /getUser/info)
- 单复数不统一(/user vs /users)
- 大小写风格混乱(camelCase、snake_case、kebab-case 混用)
HTTP语义滥用类:
- 用GET做写操作(数据修改、删除)
- 用POST做所有操作(懒人接口)
- 响应码不准确(所有错误返回200,body里带错误信息)
- 幂等性设计错误(PUT不幂等、DELETE重复调用报错)
接口行为不一致类:
- 分页参数风格不统一(page/pageNum/pageIndex/offset 混用)
- 时间字段格式不统一(时间戳、ISO8601、自定义格式)
- 空值处理不一致(有的接口null不返回,有的返回null)
- 错误码含义不统一(code:1 在A接口是成功,在B接口是失败)
安全和规范类:
- 未认证接口(应该鉴权的没有保护)
- 批量接口无数量限制(可能被滥用)
- 敏感信息暴露(手机号、邮箱全量返回)
构建API审查工具
核心思路是:先从代码里提取API定义,然后用AI分析不一致性和反模式。
第一步:从Spring Boot代码提取API规格
@Service
public class ApiSpecExtractor {
public List<ApiEndpoint> extractFromSpringBootProject(Path projectPath) {
List<ApiEndpoint> endpoints = new ArrayList<>();
// 遍历所有Controller类
List<Path> controllerFiles = findControllerFiles(projectPath);
for (Path controllerFile : controllerFiles) {
try {
List<ApiEndpoint> controllerEndpoints = extractFromController(controllerFile);
endpoints.addAll(controllerEndpoints);
} catch (Exception e) {
log.warn("提取Controller API失败: {}", controllerFile, e);
}
}
return endpoints;
}
private List<ApiEndpoint> extractFromController(Path controllerFile) throws IOException {
String sourceCode = Files.readString(controllerFile);
CompilationUnit cu = StaticJavaParser.parse(sourceCode);
List<ApiEndpoint> endpoints = new ArrayList<>();
// 提取Controller级别的基础路径
String basePath = extractControllerBasePath(cu);
for (MethodDeclaration method : cu.findAll(MethodDeclaration.class)) {
ApiEndpoint endpoint = extractEndpointFromMethod(method, basePath, controllerFile);
if (endpoint != null) {
endpoints.add(endpoint);
}
}
return endpoints;
}
private ApiEndpoint extractEndpointFromMethod(
MethodDeclaration method, String basePath, Path sourceFile) {
// 检查是否有HTTP方法注解
for (AnnotationExpr annotation : method.getAnnotations()) {
String annotationName = annotation.getNameAsString();
HttpMethod httpMethod = mapAnnotationToHttpMethod(annotationName);
if (httpMethod == null) continue;
ApiEndpoint endpoint = new ApiEndpoint();
endpoint.setHttpMethod(httpMethod);
endpoint.setPath(extractPath(annotation, basePath));
endpoint.setMethodName(method.getNameAsString());
endpoint.setSourceFile(sourceFile.toString());
endpoint.setReturnType(method.getType().asString());
// 提取参数信息
List<ApiParameter> params = extractParameters(method);
endpoint.setParameters(params);
// 提取方法注释(用于理解用途)
method.getComment().ifPresent(c -> endpoint.setComment(c.getContent()));
// 提取ResponseBody注解
boolean hasResponseBody = method.getAnnotations().stream()
.anyMatch(a -> a.getNameAsString().equals("ResponseBody")
|| a.getNameAsString().equals("RestController"));
endpoint.setReturnsJson(hasResponseBody);
return endpoint;
}
return null;
}
private String extractControllerBasePath(CompilationUnit cu) {
return cu.findFirst(ClassOrInterfaceDeclaration.class)
.flatMap(c -> c.getAnnotationByName("RequestMapping"))
.map(a -> {
if (a instanceof NormalAnnotationExpr) {
return ((NormalAnnotationExpr) a).getPairs().stream()
.filter(p -> p.getNameAsString().equals("value") || p.getNameAsString().equals("path"))
.findFirst()
.map(p -> p.getValue().toString().replace("\"", ""))
.orElse("");
} else if (a instanceof SingleMemberAnnotationExpr) {
return ((SingleMemberAnnotationExpr) a).getMemberValue().toString().replace("\"", "");
}
return "";
})
.orElse("");
}
private HttpMethod mapAnnotationToHttpMethod(String annotationName) {
return switch (annotationName) {
case "GetMapping" -> HttpMethod.GET;
case "PostMapping" -> HttpMethod.POST;
case "PutMapping" -> HttpMethod.PUT;
case "DeleteMapping" -> HttpMethod.DELETE;
case "PatchMapping" -> HttpMethod.PATCH;
default -> null;
};
}
private List<Path> findControllerFiles(Path projectPath) {
try {
return Files.walk(projectPath)
.filter(p -> p.toString().endsWith(".java"))
.filter(p -> {
try {
String content = Files.readString(p);
return content.contains("@RestController") || content.contains("@Controller");
} catch (IOException e) {
return false;
}
})
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException("遍历项目文件失败", e);
}
}
}第二步:AI驱动的一致性检查
@Service
public class ApiConsistencyChecker {
private final ClaudeApiClient claudeClient;
public ConsistencyReport checkConsistency(List<ApiEndpoint> endpoints) {
// 把所有API规格转换成简洁的文本格式
String apiInventory = formatApiInventory(endpoints);
String prompt = String.format("""
你是一位API设计专家。请审查以下API接口列表,找出不一致性和反模式。
审查维度:
## 1. 命名一致性
- URL路径风格是否统一(全部使用kebab-case或全部camelCase)
- 相同概念是否使用相同的术语(比如userId vs uid vs user_id)
- 资源名是否统一使用复数形式
- 路径中是否混入了动词(应该用名词表示资源)
## 2. HTTP方法语义
- GET请求是否只做查询,不修改状态
- 创建操作是否用POST
- 全量更新是否用PUT,部分更新是否用PATCH
- 删除是否用DELETE
## 3. 参数风格一致性
- 分页参数命名是否统一
- ID参数是否统一(路径参数 vs 查询参数)
- 时间范围查询参数命名是否统一
## 4. 路径结构
- 嵌套层级是否合理(不超过3层)
- 是否有功能重叠的接口
- 版本控制方式是否统一
## 5. 安全风险
- 批量操作接口是否有数量限制参数
- 是否有可能暴露内部ID或枚举资源的接口
请按以下JSON格式输出:
{
"consistency_issues": [
{
"type": "问题类型",
"severity": "HIGH/MEDIUM/LOW",
"description": "问题描述",
"affected_endpoints": ["受影响的接口路径"],
"recommendation": "建议修改方式",
"example_fix": "具体示例"
}
],
"anti_patterns": [
{
"pattern_name": "反模式名称",
"description": "描述",
"affected_endpoints": ["受影响的接口路径"],
"recommendation": "改进建议"
}
],
"positive_patterns": ["做得好的地方"],
"overall_consistency_score": 0-100,
"summary": "总结"
}
API接口列表:
%s
""", apiInventory);
String response = claudeClient.complete(prompt);
return parseConsistencyReport(response, endpoints);
}
private String formatApiInventory(List<ApiEndpoint> endpoints) {
StringBuilder sb = new StringBuilder();
// 按Controller分组
Map<String, List<ApiEndpoint>> byController = endpoints.stream()
.collect(Collectors.groupingBy(e ->
extractControllerName(e.getSourceFile())));
for (Map.Entry<String, List<ApiEndpoint>> entry : byController.entrySet()) {
sb.append("### ").append(entry.getKey()).append("\n");
for (ApiEndpoint endpoint : entry.getValue()) {
sb.append(String.format("%-8s %s\n",
endpoint.getHttpMethod(), endpoint.getPath()));
if (!endpoint.getParameters().isEmpty()) {
sb.append(" 参数: ");
sb.append(endpoint.getParameters().stream()
.map(p -> p.getName() + "(" + p.getIn() + ")")
.collect(Collectors.joining(", ")));
sb.append("\n");
}
if (endpoint.getComment() != null) {
sb.append(" 说明: ").append(endpoint.getComment().trim()).append("\n");
}
sb.append("\n");
}
}
return sb.toString();
}
}第三步:深度反模式检测
除了整体一致性,还需要检测具体的API反模式:
@Service
public class ApiAntiPatternDetector {
private final ClaudeApiClient claudeClient;
public List<AntiPatternFinding> detectAntiPatterns(List<ApiEndpoint> endpoints) {
List<AntiPatternFinding> findings = new ArrayList<>();
// 规则检测(不需要AI)
findings.addAll(detectByRules(endpoints));
// AI语义检测(需要理解接口含义)
findings.addAll(detectByAI(endpoints));
return findings;
}
private List<AntiPatternFinding> detectByRules(List<ApiEndpoint> endpoints) {
List<AntiPatternFinding> findings = new ArrayList<>();
for (ApiEndpoint endpoint : endpoints) {
// 检测:GET请求包含写操作动词
if (endpoint.getHttpMethod() == HttpMethod.GET) {
String path = endpoint.getPath().toLowerCase();
List<String> writeVerbs = Arrays.asList("create", "update", "delete", "remove", "add", "save");
for (String verb : writeVerbs) {
if (path.contains(verb)) {
findings.add(AntiPatternFinding.builder()
.endpoint(endpoint)
.pattern("GET_WITH_WRITE_VERB")
.severity(Severity.HIGH)
.description(String.format("GET请求路径包含写操作动词'%s',违反HTTP语义", verb))
.recommendation("将写操作改为POST/PUT/DELETE方法")
.build());
}
}
}
// 检测:路径中包含动词(非CRUD动词的业务动词)
String[] pathSegments = endpoint.getPath().split("/");
for (String segment : pathSegments) {
if (isBusinessVerb(segment) && !isResourceNoun(segment)) {
findings.add(AntiPatternFinding.builder()
.endpoint(endpoint)
.pattern("VERB_IN_RESOURCE_PATH")
.severity(Severity.MEDIUM)
.description(String.format("路径段'%s'看起来是动词,REST API路径应使用名词", segment))
.recommendation("考虑将动词转化为名词资源(如/activate → POST /activations)")
.build());
break;
}
}
// 检测:分页参数名称问题
if (hasPaginationParams(endpoint)) {
checkPaginationConsistency(endpoint, findings);
}
}
// 检测跨接口的一致性问题
checkCrossEndpointConsistency(endpoints, findings);
return findings;
}
private void checkCrossEndpointConsistency(List<ApiEndpoint> endpoints,
List<AntiPatternFinding> findings) {
// 检测ID参数命名不一致
Set<String> idParamNames = new HashSet<>();
for (ApiEndpoint endpoint : endpoints) {
for (ApiParameter param : endpoint.getParameters()) {
if (param.getName().toLowerCase().contains("id")) {
idParamNames.add(param.getName());
}
}
}
// 如果有多种ID命名风格
Set<String> idNamingStyles = idParamNames.stream()
.map(name -> detectNamingStyle(name))
.collect(Collectors.toSet());
if (idNamingStyles.size() > 1) {
findings.add(AntiPatternFinding.builder()
.pattern("INCONSISTENT_ID_NAMING")
.severity(Severity.MEDIUM)
.description("ID参数命名风格不一致: " + String.join(", ", idParamNames))
.recommendation("统一使用一种命名风格,推荐camelCase")
.build());
}
// 检测相似功能但HTTP方法不同的接口
Map<String, List<ApiEndpoint>> similarPaths = endpoints.stream()
.collect(Collectors.groupingBy(e -> normalizePathForComparison(e.getPath())));
for (Map.Entry<String, List<ApiEndpoint>> entry : similarPaths.entrySet()) {
if (entry.getValue().size() > 1) {
Set<HttpMethod> methods = entry.getValue().stream()
.map(ApiEndpoint::getHttpMethod)
.collect(Collectors.toSet());
if (methods.contains(HttpMethod.GET) && methods.contains(HttpMethod.POST)) {
// 可能是故意的(CQRS风格),但也可能是设计问题,交给AI判断
}
}
}
}
private List<AntiPatternFinding> detectByAI(List<ApiEndpoint> endpoints) {
// 只对有歧义的接口做AI分析
List<ApiEndpoint> ambiguousEndpoints = endpoints.stream()
.filter(e -> isAmbiguous(e))
.collect(Collectors.toList());
if (ambiguousEndpoints.isEmpty()) {
return Collections.emptyList();
}
String prompt = buildAIDetectionPrompt(ambiguousEndpoints);
String response = claudeClient.complete(prompt);
return parseAIDetectionResult(response, ambiguousEndpoints);
}
private boolean isAmbiguous(ApiEndpoint endpoint) {
// 接口命名模糊,需要AI帮助理解含义
String path = endpoint.getPath().toLowerCase();
return path.contains("handle") || path.contains("process") ||
path.contains("do") || path.contains("execute") ||
endpoint.getMethodName().startsWith("handle") ||
endpoint.getMethodName().startsWith("process");
}
private boolean isBusinessVerb(String segment) {
// 判断路径段是否是业务动词(简化版)
Set<String> commonVerbs = Set.of("get", "create", "update", "delete", "list",
"search", "find", "query", "fetch", "save", "remove", "add", "enable",
"disable", "activate", "deactivate", "cancel", "confirm", "approve",
"reject", "submit", "publish", "unpublish", "lock", "unlock");
return commonVerbs.contains(segment.toLowerCase());
}
private boolean isResourceNoun(String segment) {
// 判断是否也可以理解为资源名词(简化版)
// 比如 "orders", "users" 是名词,"create" 不是
return segment.endsWith("s") || segment.endsWith("ion") || segment.endsWith("ment");
}
}生成修复建议报告
发现问题之后,还要给出可落地的修复建议:
@Service
public class ApiFixSuggestionGenerator {
private final ClaudeApiClient claudeClient;
public String generateFixReport(ConsistencyReport report, List<ApiEndpoint> currentApis) {
String prompt = String.format("""
基于以下API一致性审查结果,生成一份详细的修复建议报告。
当前存在的问题:
%s
当前API列表(用于参考):
%s
请生成:
## 优先修复项(Breaking Changes,需要版本升级)
列出需要修改接口签名、URL、参数名的改动
对每个改动提供:
- 当前接口定义
- 建议的新接口定义
- 迁移方式(是否需要保留旧接口一段时间)
## 可向前兼容的修复
列出不影响现有调用方的改动
## 长期规范建议
建议团队制定的API设计规范条目
## 修复优先级矩阵
基于影响面和修复成本,给出建议的修复顺序
""",
toJson(report),
formatApiInventory(currentApis));
return claudeClient.complete(prompt);
}
}实战输出样例
针对一个真实项目的API列表,AI审查报告的关键发现:
发现的高危问题(节选):
[HIGH] GET_WITH_WRITE_SEMANTICS
- GET /api/user/deleteById?id=123
问题:GET请求执行删除操作,违反HTTP幂等性规范
影响:可能被浏览器预取缓存触发误删除;不符合RESTful规范
修复建议:改为 DELETE /api/users/{id}
[HIGH] INCONSISTENT_ERROR_FORMAT
受影响接口:23个接口
问题:错误响应格式不一致
- 部分接口:{"success": false, "message": "xxx"}
- 部分接口:{"code": 500, "error": "xxx"}
- 部分接口:{"status": "FAILED", "reason": "xxx"}
影响:前端需要处理多种错误格式,维护成本高
修复建议:统一采用 {"code": 数字错误码, "message": "错误描述", "data": null}
[MEDIUM] PAGINATION_INCONSISTENCY
受影响接口:15个分页接口
发现5种不同的分页参数风格:
- page + size
- pageNum + pageSize
- pageIndex + limit
- offset + count
- start + rows
修复建议:统一采用 page(从1开始)+ size,最大size限制为100这种报告直接给团队负责人看,反应往往是「我们一直知道有问题,但第一次看到这么清晰的全景图」。
CI集成:让API审查自动化
把这个工具接入代码PR流程:
# .github/workflows/api-review.yml
name: API Consistency Review
on:
pull_request:
paths:
- 'src/main/java/**/*Controller.java'
- 'src/main/java/**/*Resource.java'
jobs:
api-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract changed controllers
id: changed-files
run: |
echo "CHANGED_CONTROLLERS=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD | grep -E '(Controller|Resource)\.java$' | tr '\n' ',')" >> $GITHUB_ENV
- name: Run API consistency check
run: |
./mvnw api-reviewer:check \
-Dfiles="${CHANGED_CONTROLLERS}" \
-Danthropic.api.key=${{ secrets.ANTHROPIC_API_KEY }} \
-DfailOnHighSeverity=true
- name: Comment PR with findings
if: always()
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('target/api-review-report.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: report
});这样每次有人改了Controller,CI自动跑API审查,把结果评论到PR上,Code Review的时候就有参考依据了。
踩过的坑
最后说几个实践中遇到的坑:
坑一:AI对领域特定接口的误判
我们有一个接口叫 POST /api/batch-process,AI认为这违反RESTful,应该用名词。但这个接口本来就是一个异步批处理触发接口,不对应任何单一资源,用动词其实是合理的。
解法:允许在接口上加 @ApiDesignException("reason") 自定义注解,审查时跳过这些已知的例外情况。
坑二:历史债务太多导致报告没有优先级
第一次跑,发现了200多个问题,团队看到直接不知道从哪里下手。
解法:按「新代码」和「历史代码」分开报告,新代码(最近3个月改动)的问题必须修复,历史问题列入backlog逐步处理。
坑三:团队标准和AI建议冲突
AI推荐标准RESTful风格,但我们团队约定的路径分层是 /api/v1/模块名/资源名,AI有时候会建议去掉模块名。
解法:在Prompt里加入团队API规范文档,让AI基于团队规范而不是通用规范做审查。
API一致性问题是慢慢积累、快速腐烂的。用AI做自动审查,关键是要持续运行,让每次提交都能得到反馈,而不是等到技术债务积重难返才去做大规模重构。
