⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://blog.csdn.net/W03096934 「感性的程序员小王」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

在之前的博客中,我曾介绍过一款AI驱动在线协作绘图平台——IntelliDraw(在线访问地址:https://www.intellidraw.top)。

经过持续优化与创新,IntelliDraw现已升级为一个功能更为强大的开发辅助工具,能够:

  • 基于用户上传的数据库SQL文件自动生成实体关系图(ER Diagram)
  • 支持Java Spring Boot项目代码分析,可一键上传项目ZIP包并生成完整的项目架构图,清晰展示各层级间的调用关系
  • 本次更新不仅带来了功能上的扩展,还对系统性能进行了全面优化:
  • 通过改进后端提示词(Prompt)算法,将模型调用的token消耗降低了60%
  • 引入了先进的RAG(Retrieval-Augmented Generation)功能

技术实现

Spring Boot 项目解析

使用了 JavaParser 库实现的 Java 类的解析,maven 依赖如下

<!-- Source: https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core -->
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.25.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-symbol-solver-core</artifactId>
<version>3.25.8</version>
<scope>compile</scope>
</dependency>

通过JavaParser库解析Java代码,生成抽象语法树(AST),以便分析Spring Boot项目的结构和依赖关系。AST是计算机理解代码的逻辑结构,类似于将代码分解为语法节点。

例如,int x = 10 + 10;的AST包括变量声明、赋值和加法运算节点。解析整个项目时,需识别包、类、方法、字段、注解及依赖关系,特别是Spring Bean的注解。生成项目架构图时,AST需包含模块划分、层次结构、依赖关系和调用链路,以展示项目的组织和功能流程。

抽象语法树(AST)的定义与作用

抽象语法树(AST)是编程语言源代码的抽象语法结构的树形表示。它帮助计算机理解代码的结构,使代码能够被分析和处理。AST忽略了代码中的语法细节,专注于代码的逻辑结构。

AST示例解析

1.加法运算的AST结构

代码:int x = 10 + 10;

AST分解:

  • 变量声明节点:int x
  • 赋值操作节点:=
  • 加法运算节点:+,包含两个子节点10和10

2.If语句的AST结构代码:if (a > 10) { break; }

AST分解:

  • 条件判断节点:if
  • 条件表达式节点:a > 10
  • 代码块节点:break;

JavaParser在Spring Boot项目中的应用

使用JavaParser库解析整个Spring Boot项目时,需要解析以下内容:

3.包和类结构

  • 包名
  • 类名
  • 类的修饰符(如public、final)

4.类成员

  • 方法: 包括方法名、参数、返回类型、访问修饰符
  • 字段: 包括字段名、类型、访问修饰符

5.注解

  • 特别是Spring相关的注解,如@Component、@Service、@Autowired等,用于识别Spring Bean及其依赖关系

6.依赖关系

  • 通过注解和依赖注入机制,解析类之间的依赖关系,如@Autowired注入的字段或方法参数

7.调用关系

  • 分析方法之间的调用,构建方法调用链路

生成项目架构图所需的AST特征

为了生成清晰的项目架构图,AST需要包含以下信息:

8.模块划分

  • 根据包名或注解信息,将项目划分为不同的模块或层(如Controller、Service、Repository)

9.层次结构

  • 展示模块之间的层次关系,如分层架构(表现层、业务逻辑层、数据访问层)

10.依赖关系

  • 通过注解和依赖注入信息,展示类之间的依赖关系,帮助识别高耦合模块

11.调用链路

  • 展示方法之间的调用顺序,帮助理解功能流程

12.接口和实现

  • 展示接口与其实现类之间的关系,帮助理解抽象与具体实现的对应

这里我设计了几个通用的 DTO

package com.wfh.drawio.core.model;

import lombok.Data;
import java.util.List;

/**
* 1. 架构图中的“节点” (比如一个类、一个表、一个微服务)
* @author fenghuanwang
*/
@Data
publicclass ArchNode {
private String id; // 唯一标识 (e.g., "com.example.UserService")
private String name; // 显示名称 (e.g., "UserService")
private String type; // 类型 (e.g., "CLASS", "INTERFACE", "TABLE", "SERVICE")
private String stereotype; // 构造型/注解 (e.g., "@Controller", "@Repository")
private List<String> fields; // 关键字段
// 新增字段:用于存放 API 路由列表、SQL 表名或其他备注
private String description;
// 建议新增:用于分组(比如按包名分组,画出子图)
private String group;
private List<String> methods; // 关键方法
}

这个就类似于架构 图中的一个节点,或者说是一个服务。

package com.wfh.drawio.core.model;

import lombok.Data;

/**
* 2. 架构图中的“连线” (比如继承、调用、外键)
* @author fenghuanwang
*/
@Data
public class ArchRelationship {
private String sourceId; // 起点
private String targetId; // 终点
private String type; // 关系类型 (e.g., "DEPENDS_ON", "INHERITS", "CALLS", "FOREIGN_KEY")
private String label; // 连线上的文字 (e.g., "findUserById")
}

这个就是架构图中的连线,放到代码中就是服务之间的相互调用之类的。

/**
* 3. 完整的分析结果
* @author fenghuanwang
*/
@Data
public class ProjectAnalysisResult {
private List<ArchNode> nodes;
private List<ArchRelationship> relationships;
}

而这个就是完整的分析结果了,也就是要返回给前端的东西

那这个 Java 解析器具体代码逻辑是怎么样的呢

下面是完整的代码,这段代码是使用了 Java 的 SPI 机制实现的:

package com.wfh.drawio.spi.parser;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import com.wfh.drawio.core.model.ArchNode;
import com.wfh.drawio.core.model.ArchRelationship;
import com.wfh.drawio.core.model.ProjectAnalysisResult;
import com.wfh.drawio.spi.LanguageParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* @Title: JavaSpringParser
* @Author wangfenghuan
* @description: 极致优化的 Spring Boot 项目解析器 (支持 Javadoc 提取)
*/
@Slf4j
publicclass JavaSpringParser implements LanguageParser {

privatestaticfinal Map<String, String> STEREOTYPE_MAPPING = Map.of(
"RestController", "API Layer",
"Controller", "API Layer",
"Service", "Business Layer",
"Repository", "Data Layer",
"Mapper", "Data Layer",
"Component", "Infrastructure",
"Configuration", "Infrastructure",
"Entity", "Data Layer",
"Table", "Data Layer"
);

privatestaticfinal Set<String> IGNORED_SUFFIXES = Set.of(
"Test", "Tests", "DTO", "VO", "Request", "Response", "Exception", "Constant", "Config", "Utils", "Properties"
);

privatestaticfinal Set<String> IGNORED_PACKAGES = Set.of(
"java.", "javax.", "jakarta.",
"org.springframework.", "org.slf4j.", "org.apache.",
"com.baomidou.", "lombok.", "com.fasterxml."
);

@Override
public String getName() {
return"Java-Spring-Doc-Enhanced";
}

@Override
public boolean canParse(String projectDir) {
Path rootPath = Paths.get(projectDir);
if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) returnfalse;
try (Stream<Path> stream = Files.walk(rootPath, 2)) {
return stream.filter(Files::isRegularFile).anyMatch(p -> {
String name = p.getFileName().toString();
return name.equals("pom.xml") || name.equals("build.gradle") || name.equals("build.gradle.kts");
});
} catch (IOException e) {
returnfalse;
}
}

@Override
public ProjectAnalysisResult parse(String projectDir) {
log.info("Starting Analysis with Javadoc Extraction: {}", projectDir);
JavaParser javaParser = initializeSymbolSolver(projectDir);
List<ArchNode> rawNodes = new ArrayList<>();
List<ArchRelationship> rawRelationships = new ArrayList<>();
Set<String> processedClasses = new HashSet<>();

try {
List<Path> javaFiles = findJavaFiles(projectDir);
for (Path javaFile : javaFiles) {
try {
CompilationUnit cu = javaParser.parse(javaFile).getResult().orElse(null);
if (cu == null) continue;

String packageName = cu.getPackageDeclaration().map(pd -> pd.getNameAsString()).orElse("");

cu.findAll(ClassOrInterfaceDeclaration.class).forEach(clazz -> {
String className = clazz.getNameAsString();
if (isIgnoredClass(className)) return;

String fullClassName = getFullyQualifiedName(clazz, packageName);
if (processedClasses.contains(fullClassName)) return;

ArchNode node = analyzeClass(clazz, fullClassName);
if (node != null) rawNodes.add(node);

rawRelationships.addAll(analyzeRelationships(clazz, fullClassName, cu));
processedClasses.add(fullClassName);
});

} catch (Exception e) {
log.warn("Parse error: {}", e.getMessage());
}
}
} catch (IOException e) {
log.error("IO Error", e);
}

return optimizeResult(rawNodes, rawRelationships);
}

private ArchNode analyzeClass(ClassOrInterfaceDeclaration clazz, String id) {
ArchNode node = new ArchNode();
node.setId(id);
node.setName(clazz.getNameAsString());

// 1. 提取 Javadoc 注释 (新增功能)
String comment = extractClassComment(clazz);

String type = "Class";
String stereotype = "Model";

if (clazz.isInterface()) type = "Interface";

List<String> annotations = clazz.getAnnotations().stream().map(a -> a.getNameAsString()).collect(Collectors.toList());

if (clazz.getNameAsString().endsWith("Mapper") || annotations.contains("Mapper")) {
type = "Interface";
stereotype = "Data Layer";
}

for (Map.Entry<String, String> entry : STEREOTYPE_MAPPING.entrySet()) {
if (annotations.stream().anyMatch(a -> a.contains(entry.getKey()))) {
type = entry.getKey().toUpperCase();
stereotype = entry.getValue();
break;
}
}

node.setType(type);
node.setStereotype(stereotype);

// 2. 构建 Description (合并 注释 + 技术细节)
List<String> descParts = new ArrayList<>();

// Part A: 中文注释
if (comment != null && !comment.isEmpty()) {
descParts.add(comment);
}

// Part B: 技术细节 (API 路由 或 表名)
if ("API Layer".equals(stereotype)) {
List<String> routes = extractApiRoutes(clazz);
if (!routes.isEmpty()) {
descParts.add("APIs:\n" + String.join("\n", routes));
}
} elseif (clazz.getNameAsString().endsWith("Entity") || annotations.contains("TableName")) {
extractTableName(clazz).ifPresent(t -> descParts.add("Table: " + t));
}

if (!descParts.isEmpty()) {
node.setDescription(String.join("\n\n", descParts));
}

node.setFields(Collections.emptyList());
node.setMethods(Collections.emptyList());

return node;
}

/**
* 新增:提取并清洗 Javadoc
*/
private String extractClassComment(ClassOrInterfaceDeclaration clazz) {
return clazz.getComment()
.map(Comment::getContent)
.map(this::cleanJavadoc)
.orElse("");
}

/**
* 清洗 Javadoc:去除 * 号、@author 等标签
*/
private String cleanJavadoc(String content) {
if (content == null) return"";
String[] lines = content.split("\n");
StringBuilder sb = new StringBuilder();

for (String line : lines) {
// 去除开头的 * 和空格
String cleanLine = line.trim().replaceAll("^\\*+\\s?", "").trim();

// 遇到 @ 标签(如 @author, @date)停止读取,或者跳过
// 这里策略是:只读取第一段描述,遇到 @ 就停止,通常第一段是核心描述
if (cleanLine.startsWith("@")) {
break;
}
// 忽略空行
if (!cleanLine.isEmpty()) {
sb.append(cleanLine).append(" "); // 将多行描述合并为一行
}
}
return sb.toString().trim();
}

private ProjectAnalysisResult optimizeResult(List<ArchNode> nodes, List<ArchRelationship> relationships) {
Set<String> validNodeIds = nodes.stream().map(ArchNode::getId).collect(Collectors.toSet());
List<ArchRelationship> cleanRelationships = relationships.stream()
.filter(r -> validNodeIds.contains(r.getSourceId()) && validNodeIds.contains(r.getTargetId()))
.filter(r -> !r.getSourceId().equals(r.getTargetId()))
.collect(Collectors.toList());

Set<String> connectedNodes = new HashSet<>();
cleanRelationships.forEach(r -> {
connectedNodes.add(r.getSourceId());
connectedNodes.add(r.getTargetId());
});

List<ArchNode> cleanNodes = nodes.stream()
.filter(n -> {
if ("API Layer".equals(n.getStereotype())) returntrue;
return connectedNodes.contains(n.getId());
})
.collect(Collectors.toList());

ProjectAnalysisResult result = new ProjectAnalysisResult();
result.setNodes(cleanNodes);
result.setRelationships(cleanRelationships);
return result;
}

private List<ArchRelationship> analyzeRelationships(ClassOrInterfaceDeclaration clazz, String sourceId, CompilationUnit cu) {
List<ArchRelationship> rels = new ArrayList<>();
clazz.getExtendedTypes().forEach(t -> addRel(rels, sourceId, resolveType(t, cu), "EXTENDS"));
clazz.getImplementedTypes().forEach(t -> addRel(rels, sourceId, resolveType(t, cu), "IMPLEMENTS"));
clazz.getFields().forEach(field -> {
if (field.isAnnotationPresent("Autowired") || field.isAnnotationPresent("Resource") || field.isAnnotationPresent("Inject")) {
field.getVariables().forEach(v -> addRel(rels, sourceId, resolveType(field.getElementType(), cu), "DEPENDS_ON"));
}
});
clazz.getConstructors().forEach(c -> c.getParameters().forEach(p -> addRel(rels, sourceId, resolveType(p.getType(), cu), "DEPENDS_ON")));
return rels;
}

private boolean isIgnoredClass(String className) {
return IGNORED_SUFFIXES.stream().anyMatch(className::endsWith);
}

private boolean isIgnoredPackage(String typeName) {
if (typeName == null) returntrue;
return IGNORED_PACKAGES.stream().anyMatch(typeName::startsWith);
}

private void addRel(List<ArchRelationship> rels, String src, String target, String type) {
if (target == null || target.equals(src) || isIgnoredPackage(target)) return;
ArchRelationship rel = new ArchRelationship();
rel.setSourceId(src);
rel.setTargetId(target);
rel.setType(type);
rels.add(rel);
}

private JavaParser initializeSymbolSolver(String projectDir) {
CombinedTypeSolver combinedSolver = new CombinedTypeSolver();
combinedSolver.add(new ReflectionTypeSolver());
Path srcPath = Paths.get(projectDir, "src", "main", "java");
if (Files.exists(srcPath)) {
combinedSolver.add(new JavaParserTypeSolver(srcPath));
} else {
combinedSolver.add(new JavaParserTypeSolver(new File(projectDir)));
}
ParserConfiguration config = new ParserConfiguration();
config.setSymbolResolver(new JavaSymbolSolver(combinedSolver));
returnnew JavaParser(config);
}

private String resolveType(ClassOrInterfaceType type, CompilationUnit cu) {
try {
return type.resolve().asReferenceType().getQualifiedName();
} catch (Exception e) {
return inferFullyQualifiedNameFromImports(type.getNameAsString(), cu);
}
}

private String resolveType(com.github.javaparser.ast.type.Type type, CompilationUnit cu) {
try {
if (type.isClassOrInterfaceType()) return type.asClassOrInterfaceType().resolve().asReferenceType().getQualifiedName();
returnnull;
} catch (Exception e) {
return inferFullyQualifiedNameFromImports(type.asString(), cu);
}
}

private String inferFullyQualifiedNameFromImports(String simpleName, CompilationUnit cu) {
Optional<ImportDeclaration> match = cu.getImports().stream()
.filter(i -> !i.isAsterisk() && i.getNameAsString().endsWith("." + simpleName))
.findFirst();
if (match.isPresent()) return match.get().getNameAsString();
String packageName = cu.getPackageDeclaration().map(pd -> pd.getNameAsString()).orElse("");
return packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
}

private String getFullyQualifiedName(ClassOrInterfaceDeclaration clazz, String packageName) {
return packageName.isEmpty() ? clazz.getNameAsString() : packageName + "." + clazz.getNameAsString();
}

private List<String> extractApiRoutes(ClassOrInterfaceDeclaration clazz) {
List<String> routes = new ArrayList<>();
String basePath = "";
Optional<AnnotationExpr> classMapping = clazz.getAnnotationByName("RequestMapping");
if (classMapping.isPresent()) basePath = extractPath(classMapping.get());
String finalBasePath = basePath;
clazz.getMethods().forEach(m -> {
Stream.of("GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "RequestMapping").forEach(methodType -> {
m.getAnnotationByName(methodType).ifPresent(ann -> {
String methodPath = extractPath(ann);
String httpMethod = methodType.replace("Mapping", "").toUpperCase();
if ("REQUEST".equals(httpMethod)) httpMethod = "ALL";
routes.add("[" + httpMethod + "] " + (finalBasePath + methodPath).replaceAll("//", "/"));
});
});
});
return routes;
}

private String extractPath(AnnotationExpr ann) {
if (ann.isSingleMemberAnnotationExpr()) {
return ann.asSingleMemberAnnotationExpr().getMemberValue().toString().replace("\"", "");
} elseif (ann.isNormalAnnotationExpr()) {
return ann.asNormalAnnotationExpr().getPairs().stream()
.filter(p -> p.getNameAsString().equals("value") || p.getNameAsString().equals("path"))
.findFirst().map(p -> p.getValue().toString().replace("\"", "")).orElse("");
}
return"";
}

private Optional<String> extractTableName(ClassOrInterfaceDeclaration clazz) {
return clazz.getAnnotationByName("TableName").map(this::extractPath)
.or(() -> clazz.getAnnotationByName("Table").map(this::extractPath));
}

private List<Path> findJavaFiles(String projectPath) throws IOException {
try (Stream<Path> paths = Files.walk(Paths.get(projectPath))) {
return paths.filter(Files::isRegularFile).filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
}
}
}

SQL 语句解析

在实现SQL解析功能时,我选择了使用阿里巴巴开源的Druid(德鲁伊)项目中的SQLUtil模块。这一选择是基于Druid在SQL解析领域的强大功能和良好性能,同时其丰富的API和活跃的开源社区也为项目开发提供了有力支持。

在具体实现上,我采用了插件化(Service Provider Interface, SPI)的设计模式。通过定义统一的接口规范,

解析器代码如下

package com.wfh.drawio.spi.parser;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.util.JdbcConstants;
import com.wfh.drawio.core.model.ArchNode;
import com.wfh.drawio.core.model.ArchRelationship;
import com.wfh.drawio.core.model.ProjectAnalysisResult;
import com.wfh.drawio.spi.LanguageParser;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* @Title: SqlParser
* @Author wangfenghuan
* @description: 优化版 SQL 解析器 (已修复 Lambda 变量 Final 问题)
*/
@Slf4j
publicclass SqlParser implements LanguageParser {

privatestaticfinal Set<String> IGNORED_COLUMNS = Set.of(
"create_time", "update_time", "create_at", "update_at",
"created_time", "updated_time", "created_at", "updated_at",
"is_delete", "is_deleted", "version", "revision"
);

@Override
public String getName() {
return"SQL-DDL-Enhanced";
}

@Override
public boolean canParse(String projectDir) {
File file = new File(projectDir);
if (!file.exists()) {
returnfalse;
}
if (file.isFile()) {
return file.getName().toLowerCase().endsWith(".sql");
}
try (Stream<Path> paths = Files.walk(Paths.get(projectDir), 3)) {
return paths.anyMatch(p -> p.toString().toLowerCase().endsWith(".sql"));
} catch (IOException e) {
returnfalse;
}
}

@Override
public ProjectAnalysisResult parse(String projectDir) {
log.info("Starting SQL analysis: {}", projectDir);
ProjectAnalysisResult result = new ProjectAnalysisResult();
List<ArchNode> nodes = new ArrayList<>();
List<ArchRelationship> relationships = new ArrayList<>();
Set<String> tableNames = new HashSet<>();

try {
List<Path> sqlFiles = findSqlFiles(projectDir);
for (Path sqlFile : sqlFiles) {
String content = Files.readString(sqlFile);
parseSqlContent(content, nodes, relationships, tableNames);
}
inferLogicalRelationships(nodes, relationships, tableNames);
} catch (Exception e) {
log.error("Error parsing SQL files", e);
}

result.setNodes(nodes);
result.setRelationships(relationships);
return result;
}

private void parseSqlContent(String content, List<ArchNode> nodes, List<ArchRelationship> relationships, Set<String> tableNames) {
List<SQLStatement> statements = parseWithFallback(content);
for (SQLStatement stmt : statements) {
if (stmt instanceof SQLCreateTableStatement) {
SQLCreateTableStatement createTable = (SQLCreateTableStatement) stmt;
ArchNode node = processCreateTable(createTable, relationships);
if (node != null) {
nodes.add(node);
tableNames.add(node.getId());
}
}
}
}

private List<SQLStatement> parseWithFallback(String content) {
try {
return SQLUtils.parseStatements(content, JdbcConstants.MYSQL);
} catch (Exception e1) {
try {
return SQLUtils.parseStatements(content, JdbcConstants.POSTGRESQL);
} catch (Exception e2) {
try {
return SQLUtils.parseStatements(content, JdbcConstants.ORACLE);
} catch (Exception e3) {
return Collections.emptyList();
}
}
}
}

private ArchNode processCreateTable(SQLCreateTableStatement createTable, List<ArchRelationship> relationships) {
String tableName = cleanName(createTable.getTableName());
ArchNode node = new ArchNode();
node.setId(tableName);
node.setName(tableName);
node.setType("TABLE");
node.setStereotype("Database Table");

if (createTable.getComment() != null) {
node.setDescription(cleanComment(createTable.getComment().toString()));
}

List<String> fields = new ArrayList<>();

for (SQLTableElement element : createTable.getTableElementList()) {
if (element instanceof SQLColumnDefinition) {
SQLColumnDefinition column = (SQLColumnDefinition) element;
String colName = cleanName(column.getName().getSimpleName());

if (IGNORED_COLUMNS.contains(colName.toLowerCase())) {
continue;
}

String colType = column.getDataType().getName();
StringBuilder fieldStr = new StringBuilder(colName).append(": ").append(colType);

if (column.isPrimaryKey()) {
fieldStr.append(" (PK)");
} elseif (createTable.findColumn(colName) != null && isPrimaryKeyInConstraints(createTable, colName)) {
fieldStr.append(" (PK)");
}

if (column.getComment() != null) {
String comment = cleanComment(column.getComment().toString());
if (!comment.isEmpty()) {
fieldStr.append(" // ").append(comment);
}
}
fields.add(fieldStr.toString());

} elseif (element instanceof SQLForeignKeyConstraint) {
SQLForeignKeyConstraint fk = (SQLForeignKeyConstraint) element;
String targetTable = cleanName(fk.getReferencedTableName().getSimpleName());
ArchRelationship rel = new ArchRelationship();
rel.setSourceId(tableName);
rel.setTargetId(targetTable);
rel.setType("FOREIGN_KEY");
String colName = fk.getReferencingColumns().stream()
.map(c -> cleanName(c.getSimpleName()))
.collect(Collectors.joining(","));
rel.setLabel("FK: " + colName);
relationships.add(rel);
}
}
node.setFields(fields);
node.setMethods(Collections.emptyList());
return node;
}

/**
* 修复点:inferLogicalRelationships 方法
* 引入 finalTarget 变量,解决 Lambda 表达式报错
*/
private void inferLogicalRelationships(List<ArchNode> nodes, List<ArchRelationship> relationships, Set<String> allTables) {
for (ArchNode node : nodes) {
if (node.getFields() == null) continue;

for (String fieldStr : node.getFields()) {
String colName = fieldStr.split(":")[0].trim();

if (colName.toLowerCase().endsWith("_id")) {
String potentialTableName = colName.substring(0, colName.length() - 3);

String tempTarget = matchTable(potentialTableName, allTables);
if (tempTarget == null) {
tempTarget = matchTable(potentialTableName + "s", allTables);
}

// ✅ 关键修复:将可能变化的 tempTarget 赋值给一个 final 变量
String finalTarget = tempTarget;

if (finalTarget != null && !finalTarget.equals(node.getId())) {
// 在 Lambda 中只使用 finalTarget
boolean exists = relationships.stream().anyMatch(r ->
r.getSourceId().equals(node.getId()) && r.getTargetId().equals(finalTarget)
);

if (!exists) {
ArchRelationship rel = new ArchRelationship();
rel.setSourceId(node.getId());
rel.setTargetId(finalTarget);
rel.setType("LOGICAL_KEY");
rel.setLabel("Link: " + colName);
relationships.add(rel);
}
}
}
}
}
}

private String matchTable(String guess, Set<String> tables) {
for (String table : tables) {
if (table.equalsIgnoreCase(guess)) return table;
}
returnnull;
}

private boolean isPrimaryKeyInConstraints(SQLCreateTableStatement table, String colName) {
if (table.getTableElementList() == null) returnfalse;
for (SQLTableElement element : table.getTableElementList()) {
if (element instanceof SQLPrimaryKey) {
SQLPrimaryKey pk = (SQLPrimaryKey) element;
return pk.getColumns().stream().anyMatch(c -> cleanName(c.getExpr().toString()).equals(colName));
}
}
returnfalse;
}

private String cleanName(String name) {
if (name == null) return"";
return name.replace("`", "").replace("\"", "").replace("'", "").trim();
}
private String cleanComment(String comment) {
if (comment == null) return"";
return comment.replace("'", "").trim();
}
private List<Path> findSqlFiles(String projectPath) throws IOException {
Path startPath = Paths.get(projectPath);
if (Files.isRegularFile(startPath) && startPath.toString().toLowerCase().endsWith(".sql")) {
return List.of(startPath);
}
try (Stream<Path> paths = Files.walk(startPath)) {
return paths
.filter(Files::isRegularFile)
.filter(path -> path.toString().toLowerCase().endsWith(".sql"))
.collect(Collectors.toList());
}
}
}

刚才定义的一系列 DTO 是对于任何的图表都适用的,因为节点和连线就是构成一个不管是结构图还有 ER 图的主要元素。

GitHub 仓库

前端

后端

文章目录
  1. 1. 前言
  2. 2. 技术实现
    1. 2.1. Spring Boot 项目解析
    2. 2.2. 抽象语法树(AST)的定义与作用
      1. 2.2.1. AST示例解析
    3. 2.3. SQL 语句解析
  3. 3. GitHub 仓库
    1. 3.0.1. 前端
    2. 3.0.2. 后端