package com.aicheck.parser; import com.aicheck.model.ApiEndpoint; import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.type.Type; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Stream; /** * 扫描 Controller / Feign 接口,提取 HTTP 方法、URI、入参/返回类型。 */ public class EndpointParser { private static final Set MAPPING_ANNOTATIONS = Set.of( "GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping" ); private static final Map MAPPING_DEFAULT_METHOD = Map.of( "GetMapping", "GET", "PostMapping", "POST", "PutMapping", "PUT", "DeleteMapping", "DELETE", "PatchMapping", "PATCH" ); /** 扫描 @RestController / @Controller 目录 */ public List scanControllerDirectory(Path rootDir, String relativePrefix) throws IOException { return scanDirectory(rootDir, relativePrefix, ScanMode.CONTROLLER); } /** 扫描 @FeignClient 接口目录 */ public List scanFeignDirectory(Path rootDir, String relativePrefix) throws IOException { return scanDirectory(rootDir, relativePrefix, ScanMode.FEIGN); } /** 递归 walk 目录下 .java 并解析 */ private List scanDirectory(Path rootDir, String relativePrefix, ScanMode mode) throws IOException { if (!Files.exists(rootDir)) { return List.of(); } List endpoints = new ArrayList<>(); try (Stream paths = Files.walk(rootDir)) { paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> { try { String source = Files.readString(path, StandardCharsets.UTF_8); String relativePath = toRelativePath(relativePrefix, rootDir, path); endpoints.addAll(parseCompilationUnit(source, relativePath, mode)); } catch (IOException ignored) { // 跳过无法读取的文件 } }); } return endpoints; } /** 解析单个编译单元,过滤 Controller 或 Feign */ private List parseCompilationUnit(String source, String relativePath, ScanMode mode) { CompilationUnit cu = StaticJavaParser.parse(source); List endpoints = new ArrayList<>(); for (TypeDeclaration type : cu.getTypes()) { if (!(type instanceof ClassOrInterfaceDeclaration)) { continue; } ClassOrInterfaceDeclaration declaration = (ClassOrInterfaceDeclaration) type; if (mode == ScanMode.CONTROLLER && !isController(declaration)) { continue; } if (mode == ScanMode.FEIGN && !isFeignClient(declaration)) { continue; } String basePath = mode == ScanMode.FEIGN ? joinPaths(extractFeignBasePath(declaration), extractTypeLevelPath(declaration)) : extractTypeLevelPath(declaration); for (MethodDeclaration method : declaration.getMethods()) { if (mode == ScanMode.FEIGN && declaration.isInterface()) { endpoints.addAll(parseMethod(method, basePath, relativePath)); } else if (mode == ScanMode.CONTROLLER && !declaration.isInterface()) { endpoints.addAll(parseMethod(method, basePath, relativePath)); } } } return endpoints; } /** 解析方法上的 Mapping 注解,生成 ApiEndpoint */ private List parseMethod(MethodDeclaration method, String basePath, String sourceFile) { List endpoints = new ArrayList<>(); for (AnnotationExpr annotation : method.getAnnotations()) { String annName = annotation.getNameAsString(); if (!MAPPING_ANNOTATIONS.contains(annName)) { continue; } List subPaths = extractPaths(annotation); List httpMethods = extractHttpMethods(annotation, annName); for (String httpMethod : httpMethods) { for (String subPath : subPaths) { String uri = joinPaths(basePath, subPath); Set paramTypes = extractParamTypes(method); Set returnTypes = TypeNameUtils.peelDirectTypeNames(method.getType()); endpoints.add(new ApiEndpoint(httpMethod, uri, sourceFile, paramTypes, returnTypes)); } } } return endpoints; } /** 收集方法入参类型简单名 */ private Set extractParamTypes(MethodDeclaration method) { Set paramTypes = new LinkedHashSet<>(); for (Parameter parameter : method.getParameters()) { Type type = parameter.getType(); paramTypes.add(TypeNameUtils.simpleName(TypeNameUtils.typeToString(type))); paramTypes.addAll(TypeNameUtils.peelDirectTypeNames(type)); } return paramTypes; } /** 是否 Spring Controller */ private boolean isController(ClassOrInterfaceDeclaration declaration) { return declaration.getAnnotations().stream() .anyMatch(ann -> { String name = ann.getNameAsString(); return "RestController".equals(name) || "Controller".equals(name); }); } /** 是否 Feign 客户端接口 */ private boolean isFeignClient(ClassOrInterfaceDeclaration declaration) { return declaration.isInterface() && declaration.getAnnotations().stream() .anyMatch(ann -> "FeignClient".equals(ann.getNameAsString())); } /** 类级 @RequestMapping 路径 */ private String extractTypeLevelPath(ClassOrInterfaceDeclaration declaration) { for (AnnotationExpr annotation : declaration.getAnnotations()) { if ("RequestMapping".equals(annotation.getNameAsString())) { List paths = extractPaths(annotation); if (!paths.isEmpty()) { return paths.get(0); } } } return ""; } /** @FeignClient(path=...) 基础路径 */ private String extractFeignBasePath(ClassOrInterfaceDeclaration declaration) { for (AnnotationExpr annotation : declaration.getAnnotations()) { if ("FeignClient".equals(annotation.getNameAsString())) { List paths = AnnotationValueReader.readStringArray(annotation, "path"); if (!paths.isEmpty()) { return paths.get(0); } } } return ""; } /** 从 Mapping 注解读取 value/path */ private List extractPaths(AnnotationExpr annotation) { return AnnotationValueReader.readStringArray(annotation, "value", "path"); } /** 推断 HTTP 方法;RequestMapping 无 method 时默认 GET */ private List extractHttpMethods(AnnotationExpr annotation, String annName) { if (!"RequestMapping".equals(annName)) { return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET")); } List methods = AnnotationValueReader.readEnumArray(annotation, "method"); if (methods.isEmpty()) { return List.of("GET"); } return methods; } /** 拼接类级与方法级路径 */ private String joinPaths(String base, String sub) { String normalizedBase = normalizePath(base); String normalizedSub = normalizePath(sub); if (normalizedBase.isEmpty()) { return normalizedSub.isEmpty() ? "/" : normalizedSub; } if (normalizedSub.isEmpty()) { return normalizedBase; } String joined = normalizedBase + "/" + normalizedSub.substring(1); return joined.replaceAll("/+", "/"); } /** 规范化 URI 路径 */ private String normalizePath(String path) { if (path == null || path.isBlank()) { return ""; } String trimmed = path.trim(); if (!trimmed.startsWith("/")) { trimmed = "/" + trimmed; } return trimmed.replaceAll("/+", "/"); } /** 生成相对仓库根的路径 */ private String toRelativePath(String relativePrefix, Path rootDir, Path file) { String relative = rootDir.relativize(file).toString().replace("\\", "/"); if (relativePrefix == null || relativePrefix.isBlank()) { return relative; } String prefix = relativePrefix.endsWith("/") ? relativePrefix.substring(0, relativePrefix.length() - 1) : relativePrefix; return prefix + "/" + relative; } private enum ScanMode { CONTROLLER, FEIGN } /** 从注解 AST 读取字符串或枚举数组 */ static final class AnnotationValueReader { private AnnotationValueReader() { } static List readStringArray(AnnotationExpr annotation, String... keys) { NodeList values = readArrayValues(annotation, keys); List result = new ArrayList<>(); for (Object value : values) { String text = value.toString().replace("\"", "").trim(); if (!text.isBlank()) { result.add(text); } } if (result.isEmpty()) { result.add(""); } return result; } static List readEnumArray(AnnotationExpr annotation, String key) { NodeList values = readArrayValues(annotation, key); List result = new ArrayList<>(); for (Object value : values) { String text = value.toString().trim(); if (text.contains(".")) { text = text.substring(text.lastIndexOf('.') + 1); } result.add(text.toUpperCase(Locale.ROOT)); } return result; } private static NodeList readArrayValues(AnnotationExpr annotation, String... keys) { if (annotation.isSingleMemberAnnotationExpr()) { Expression value = annotation.asSingleMemberAnnotationExpr().getMemberValue(); if (value.isArrayInitializerExpr()) { return value.asArrayInitializerExpr().getValues(); } return new NodeList<>(value); } if (annotation.isNormalAnnotationExpr()) { var pairs = annotation.asNormalAnnotationExpr().getPairs(); for (var pair : pairs) { for (String key : keys) { if (pair.getNameAsString().equals(key)) { if (pair.getValue().isArrayInitializerExpr()) { return pair.getValue().asArrayInitializerExpr().getValues(); } return new NodeList<>(pair.getValue()); } } } for (var pair : pairs) { if ("value".equals(pair.getNameAsString())) { if (pair.getValue().isArrayInitializerExpr()) { return pair.getValue().asArrayInitializerExpr().getValues(); } return new NodeList<>(pair.getValue()); } } } return new NodeList<>(); } } }