This commit is contained in:
62
.gitea/java-parser/pom.xml
Normal file
62
.gitea/java-parser/pom.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.aicheck</groupId>
|
||||
<artifactId>controller-parser</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>Controller Parameter AST Parser</name>
|
||||
<description>基于 JavaParser 解析 Spring Controller 接口参数</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javaparser.version>3.25.10</javaparser.version>
|
||||
<jackson.version>2.17.2</jackson.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Java AST 解析库 -->
|
||||
<dependency>
|
||||
<groupId>com.github.javaparser</groupId>
|
||||
<artifactId>javaparser-core</artifactId>
|
||||
<version>${javaparser.version}</version>
|
||||
</dependency>
|
||||
<!-- JSON 输出,供 Python 主程序读取 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.aicheck.ControllerParserMain</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.aicheck;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单个 Controller 接口端点的模型,包含 URI、HTTP 方法及参数列表。
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiEndpoint {
|
||||
|
||||
/** HTTP 方法:GET / POST / PUT / DELETE / PATCH */
|
||||
private String httpMethod;
|
||||
|
||||
/** 完整 URI 路径,如 /api/users/{id} */
|
||||
private String uri;
|
||||
|
||||
/** 所属 Controller 类名 */
|
||||
private String controllerClass;
|
||||
|
||||
/** Java 方法名 */
|
||||
private String methodName;
|
||||
|
||||
/** 源文件相对路径 */
|
||||
private String sourceFile;
|
||||
|
||||
/** 接口参数列表 */
|
||||
private List<ApiParameter> parameters = new ArrayList<>();
|
||||
|
||||
public String getHttpMethod() {
|
||||
return httpMethod;
|
||||
}
|
||||
|
||||
public void setHttpMethod(String httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void setUri(String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public String getControllerClass() {
|
||||
return controllerClass;
|
||||
}
|
||||
|
||||
public void setControllerClass(String controllerClass) {
|
||||
this.controllerClass = controllerClass;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return methodName;
|
||||
}
|
||||
|
||||
public void setMethodName(String methodName) {
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
public String getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public void setSourceFile(String sourceFile) {
|
||||
this.sourceFile = sourceFile;
|
||||
}
|
||||
|
||||
public List<ApiParameter> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void setParameters(List<ApiParameter> parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一标识,用于跨版本比对接口是否为同一个。
|
||||
* 格式:HTTP_METHOD + 空格 + URI
|
||||
*/
|
||||
public String getEndpointKey() {
|
||||
return httpMethod + " " + uri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.aicheck;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 单个接口参数的模型,对应 Controller 方法上的一个入参。
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiParameter {
|
||||
|
||||
/** 参数名称(@RequestParam / @PathVariable 的 value,或字段名) */
|
||||
private String name;
|
||||
|
||||
/** Java 类型,如 String、Long、Boolean */
|
||||
private String type;
|
||||
|
||||
/** 是否必填(来自 required 属性或 @NotNull 等,默认 true) */
|
||||
private boolean required = true;
|
||||
|
||||
/** 参数来源:query / path / body / header / form */
|
||||
private String source;
|
||||
|
||||
/** 参数说明(来自 @ApiParam、@Parameter 等注解的 description) */
|
||||
private String description;
|
||||
|
||||
public ApiParameter() {
|
||||
}
|
||||
|
||||
public ApiParameter(String name, String type, boolean required, String source) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.required = required;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package com.aicheck;
|
||||
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.body.Parameter;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.MemberValuePair;
|
||||
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
|
||||
import com.github.javaparser.ast.type.ClassOrInterfaceType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 基于 JavaParser 的 Spring Controller AST 解析器。
|
||||
* 扫描指定目录下的 Java 文件,提取带 @RestController / @Controller 注解类中的接口定义。
|
||||
*/
|
||||
public class ControllerAstParser {
|
||||
|
||||
/** Spring 映射注解 -> HTTP 方法 */
|
||||
private static final Set<String> MAPPING_ANNOTATIONS = Set.of(
|
||||
"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"
|
||||
);
|
||||
|
||||
/** 标识 Controller 的类级别注解 */
|
||||
private static final Set<String> CONTROLLER_ANNOTATIONS = Set.of("RestController", "Controller");
|
||||
|
||||
/**
|
||||
* 解析目录下所有 Java 文件中的 Controller 接口。
|
||||
*
|
||||
* @param rootDir 项目根目录或源码目录
|
||||
* @return 解析出的所有 API 端点列表
|
||||
*/
|
||||
public List<ApiEndpoint> parseDirectory(Path rootDir) throws IOException {
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
|
||||
if (!Files.exists(rootDir)) {
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
List<Path> javaFiles = paths
|
||||
.filter(p -> p.toString().endsWith(".java"))
|
||||
.filter(p -> p.toString().contains("Controller"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Path javaFile : javaFiles) {
|
||||
endpoints.addAll(parseFile(javaFile, rootDir));
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个 Java 源文件。
|
||||
*
|
||||
* @param javaFile 源文件路径
|
||||
* @param rootDir 根目录,用于计算相对路径
|
||||
*/
|
||||
public List<ApiEndpoint> parseFile(Path javaFile, Path rootDir) throws IOException {
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
String source = Files.readString(javaFile);
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
|
||||
String relativePath = rootDir.relativize(javaFile).toString().replace("\\", "/");
|
||||
|
||||
for (ClassOrInterfaceDeclaration clazz : cu.findAll(ClassOrInterfaceDeclaration.class)) {
|
||||
if (!isController(clazz)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String classBasePath = extractClassBasePath(clazz);
|
||||
|
||||
for (MethodDeclaration method : clazz.getMethods()) {
|
||||
Optional<ApiEndpoint> endpointOpt = parseMethod(method, clazz, classBasePath, relativePath, rootDir);
|
||||
endpointOpt.ifPresent(endpoints::add);
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断类是否为 Spring Controller(含 @RestController 或 @Controller)。
|
||||
*/
|
||||
private boolean isController(ClassOrInterfaceDeclaration clazz) {
|
||||
return clazz.getAnnotations().stream()
|
||||
.anyMatch(a -> CONTROLLER_ANNOTATIONS.contains(getSimpleAnnotationName(a)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取类级别 @RequestMapping 的基础路径。
|
||||
*/
|
||||
private String extractClassBasePath(ClassOrInterfaceDeclaration clazz) {
|
||||
for (AnnotationExpr annotation : clazz.getAnnotations()) {
|
||||
if ("RequestMapping".equals(getSimpleAnnotationName(annotation))) {
|
||||
return normalizePath(extractAnnotationStringValue(annotation, "value", "path"));
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个 Controller 方法,若不含映射注解则返回 empty。
|
||||
*/
|
||||
private Optional<ApiEndpoint> parseMethod(
|
||||
MethodDeclaration method,
|
||||
ClassOrInterfaceDeclaration clazz,
|
||||
String classBasePath,
|
||||
String relativePath,
|
||||
Path rootDir) {
|
||||
|
||||
for (AnnotationExpr annotation : method.getAnnotations()) {
|
||||
String annName = getSimpleAnnotationName(annotation);
|
||||
if (!MAPPING_ANNOTATIONS.contains(annName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ApiEndpoint endpoint = new ApiEndpoint();
|
||||
endpoint.setControllerClass(clazz.getNameAsString());
|
||||
endpoint.setMethodName(method.getNameAsString());
|
||||
endpoint.setSourceFile(relativePath);
|
||||
endpoint.setHttpMethod(resolveHttpMethod(annName, annotation));
|
||||
endpoint.setUri(joinPaths(classBasePath, normalizePath(extractAnnotationStringValue(annotation, "value", "path"))));
|
||||
endpoint.setParameters(extractParameters(method, rootDir));
|
||||
|
||||
return Optional.of(endpoint);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从映射注解推断 HTTP 方法。
|
||||
*/
|
||||
private String resolveHttpMethod(String annName, AnnotationExpr annotation) {
|
||||
switch (annName) {
|
||||
case "GetMapping":
|
||||
return "GET";
|
||||
case "PostMapping":
|
||||
return "POST";
|
||||
case "PutMapping":
|
||||
return "PUT";
|
||||
case "DeleteMapping":
|
||||
return "DELETE";
|
||||
case "PatchMapping":
|
||||
return "PATCH";
|
||||
case "RequestMapping":
|
||||
String method = extractAnnotationStringValue(annotation, "method");
|
||||
if (!method.isEmpty()) {
|
||||
return method.replace("RequestMethod.", "").toUpperCase();
|
||||
}
|
||||
return "GET";
|
||||
default:
|
||||
return "GET";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取方法的所有入参(含 @RequestBody DTO 字段展开)。
|
||||
*/
|
||||
private List<ApiParameter> extractParameters(MethodDeclaration method, Path rootDir) {
|
||||
List<ApiParameter> params = new ArrayList<>();
|
||||
|
||||
for (Parameter param : method.getParameters()) {
|
||||
String paramType = param.getType().asString();
|
||||
|
||||
// @RequestBody:尝试展开 DTO 类字段
|
||||
if (hasAnnotation(param, "RequestBody")) {
|
||||
params.addAll(expandDtoFields(paramType, rootDir, "body"));
|
||||
continue;
|
||||
}
|
||||
|
||||
ApiParameter apiParam = new ApiParameter();
|
||||
apiParam.setType(paramType);
|
||||
apiParam.setSource(resolveParameterSource(param));
|
||||
apiParam.setName(resolveParameterName(param));
|
||||
apiParam.setRequired(resolveRequired(param));
|
||||
apiParam.setDescription(extractParamDescription(param));
|
||||
params.add(apiParam);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 @RequestBody DTO 类的字段为独立参数(便于对比字段增删改)。
|
||||
*/
|
||||
private List<ApiParameter> expandDtoFields(String typeName, Path rootDir, String source) {
|
||||
List<ApiParameter> fields = new ArrayList<>();
|
||||
Optional<Path> dtoFile = findJavaFileBySimpleName(typeName, rootDir);
|
||||
|
||||
if (dtoFile.isEmpty()) {
|
||||
// 找不到 DTO 源文件时,保留整体类型
|
||||
ApiParameter body = new ApiParameter();
|
||||
body.setName(typeName);
|
||||
body.setType(typeName);
|
||||
body.setSource(source);
|
||||
body.setRequired(true);
|
||||
fields.add(body);
|
||||
return fields;
|
||||
}
|
||||
|
||||
try {
|
||||
CompilationUnit cu = StaticJavaParser.parse(dtoFile.get());
|
||||
for (FieldDeclaration field : cu.findAll(FieldDeclaration.class)) {
|
||||
if (field.isStatic()) {
|
||||
continue;
|
||||
}
|
||||
for (var variable : field.getVariables()) {
|
||||
ApiParameter fp = new ApiParameter();
|
||||
fp.setName(variable.getNameAsString());
|
||||
fp.setType(field.getElementType().asString());
|
||||
fp.setSource(source);
|
||||
fp.setRequired(!hasAnnotation(field, "Nullable"));
|
||||
fields.add(fp);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// 解析失败时退化为整体 body 参数
|
||||
ApiParameter body = new ApiParameter();
|
||||
body.setName(typeName);
|
||||
body.setType(typeName);
|
||||
body.setSource(source);
|
||||
fields.add(body);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在源码目录中按简单类名查找 Java 文件。
|
||||
*/
|
||||
private Optional<Path> findJavaFileBySimpleName(String typeName, Path rootDir) {
|
||||
String simpleName = typeName.contains(".") ? typeName.substring(typeName.lastIndexOf('.') + 1) : typeName;
|
||||
simpleName = simpleName.replace(">", "").replace("<", "").trim();
|
||||
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
final String target = simpleName;
|
||||
return paths
|
||||
.filter(p -> p.getFileName().toString().equals(target + ".java"))
|
||||
.findFirst();
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断参数来源:query / path / header / form / body。
|
||||
*/
|
||||
private String resolveParameterSource(Parameter param) {
|
||||
if (hasAnnotation(param, "PathVariable")) return "path";
|
||||
if (hasAnnotation(param, "RequestHeader")) return "header";
|
||||
if (hasAnnotation(param, "RequestPart")) return "form";
|
||||
if (hasAnnotation(param, "ModelAttribute")) return "form";
|
||||
return "query";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析参数名称:优先取注解 value/name,否则用变量名。
|
||||
*/
|
||||
private String resolveParameterName(Parameter param) {
|
||||
for (String ann : Arrays.asList("RequestParam", "PathVariable", "RequestHeader", "RequestPart")) {
|
||||
if (hasAnnotation(param, ann)) {
|
||||
Optional<AnnotationExpr> opt = param.getAnnotationByName(ann);
|
||||
if (opt.isPresent()) {
|
||||
String val = extractAnnotationStringValue(opt.get(), "value", "name");
|
||||
if (!val.isEmpty()) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return param.getNameAsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析参数是否必填。
|
||||
*/
|
||||
private boolean resolveRequired(Parameter param) {
|
||||
if (hasAnnotation(param, "RequestParam")) {
|
||||
Optional<AnnotationExpr> opt = param.getAnnotationByName("RequestParam");
|
||||
if (opt.isPresent()) {
|
||||
String required = extractAnnotationMemberValue(opt.get(), "required");
|
||||
if ("false".equalsIgnoreCase(required)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (param.getType() instanceof ClassOrInterfaceType) {
|
||||
ClassOrInterfaceType cit = (ClassOrInterfaceType) param.getType();
|
||||
if ("Optional".equals(cit.getNameAsString())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !hasAnnotation(param, "Nullable");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 @ApiParam / @Parameter 的 description。
|
||||
*/
|
||||
private String extractParamDescription(Parameter param) {
|
||||
for (String ann : Arrays.asList("ApiParam", "Parameter", "Schema")) {
|
||||
Optional<AnnotationExpr> opt = param.getAnnotationByName(ann);
|
||||
if (opt.isPresent()) {
|
||||
return extractAnnotationStringValue(opt.get(), "description", "value");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasAnnotation(Object node, String simpleName) {
|
||||
if (node instanceof Parameter) {
|
||||
Parameter p = (Parameter) node;
|
||||
return p.getAnnotationByName(simpleName).isPresent();
|
||||
}
|
||||
if (node instanceof FieldDeclaration) {
|
||||
FieldDeclaration f = (FieldDeclaration) node;
|
||||
return f.getAnnotationByName(simpleName).isPresent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注解的简单名称(去掉包名)。
|
||||
*/
|
||||
private String getSimpleAnnotationName(AnnotationExpr annotation) {
|
||||
String name = annotation.getNameAsString();
|
||||
int dot = name.lastIndexOf('.');
|
||||
return dot >= 0 ? name.substring(dot + 1) : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从注解中提取字符串属性,支持 value/path/name 等多个候选 key。
|
||||
*/
|
||||
private String extractAnnotationStringValue(AnnotationExpr annotation, String... keys) {
|
||||
Set<String> keySet = new HashSet<>(Arrays.asList(keys));
|
||||
|
||||
if (annotation instanceof SingleMemberAnnotationExpr) {
|
||||
SingleMemberAnnotationExpr single = (SingleMemberAnnotationExpr) annotation;
|
||||
return stripQuotes(single.getMemberValue().toString());
|
||||
}
|
||||
|
||||
if (annotation instanceof NormalAnnotationExpr) {
|
||||
NormalAnnotationExpr normal = (NormalAnnotationExpr) annotation;
|
||||
for (MemberValuePair pair : normal.getPairs()) {
|
||||
if (keySet.contains(pair.getNameAsString())) {
|
||||
return stripQuotes(pair.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取注解成员的原始字符串值。
|
||||
*/
|
||||
private String extractAnnotationMemberValue(AnnotationExpr annotation, String key) {
|
||||
if (annotation instanceof NormalAnnotationExpr) {
|
||||
NormalAnnotationExpr normal = (NormalAnnotationExpr) annotation;
|
||||
for (MemberValuePair pair : normal.getPairs()) {
|
||||
if (key.equals(pair.getNameAsString())) {
|
||||
return stripQuotes(pair.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String stripQuotes(String value) {
|
||||
return value.replace("\"", "").replace("'", "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接类级别与方法级别的路径。
|
||||
*/
|
||||
private String joinPaths(String base, String methodPath) {
|
||||
String b = normalizePath(base);
|
||||
String m = normalizePath(methodPath);
|
||||
|
||||
if (b.isEmpty()) return m.isEmpty() ? "/" : m;
|
||||
if (m.isEmpty()) return b;
|
||||
|
||||
if (b.endsWith("/") && m.startsWith("/")) {
|
||||
return b + m.substring(1);
|
||||
}
|
||||
if (!b.endsWith("/") && !m.startsWith("/")) {
|
||||
return b + "/" + m;
|
||||
}
|
||||
return b + m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径:确保以 / 开头,去除多余斜杠。
|
||||
*/
|
||||
private String normalizePath(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
path = path.trim();
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
return path.replaceAll("/+", "/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.aicheck;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Java AST 解析器命令行入口。
|
||||
* 用法:java -jar controller-parser.jar <源码目录> [输出JSON文件路径]
|
||||
*
|
||||
* 示例:
|
||||
* java -jar controller-parser.jar ./src/main/java ./endpoints.json
|
||||
*/
|
||||
public class ControllerParserMain {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
|
||||
/**
|
||||
* 程序入口:解析指定目录并输出 JSON。
|
||||
*
|
||||
* @param args [0]=源码目录, [1]=可选的输出文件路径(默认 stdout)
|
||||
*/
|
||||
public static void main(String[] args) throws IOException {
|
||||
if (args.length < 1) {
|
||||
System.err.println("用法: java -jar controller-parser.jar <源码目录> [输出JSON路径]");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
Path sourceDir = Paths.get(args[0]).toAbsolutePath().normalize();
|
||||
ControllerAstParser parser = new ControllerAstParser();
|
||||
List<ApiEndpoint> endpoints = parser.parseDirectory(sourceDir);
|
||||
|
||||
String json = MAPPER.writeValueAsString(endpoints);
|
||||
|
||||
if (args.length >= 2) {
|
||||
Path output = Paths.get(args[1]);
|
||||
MAPPER.writeValue(output.toFile(), endpoints);
|
||||
System.out.println("已解析 " + endpoints.size() + " 个接口,输出至: " + output);
|
||||
} else {
|
||||
System.out.println(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
.gitea/java-parser/target/maven-archiver/pom.properties
Normal file
5
.gitea/java-parser/target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Wed Jun 03 11:29:14 GMT+08:00 2026
|
||||
groupId=com.aicheck
|
||||
artifactId=controller-parser
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,4 @@
|
||||
com\aicheck\ControllerParserMain.class
|
||||
com\aicheck\ControllerAstParser.class
|
||||
com\aicheck\ApiParameter.class
|
||||
com\aicheck\ApiEndpoint.class
|
||||
@@ -0,0 +1,4 @@
|
||||
C:\Users\EDY\Desktop\AI-Check\java-parser\src\main\java\com\aicheck\ControllerAstParser.java
|
||||
C:\Users\EDY\Desktop\AI-Check\java-parser\src\main\java\com\aicheck\ApiParameter.java
|
||||
C:\Users\EDY\Desktop\AI-Check\java-parser\src\main\java\com\aicheck\ControllerParserMain.java
|
||||
C:\Users\EDY\Desktop\AI-Check\java-parser\src\main\java\com\aicheck\ApiEndpoint.java
|
||||
Reference in New Issue
Block a user