Java代码变更影响分析(一)

Java代码变更影响分析(一)

背景:为了做一些精准测试,根据代码的变更分析哪些接口受到影响,本文采用的是静态代码分析(更精准的是字节码分析)

说明:由于代码量较多,会拆分两篇文章来解释所有的功能,第一篇文章主要介绍如何查找到变更的类;第二篇主要介绍如何构建调用链路图以及通过调用链路图查询影响的接口

1、最终效果

项目最终打成一个jar包,我是作为一个mcp工具使用,当然也可以在java环境下直接java -jar运行

2、项目依赖

核心依赖包是jgit、javaparser-symbol-solver-core、picocli

xml 复制代码
<?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.bilibili</groupId>
    <artifactId>java-analyzer</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- JavaParser - 解析Java代码 -->
        <dependency>
            <groupId>com.github.javaparser</groupId>
            <artifactId>javaparser-symbol-solver-core</artifactId>
            <version>3.25.8</version>
        </dependency>

        <!-- JGit - Git操作 -->
        <dependency>
            <groupId>org.eclipse.jgit</groupId>
            <artifactId>org.eclipse.jgit</artifactId>
            <version>5.13.3.202401111512-r</version>
        </dependency>

        <!-- PicoCLI - 命令行框架 -->
        <dependency>
            <groupId>info.picocli</groupId>
            <artifactId>picocli</artifactId>
            <version>4.7.5</version>
        </dependency>

        <!-- Jackson - JSON序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.16.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

        <!-- JUnit 5 - 测试框架 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>

        <!-- SLF4J Simple - 日志实现 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.36</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Assembly Plugin - 打包成单个jar -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.bilibili.Main</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <appendAssemblyId>false</appendAssemblyId>
                    <finalName>analyzer</finalName>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

3、代码分析

3.1、GitUtil

主要提供 Git 仓库的基础操作方法

java 复制代码
package com.bilibili.utils.git;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.*;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * Git 仓库操作工具类
 * <p>
 * 提供 Git 仓库的基础操作方法,包括:
 * <ul>
 *     <li>打开和关闭 Git 仓库</li>
 *     <li>解析 Git 引用到 CommitId</li>
 * </ul>
 * </p>
 * <p>
 * <b>注意</b>:此类仅包含静态工具方法,不应该被实例化。
 * 文件读取等高级操作请使用 {@link com.bilibili.GitReader}。
 * </p>
 */
@Slf4j
public final class GitUtil {

    private GitUtil() {
        throw new AssertionError("Utility class should not be instantiated");
    }

    /**
     * 打开指定路径下的 Git 仓库
     *
     * @param projectPath Git 项目的根目录路径
     * @return Git 对象(使用完毕后必须调用 Close 关闭),如果打开失败返回 null
     */
    public static Git openRepository(String projectPath) {
        try {
            return Git.open(new File(projectPath));
        } catch (IOException e) {
            log.error("打开 Git 仓库失败: {}", e.getMessage());
            return null;
        }
    }

    /**
     * 关闭 Git 对象(会自动关闭内部的 Repository)
     *
     * @param git 要关闭的 Git 对象
     */
    public static void closeGit(Git git) {
        if (git != null) {
            git.close();
        }
    }

    /**
     * 解析 Git引用 到 CommitId
     * <ul>
     *   <li>commit id: "abc123def456..."</li>
     *   <li>feature name: "main", "feature/new-api"</li>
     * </ul>
     *
     * @param repository Repository对象
     * @param ref        Git引用
     * @return CommitId,如果无法解析返回null
     */
    public static ObjectId resolveReference(Repository repository, String ref) {
        try {
            return repository.resolve(ref);
        } catch (Exception e) {
            log.error("解析 Git引用「{}」 失败: {}", ref, e.getMessage());
            return null;
        }
    }
}

3.2、GitReader

封装了通过 JGit 从 Git 仓库读取文件的核心操作,主要提供读取.git文件内容的功能

java 复制代码
package com.bilibili;

import com.bilibili.utils.git.GitUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Predicate;

/**
 * Git 文件读取器
 * <p>
 * 封装了通过 JGit 从 Git 仓库读取文件的核心操作,支持:
 * <ul>
 *     <li>获取两个 Git 引用之间变更的 Java 文件路径列表</li>
 *     <li>读取指定 Git 引用的所有 Java 文件内容</li>
 *     <li>读取指定 Git 引用的单个文件内容</li>
 * </ul>
 * </p>
 * <p>
 * 此类持有 {@link Repository} 对象和两个 Git 引用(ref1, ref2),
 * 通过 JGit 的 {@link DiffFormatter}、{@link TreeWalk} 等 API 实现文件读取。
 * </p>
 */
@Data
@Slf4j
@AllArgsConstructor
@NoArgsConstructor
public class GitReader {
    private Repository repository;
    private String ref1;
    private String ref2;

    /**
     * 获取两个Git引用之间变更的Java文件路径列表(带过滤器)
     *
     * @param filter     路径过滤器,用于筛选特定路径下的文件
     * @return 变更的Java文件路径集合
     */
    public Set<String> changeJavaFilePaths(Predicate<String> filter) throws IOException {
        // 使用LinkedHashSet保持文件路径的插入顺序,同时避免重复
        Set<String> changedFiles = new LinkedHashSet<>();

        // 步骤1: 将两个Git引用解析为CommitId
        ObjectId commitId1 = GitUtil.resolveReference(repository, ref1);
        ObjectId commitId2 = GitUtil.resolveReference(repository, ref2);
        if (commitId1 == null || commitId2 == null) {
            throw new IllegalArgumentException("无法解析源引用: " + ref1 + "," + ref2);
        }

        // 步骤2: 获取两个commit对应的Tree对象ID
        ObjectId tree1 = getTreeId(repository, commitId1);
        ObjectId tree2 = getTreeId(repository, commitId2);

        // 步骤3: 使用DiffFormatter比较两个Tree
        // - DiffFormatter是JGit提供的核心差异比较工具
        // - DisabledOutputStream.INSTANCE表示不输出diff的详细内容
        try (DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
            diffFormatter.setRepository(repository);
            // 步骤4: 扫描两个Tree之间的差异
            // - scan()方法会遍历两个Tree,找出所有不同的文件,返回DiffEntry列表,每个DiffEntry代表一个文件的变更
            List<DiffEntry> diffs = diffFormatter.scan(tree1, tree2);

            // 步骤5: 遍历所有变更,筛选出Java文件
            for (DiffEntry diff : diffs) {
                // 获取变更类型:ADD(新增), MODIFY(修改), DELETE(删除), RENAME(重命名), COPY(复制)
                DiffEntry.ChangeType changeType = diff.getChangeType();
                // 根据变更类型获取合适的文件路径
                // 根据变更类型获取合适的文件路径
                String path;

                if (changeType == DiffEntry.ChangeType.DELETE) {
                    // 删除的文件:使用旧路径(因为新路径在ref2中不存在)
                    // 注意:删除的文件在getNewPath()中返回"/dev/null"
                    path = diff.getOldPath();
                } else if (changeType == DiffEntry.ChangeType.RENAME) {
                    // 重命名的文件:使用新路径(新名称)
                    // 注意:如果需要同时追踪旧路径,可以用getOldPath()
                    path = diff.getNewPath();
                } else {
                    // ADD, MODIFY, COPY等情况:使用新路径
                    path = diff.getNewPath();
                }

                // 步骤6: 应用过滤条件
                if (path.endsWith(".java") && filter.test(path)) {
                    changedFiles.add(path);
                }
            }
        }

        return changedFiles;
    }

    /**
     * 获取指定 Git引用 的 java 文件(带过滤器)
     *
     * @param ref        Git引用
     * @return 文件路径到文件内容的映射
     */
    public Map<String, String> javaFiles(String ref, Predicate<String> filter) throws IOException {
        // 使用LinkedHashSet保持文件路径的插入顺序,同时避免重复
        Map<String, String> files = new LinkedHashMap<>();

        // 步骤1: 将Git引用解析为CommitId
        ObjectId CommitId = GitUtil.resolveReference(repository, ref);
        if (CommitId == null) {
            throw new IllegalArgumentException("无法解析引用: " + ref);
        }

        // 步骤2: 使用RevWalk(用于解析和遍历Git的commit历史)遍历Git对象图
        try (RevWalk revWalk = new RevWalk(repository)) {
            // 将ObjectId解析为RevCommit对象,获取完整的commit信息
            RevCommit commit = revWalk.parseCommit(CommitId);

            // 获取该commit对应的Tree对象(代表该commit时刻的完整文件系统快照)
            RevTree tree = commit.getTree();

            // 步骤3: 使用TreeWalk遍历Tree中的所有文件和目录
            try (TreeWalk treeWalk = new TreeWalk(repository)) {
                // 将tree添加到treeWalk中,指定要遍历的目标Tree
                treeWalk.addTree(tree);

                // 设置为递归模式,这样会遍历所有子目录(注意递归模式下只会返回文件路径,不会返回目录路径)
                treeWalk.setRecursive(true);

                // 步骤4: 遍历所有文件,获取文件内容
                while (treeWalk.next()) {
                    // 获取当前文件的完整路径(相对于项目根目录)例如: "src/main/java/com/example/Main.java"
                    String path = treeWalk.getPathString();

                    if (filter.test(path) && path.endsWith(".java") && !path.contains("/src/test")) {
                        // 获取当前文件对应的Git对象ID
                        // - TreeWalk可以同时遍历多个Tree,参数表示Tree的索引,0表示第一个Tree
                        // - blobId是Git计算文件中的内容后生成的SHA-1哈希
                        ObjectId blobId = treeWalk.getObjectId(0);

                        // 根据Blob对象ID读取文件的实际内容
                        String content = readBlobContent(repository, blobId);

                        // 将文件路径和内容存储到Map中
                        files.put(path, content);
                    }
                }
            }
        }

        return files;
    }

    /**
     * 读取指定commit的单个文件
     *
     * @param ref        Git引用
     * @param filePath   文件路径(相对于项目根目录)
     * @return 文件内容,如果文件不存在返回null
     */
    public String readJavaFile(String ref, String filePath) throws IOException {
        // 步骤1: 将Git引用解析为CommitId
        ObjectId commitId = GitUtil.resolveReference(repository, ref);
        if (commitId == null) {
            throw new IllegalArgumentException("无法解析引用: " + ref);
        }

        // 步骤2: 使用RevWalk(用于解析和遍历Git的commit历史)遍历Git对象图
        try (RevWalk revWalk = new RevWalk(repository)) {
            // 将ObjectId解析为RevCommit对象,获取完整的commit信息
            RevCommit commit = revWalk.parseCommit(commitId);

            // 获取该commit对应的Tree对象(代表该commit时刻的完整文件系统快照)
            RevTree tree = commit.getTree();

            // 步骤3: 使用 TreeWalk 查找指定路径的文件
            // - TreeWalk.forPath() 是一个便捷静态方法,用于直接定位到指定文件路径
            try (TreeWalk treeWalk = TreeWalk.forPath(repository, filePath, tree)) {
                if (treeWalk == null) {
                    return null;
                }

                // 步骤4: 获取文件内容的 Blob 对象 ID 并读取
                ObjectId blobId = treeWalk.getObjectId(0);

                // 根据Blob对象ID读取文件的实际内容
                return readBlobContent(repository, blobId);
            }
        }
    }

    /**
     * 获取Tree对象ID
     *
     * @param repository Repository对象
     * @param commitId   commit对象ID
     * @return Tree对象ID
     */
    private ObjectId getTreeId(Repository repository, ObjectId commitId) throws IOException {
        try (RevWalk revWalk = new RevWalk(repository)) {
            RevCommit commit = revWalk.parseCommit(commitId);
            return commit.getTree().getId();
        }
    }

    /**
     * 读取Blob对象内容
     *
     * @param repository Repository对象
     * @param blobId     Blob对象ID
     * @return 文件内容(UTF-8编码)
     */
    private String readBlobContent(Repository repository, ObjectId blobId) throws IOException {
        // 创建ObjectReader用于从Git对象数据库读取数据
        try (ObjectReader objectReader = repository.newObjectReader()) {
            // 使用ObjectLoader加载Blob对象
            ObjectLoader loader = objectReader.open(blobId);
            // 读取Blob对象的原始字节数据
            byte[] bytes = loader.getBytes();
            // 将字节数组转换为UTF-8编码的字符串
            return new String(bytes, StandardCharsets.UTF_8);
        }
    }
}

3.3、JavaParserUtil

创建配置 Symbol Solver 的 JavaParser 实例,用于解析类的完整类型(包含项目中依赖的三方jar包)

java 复制代码
package com.bilibili.utils.jp;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import lombok.extern.slf4j.Slf4j;

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

/**
 * JavaParser 工具类
 * <p>
 * 用于创建配置了 Symbol Solver 的 JavaParser 实例,
 * 支持自动扫描多模块项目源码和 Maven 依赖,实现完整的类型推导功能。
 * </p>
 */
@Slf4j
public final class JavaParserUtil {

    private JavaParserUtil() {
        throw new AssertionError("Utility class should not be instantiated");
    }

    /**
     * 创建配置了 Symbol Solver 的 JavaParser 实例
     * <p>
     * Symbol Solver 用于解析 Java 代码中的类型引用,将简单类名(如 User)
     * 推导为完全限定类名(如 com.example.domain.User),支持:
     * <ul>
     *     <li>Java 标准库类型(通过 ReflectionTypeSolver)</li>
     *     <li>项目源码类型(通过 JavaParserTypeSolver,自动扫描所有 src/main/java 目录)</li>
     *     <li>Maven 依赖类型(通过 JarTypeSolver,扫描 ~/.m2/repository 中的 jar 包)</li>
     * </ul>
     * </p>
     *
     * @param projectRootPath 项目根目录路径(绝对路径)
     * @return 配置好的 JavaParser 实例,如果初始化失败返回 null
     */
    public static JavaParser javaParserSymbolSolver(String projectRootPath) {
        try {
            // 步骤1: 创建 CombinedTypeSolver(组合类型解析器,用于组合多个 TypeSolver,按添加顺序查找类型)
            CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver();

            // 步骤2: 添加 ReflectionTypeSolver(Java 标准库解析器)
            combinedTypeSolver.add(new ReflectionTypeSolver());

            // 步骤3: 添加 JavaParserTypeSolver(项目源码解析器)
            addMultiModuleSourcePaths(combinedTypeSolver, projectRootPath);

            // 步骤4: 添加 Maven 依赖(新增)
            addMavenDependencies(combinedTypeSolver);

            // 步骤5: 创建 JavaSymbolSolver(JavaParser 的符号解析器,当解析代码时遇到类型引用,就通过 TypeSolver 查找类型定义)
            JavaSymbolSolver symbolSolver = new JavaSymbolSolver(combinedTypeSolver);

            // 步骤6: 创建 ParserConfiguration(JavaParser 的配置对象) 并设置 SymbolResolver
            ParserConfiguration config = new ParserConfiguration();
            config.setSymbolResolver(symbolSolver);

            log.info("Symbol Solver 初始化成功");
            return new JavaParser(config);
        } catch (Exception e) {
            // 捕获所有异常,避免初始化失败导致程序崩溃
            log.error("Symbol Solver 初始化失败: {}", e.getMessage());
            return null;
        }
    }

    /**
     * 自动扫描并添加多模块源码路径到 CombinedTypeSolver
     *
     * <ol>
     *   <li>从项目根目录开始,递归遍历文件树(最多 5 层深度)</li>
     *   <li>识别所有符合 Maven 标准目录结构的源码路径(src/main/java)</li>
     *   <li>为每个源码路径创建 JavaParserTypeSolver 并添加到 CombinedTypeSolver</li>
     * </ol>
     *
     * @param solver      CombinedTypeSolver 实例,用于组合多个 TypeSolver
     * @param projectRoot 项目根路径(绝对路径,如 "D:\IdeaProject\crm")
     */
    private static void addMultiModuleSourcePaths(CombinedTypeSolver solver, String projectRoot) {
        try {
            // 步骤1: 转换项目根路径为 Path 对象
            Path rootPath = Paths.get(projectRoot);

            // 步骤2: 递归遍历文件树
            // Files.walk(path, maxDepth) 创建一个深度优先的文件树遍历流(注意: 必须使用 try-with-resources 关闭流,避免文件句柄泄漏)
            //   - 参数1: rootPath - 遍历起点
            //   - 参数2: 4 - 最大深度限制
            //     深度说明:
            //       - 0 = 只包含 rootPath 本身
            //       - 1 = rootPath 及其直接子目录
            //       - 4 = 足够覆盖 projectRoot/module/src/main/java(深度为4)
            //   - 返回值: Stream<Path> - 包含所有遍历到的路径(文件+目录)
            try (Stream<Path> paths = Files.walk(rootPath, 4)) {
                // 步骤3: 过滤出符合条件的源码目录
                paths
                        // 过滤器1: 识别 src/main/java 目录
                        // path.endsWith("src/main/java") 检查路径是否以 "src/main/java" 结尾
                        .filter(path -> path.endsWith("src/main/java") || path.endsWith("src\\main\\java"))

                        // 过滤器2: 确保是目录而非文件
                        // Files.isDirectory(path) 检查路径是否为目录
                        .filter(path -> Files.isDirectory(path))

                        // 步骤4: 为每个源码目录创建 TypeSolver
                        .forEach(srcPath -> {
                            try {
                                // 创建 JavaParserTypeSolver
                                JavaParserTypeSolver typeSolver = new JavaParserTypeSolver(srcPath.toFile());

                                // 添加到 CombinedTypeSolver
                                solver.add(typeSolver);
                                log.info("已添加源码路径: {}", srcPath);

                            } catch (Exception e) {
                                log.warn("添加源码路径失败: {} - {}", srcPath, e.getMessage());
                            }
                        });
            }

        } catch (IOException e) {
            log.error("扫描源码路径失败: {}", e.getMessage());
        }
    }

    /**
     * 添加 Maven 本地仓库中的依赖到 CombinedTypeSolver
     * <p>
     * 处理流程:
     * <ol>
     *     <li>查找 Maven 本地仓库路径(默认为 ~/.m2/repository)</li>
     *     <li>递归扫描仓库目录,添加所有 jar 文件到 TypeSolver</li>
     * </ol>
     * </p>
     * <p>
     * <b>性能考虑</b>:Maven 仓库可能包含数千个 jar 文件,
     * 加载所有 jar 会显著增加 Symbol Solver 的初始化时间和内存占用。
     * </p>
     *
     * @param solver      CombinedTypeSolver 实例
     */
    private static void addMavenDependencies(CombinedTypeSolver solver) {
        try {
            // Maven 本地仓库路径(通常在用户目录下)
            String userHome = System.getProperty("user.home");
            Path m2Repo = Paths.get(userHome, ".m2", "repository");
            log.info("Maven Repository: {}", m2Repo.toAbsolutePath());

            if (!Files.exists(m2Repo)) {
                log.warn("Maven 本地仓库不存在: {}", m2Repo);
                return;
            }

            // 扫描本地依赖
            addJarsFromDirectory(solver, m2Repo);

        } catch (Exception e) {
            log.error("添加 Maven 依赖失败: {}", e.getMessage());
        }
    }

    /**
     * 从指定目录递归扫描并添加所有 jar 文件到 CombinedTypeSolver
     * <p>
     * 处理流程:
     * <ol>
     *     <li>检查目录是否存在</li>
     *     <li>递归遍历目录(最多 10 层深度)</li>
     *     <li>找出所有 .jar 文件</li>
     *     <li>为每个 jar 文件创建 JarTypeSolver 并添加到 CombinedTypeSolver</li>
     * </ol>
     * </p>
     * <p>
     * <b>注意</b>:此方法会静默忽略无法解析的 jar 文件,只在日志中输出警告信息。
     * </p>
     *
     * @param solver    CombinedTypeSolver 实例
     * @param directory 要扫描的目录路径
     */
    private static void addJarsFromDirectory(CombinedTypeSolver solver, Path directory) {
        if (!Files.exists(directory)) {
            return;
        }

        try (Stream<Path> paths = Files.walk(directory, 10)) {
            paths
                    .filter(path -> path.toString().endsWith(".jar"))
                    .forEach(jarPath -> {
                        try {
                            solver.add(new JarTypeSolver(jarPath.toString()));
                            // log.info("已添加依赖: {}", jarPath.getFileName());
                        } catch (Exception e) {
                            log.warn("添加 jar 失败: {} - {}", jarPath, e.getMessage());
                        }
                    });
        } catch (IOException e) {
            log.error("扫描 jar 目录失败: {}", e.getMessage());
        }
    }
}

3.4、AstUtil

主要提供 AST 语法树的语义级别对比功能

java 复制代码
package com.bilibili.utils.ast;

import com.bilibili.enums.ChangeType;
import com.bilibili.models.*;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.*;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

/**
 * AST 差异对比工具类
 * <p>
 * 提供 AST 语法树的语义级别对比功能,包括:
 * <ul>
 *     <li>对比两个 CompilationUnit(文件级别)的差异</li>
 *     <li>对比两个类声明的差异(类、方法、字段、注解、继承等)</li>
 *     <li>对比方法、构造函数、字段的差异</li>
 *     <li>对比注解、接口实现、继承关系的差异</li>
 * </ul>
 * </p>
 * <p>
 * 所有对比方法使用 Symbol Solver 进行类型推导,生成语义级别的差异信息。
 * </p>
 */
@Slf4j
public final class AstUtil {

    private AstUtil() {
        throw new AssertionError("Utility class should not be instantiated");
    }

    public static void compareAst(FileDiff diff, CompilationUnit oldAst, CompilationUnit newAst) {
        if (oldAst == null && newAst == null) {
            diff.setChangeType(ChangeType.NONE);
        } else if (oldAst == null) {
            diff.setChangeType(ChangeType.ADDED);
            DiffUtil.astDiff(diff, newAst, ChangeType.ADDED);
        } else if (newAst == null) {
            diff.setChangeType(ChangeType.DELETED);
            DiffUtil.astDiff(diff, oldAst, ChangeType.DELETED);
        } else {
            // 步骤1:提取旧版本和新版本的所有类
            Map<String, ClassOrInterfaceDeclaration> oldClasses = ExtractUtil.extractClasses(oldAst);
            Map<String, ClassOrInterfaceDeclaration> newClasses = ExtractUtil.extractClasses(newAst);

            // 步骤2:合并所有类名,包含所有需要对比的类名
            // 使用 LinkedHashSet 保持插入顺序(便于调试和生成报告)
            Set<String> ClassSignatures = new LinkedHashSet<>();
            ClassSignatures.addAll(oldClasses.keySet());
            ClassSignatures.addAll(newClasses.keySet());

            // 步骤3:逐个对比每个类的差异
            for (String classSignature : ClassSignatures) {
                // 从旧版本的 Map 中获取类声明,如果类在旧版本中不存在,oldClass 为 null(表示新增的类)
                ClassOrInterfaceDeclaration oldClass = oldClasses.get(classSignature);

                // 从新版本的 Map 中获取类声明,如果类在新版本中不存在,newClass 为 null(表示删除的类)
                ClassOrInterfaceDeclaration newClass = newClasses.get(classSignature);

                // 对比单个类进行差异对比
                ClassDiff classDiff = compareClass(classSignature, oldClass, newClass);

                // 更新将类的对比结果
                if (classDiff.getChangeType() != ChangeType.NONE) {
                    diff.getClassDiffs().add(classDiff);
                }
            }
        }
    }

    /**
     * 对比两个类的差异(语义级别)
     *
     * @param classSignature 类的完全限定名(从 extractClassesWithResolution 获取)
     * @param oldClass       旧版本的类声明(如果为 null,表示新增的类)
     * @param newClass       新版本的类声明(如果为 null,表示删除的类)
     * @return ClassDiff 对象,包含类的完整差异信息
     */
    public static ClassDiff compareClass(String classSignature, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        // 步骤1:创建 ClassDiff 对象
        ClassDiff classDiff = new ClassDiff();
        classDiff.setResolvedClassName(classSignature);

        // 步骤2:判断类的变化类型并执行相应的分析,根据 oldClass 和 newClass 的组合,分为四种情况:
        if (oldClass == null && newClass == null) {
            // 场景1:新旧版本的 AST 中都没有这个类
            classDiff.setChangeType(ChangeType.NONE);
        } else if (oldClass == null) {
            // 场景2:新增类,旧版本的 AST 中没有这个类(oldClass == null),新版本的 AST 中有这个类(newClass != null)
            classDiff.setChangeType(ChangeType.ADDED);
            classDiff.setClassName(newClass.getNameAsString());

            // 提取新类的所有方法和所有字段
            DiffUtil.classDiff(classDiff, newClass, ChangeType.ADDED);
        } else if (newClass == null) {
            // 场景3:删除类,旧版本的 AST 中有这个类(oldClass != null),新版本的 AST 中没有这个类(newClass == null)
            classDiff.setChangeType(ChangeType.DELETED);
            classDiff.setClassName(oldClass.getNameAsString());

            // 提取旧类的所有方法和所有字段
            DiffUtil.classDiff(classDiff, oldClass, ChangeType.DELETED);
        } else {
            // 场景4:可能是修改类,旧版本的 AST 中有这个类(oldClass != null),新版本的 AST 中也有这个类(newClass != null)
            // 对比方法的差异
            boolean methodChanged = compareMethod(classDiff, oldClass, newClass);

            // 对比构造函数差异
            boolean constructorMethodChanged = compareConstructorMethod(classDiff, oldClass, newClass);

            // 对比字段的差异
            boolean fieldChanged = compareField(classDiff, oldClass, newClass);

            // 对比注解的差异
            boolean annotationChanged = compareAnnotation(classDiff, oldClass, newClass);

            // 对比接口实现的差异
            boolean implementChanged = compareImplement(classDiff, oldClass, newClass);

            // 对比接口继承的差异
            boolean extendChanged = compareExtend(classDiff, oldClass, newClass);

            if (methodChanged || constructorMethodChanged || fieldChanged || annotationChanged || implementChanged || extendChanged) {
                classDiff.setChangeType(ChangeType.MODIFIED);
            } else {
                classDiff.setChangeType(ChangeType.NONE);
            }
        }
        return classDiff;
    }

    /**
     * 对比两个类的方法差异(语义级别)
     * <p>
     * <b>注意</b>:此方法会同时对比普通方法和构造函数
     * </p>
     *
     * @param classDiff ClassDiff 对象,用于存储方法差异信息
     * @param oldClass  旧版本的类声明
     * @param newClass  新版本的类声明
     */
    public static boolean compareMethod(ClassDiff classDiff, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        boolean changed = false;
        // 步骤1:提取旧类和新类的所有普通方法
        Map<String, MethodDeclaration> oldMethods = ExtractUtil.extractMethods(oldClass);
        Map<String, MethodDeclaration> newMethods = ExtractUtil.extractMethods(newClass);

        // 步骤2:合并所有方法签名
        Set<String> methodSignatures = new LinkedHashSet<>();
        methodSignatures.addAll(oldMethods.keySet());
        methodSignatures.addAll(newMethods.keySet());

        // 步骤3:逐个对比每个方法
        for (String methodSignature : methodSignatures) {
            MethodDeclaration oldMethod = oldMethods.get(methodSignature);
            MethodDeclaration newMethod = newMethods.get(methodSignature);

            // 创建 MethodDiff 对象记录方法差异
            MethodDiff methodDiff = new MethodDiff();
            methodDiff.setMethodSignature(methodSignature);

            // 判断方法的变化类型
            if (oldMethod == null && newMethod == null) {
                // 场景1:新旧类中都不存在
                methodDiff.setChangeType(ChangeType.NONE);
            } else if (oldMethod == null) {
                // 场景2:新增方法(旧类中不存在,新类中存在)
                methodDiff.setChangeType(ChangeType.ADDED);

                // 记录新方法的源码
                methodDiff.setNewBody(NormalizeUtil.normalizeMethodString(newMethod));
            } else if (newMethod == null) {
                // 场景3:删除方法(旧类中存在,新类中不存在)
                methodDiff.setChangeType(ChangeType.DELETED);

                // 记录旧方法的完整源码(供代码审查和影响分析使用)
                methodDiff.setOldBody(NormalizeUtil.normalizeMethodString(oldMethod));
            } else {
                // 场景4:可能修改的方法(旧类和新类中都存在)
                String oldBodyNormalized = NormalizeUtil.normalizeMethodString(oldMethod);
                String newBodyNormalized = NormalizeUtil.normalizeMethodString(newMethod);
                boolean bodyChanged = !oldBodyNormalized.equals(newBodyNormalized);

                // 对比注解是否有变化
                boolean annotationsChanged = AnnotationUtil.hasMethodAnnotationChange(oldMethod, newMethod);

                if (bodyChanged || annotationsChanged) {
                    // 方法体或注解发生了变化,标记为修改
                    methodDiff.setChangeType(ChangeType.MODIFIED);
                    methodDiff.setOldBody(oldMethod.toString());
                    methodDiff.setNewBody(newMethod.toString());
                } else {
                    // 场景5:方法体和注解都未变化,跳过本次循环
                    methodDiff.setChangeType(ChangeType.NONE);
                }
            }
            if (methodDiff.getChangeType() != ChangeType.NONE) {
                // 将方法差异添加到类差异对象,只有发生了变化的方法才会被添加(ADDED、DELETED、MODIFIED)
                classDiff.getMethodDiffs().add(methodDiff);
                changed = true;
            }
        }
        return changed;
    }

    /**
     * 对比两个类的构造函数差异(语义级别)
     *
     * @param classDiff ClassDiff 对象,用于存储方法差异信息
     * @param oldClass  旧版本的类声明
     * @param newClass  新版本的类声明
     */
    public static boolean compareConstructorMethod(ClassDiff classDiff, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        boolean changed = false;
        // 步骤1:提取旧类和新类的所有构造函数
        Map<String, ConstructorDeclaration> oldConstructorMethod = ExtractUtil.extractConstructorMethods(oldClass);
        Map<String, ConstructorDeclaration> newConstructorMethod = ExtractUtil.extractConstructorMethods(newClass);

        // 步骤2:合并所有构造函数签名
        Set<String> constructorMethodSignatures = new LinkedHashSet<>();
        constructorMethodSignatures.addAll(oldConstructorMethod.keySet());
        constructorMethodSignatures.addAll(newConstructorMethod.keySet());

        // 步骤3:逐个对比每个构造函数
        for (String constructorMethodSignature : constructorMethodSignatures) {
            ConstructorDeclaration oldConstructor = oldConstructorMethod.get(constructorMethodSignature);
            ConstructorDeclaration newConstructor = newConstructorMethod.get(constructorMethodSignature);

            // 创建 MethodDiff 对象记录构造函数差异(注意:构造函数也使用 MethodDiff)
            MethodDiff methodDiff = new MethodDiff();
            methodDiff.setMethodSignature(constructorMethodSignature);

            // 判断构造函数的变化类型
            if (oldConstructorMethod == null && newConstructorMethod == null) {
                // 场景1:新旧类中都不存在
                methodDiff.setChangeType(ChangeType.NONE);
            } else if (oldConstructor == null) {
                // 场景1:新增构造函数(旧类中不存在,新类中存在)
                methodDiff.setChangeType(ChangeType.ADDED);

                // 记录新构造函数的完整源码
                methodDiff.setNewBody(newConstructor.toString());
            } else if (newConstructor == null) {
                // 场景2:删除构造函数(旧类中存在,新类中不存在)
                methodDiff.setChangeType(ChangeType.DELETED);

                // 记录旧构造函数的完整源码(供代码审查和影响分析使用)
                methodDiff.setOldBody(oldConstructor.toString());
            } else {
                // 场景3:可能修改的构造函数(旧类和新类中都存在)
                // 使用语义级别对比,忽略格式化和注释的变化
                String oldBodyNormalized = NormalizeUtil.constructorMethodNormalize(oldConstructor);
                String newBodyNormalized = NormalizeUtil.constructorMethodNormalize(newConstructor);
                boolean bodyChanged = !oldBodyNormalized.equals(newBodyNormalized);

                // 对比注解是否有变化
                boolean annotationsChanged = AnnotationUtil.hasConstructorMethodAnnotationChange(oldConstructor, newConstructor);

                if (bodyChanged || annotationsChanged) {
                    // 构造函数体或注解发生了变化,标记为修改
                    methodDiff.setChangeType(ChangeType.MODIFIED);
                    methodDiff.setOldBody(oldConstructor.toString());
                    methodDiff.setNewBody(newConstructor.toString());
                } else {
                    // 场景4:构造函数体和注解都未变化,跳过本次循环
                    methodDiff.setChangeType(ChangeType.NONE);
                    continue;
                }
            }
            if (methodDiff.getChangeType() != ChangeType.NONE) {
                // 将构造函数差异添加到类差异对象,只有发生了变化的构造函数才会被添加(ADDED、DELETED、MODIFIED)
                classDiff.getMethodDiffs().add(methodDiff);
                changed = true;
            }
        }
        return changed;
    }

    /**
     * 对比两个类的字段差异(语义级别,带类型推导)
     *
     * @param classDiff ClassDiff 对象,用于存储字段差异信息
     * @param oldClass  旧版本的类声明
     * @param newClass  新版本的类声明
     */
    public static boolean compareField(ClassDiff classDiff, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        boolean changed = false;
        // 步骤1:提取旧类和新类的所有字段
        Map<String, VariableDeclarator> oldFields = ExtractUtil.extractFields(oldClass);
        Map<String, VariableDeclarator> newFields = ExtractUtil.extractFields(newClass);

        // 步骤2:合并所有字段名
        Set<String> fieldNames = new LinkedHashSet<>();
        fieldNames.addAll(oldFields.keySet());
        fieldNames.addAll(newFields.keySet());

        // 步骤3:逐个对比每个字段
        for (String fieldName : fieldNames) {
            // 从 Map 中获取字段信息,如果字段不存在,返回 null
            VariableDeclarator oldVar = oldFields.get(fieldName);
            VariableDeclarator newVar = newFields.get(fieldName);

            // 创建 FieldDiff 对象记录字段差异
            FieldDiff fieldDiff = new FieldDiff();

            // 判断字段的变化类型
            fieldDiff.setChangeType(ChangeType.NONE);
            if (oldVar == null && newVar == null) {
                fieldDiff.setChangeType(ChangeType.NONE);
            } else if (oldVar == null) {
                // 场景1:新增字段(旧类中不存在,新类中存在)
                fieldDiff.setFieldName(classDiff.getResolvedClassName() + "#" + newVar.getNameAsString());
                fieldDiff.setChangeType(ChangeType.ADDED);


                // 设置字段的完全限定类型
                fieldDiff.setFieldType(ResolveTypeUtil.resolveVariableDeclaratorType(newVar));
            } else if (newVar == null) {
                // 场景2:删除字段(旧类中存在,新类中不存在)
                fieldDiff.setFieldName(classDiff.getResolvedClassName() + "#" + oldVar.getNameAsString());
                fieldDiff.setChangeType(ChangeType.DELETED);

                // 设置 oldType(用于报告中显示"删除了什么类型的字段")
                fieldDiff.setOldFieldType(ResolveTypeUtil.resolveVariableDeclaratorType(oldVar));
            } else {
                // 场景3:可能修改的字段(旧类和新类中都存在)
                String oldVarType = ResolveTypeUtil.resolveVariableDeclaratorType(oldVar);
                String newVarType = ResolveTypeUtil.resolveVariableDeclaratorType(newVar);
                boolean typeChanged = !oldVarType.equals(newVarType);

                // 检查字段注解是否有变化
                boolean annotationsChanged = AnnotationUtil.hasFieldAnnotationChange(oldClass, newClass, fieldName);

                if (typeChanged || annotationsChanged) {
                    // 字段类型或注解发生了变化,标记为修改
                    fieldDiff.setFieldName(classDiff.getResolvedClassName() + "#" + newVar.getNameAsString());
                    fieldDiff.setChangeType(ChangeType.MODIFIED);

                    // 记录新类型(完全限定)
                    fieldDiff.setFieldType(ResolveTypeUtil.resolveVariableDeclaratorType(newVar));

                    // 记录旧类型(完全限定)
                    fieldDiff.setOldFieldType(oldVarType);
                }
            }

            if (fieldDiff.getChangeType() != ChangeType.NONE) {
                // 将字段差异添加到类差异对象,只有发生了变化的字段才会被添加(ADDED、DELETED、MODIFIED)
                classDiff.getFieldDiffs().add(fieldDiff);
                changed = true;
            }
        }
        return changed;
    }

    /**
     * 对比类/接口注解变化(字面量级别)
     *
     * @param classDiff ClassDiff 对象,用于存储注解变化信息
     * @param oldClass  旧版本的类声明
     * @param newClass  新版本的类声明
     */
    public static boolean compareAnnotation(ClassDiff classDiff, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        boolean changed = AnnotationUtil.hasClassAnnotationChange(classDiff, oldClass, newClass);
        classDiff.setAnnotationChanged(changed);
        return changed;
    }

    /**
     * 对比接口实现的变化(语义级别)
     *
     * @param classDiff ClassDiff 对象,用于存储接口变化信息
     * @param oldClass  旧版本的类声明
     * @param newClass  新版本的类声明
     */
    public static boolean compareImplement(ClassDiff classDiff, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        // 提取旧类和新类实现的所有接口
        Set<String> oldInterfaces = ExtractUtil.extractImplements(oldClass);
        Set<String> newInterfaces = ExtractUtil.extractImplements(newClass);

        boolean changed = !oldInterfaces.equals(newInterfaces);
        classDiff.setImplementChanged(changed);
        return changed;
    }

    /**
     * 对比接口继承的变化(语义级别)
     *
     * @param classDiff ClassDiff 对象,用于存储接口变化信息
     * @param oldClass  旧版本的类声明
     * @param newClass  新版本的类声明
     */
    public static boolean compareExtend(ClassDiff classDiff, ClassOrInterfaceDeclaration oldClass, ClassOrInterfaceDeclaration newClass) {
        // 提取旧类和新类实现的所有接口
        Set<String> oldInterfaces = ExtractUtil.extractExtends(oldClass);
        Set<String> newInterfaces = ExtractUtil.extractExtends(newClass);

        boolean changed = !oldInterfaces.equals(newInterfaces);
        classDiff.setImplementChanged(changed);
        return changed;
    }
}

3.5、AstAnalyzer

封装了 AST 解析和文件对比的核心逻辑

java 复制代码
package com.bilibili;

import com.bilibili.models.FileDiff;
import com.bilibili.utils.ast.AstUtil;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * AST 语法树分析器
 * <p>
 * 封装了 AST 解析和文件对比的核心逻辑,用于:
 * <ul>
 *     <li>从 Git 仓库读取文件并解析为 AST 语法树</li>
 *     <li>对比两个 Git 引用中同一文件的语义差异</li>
 * </ul>
 * </p>
 * <p>
 * 此类依赖 {@link GitReader} 读取文件内容,使用配置了 Symbol Solver 的 {@link JavaParser} 进行类型推导。
 * </p>
 */
@Data
@Slf4j
@AllArgsConstructor
@NoArgsConstructor
public class AstAnalyzer {
    private GitReader gitReader;
    private JavaParser javaParser;

    /**
     * 从指定 Git引用 读取文件并解析为 AST 语法树
     *
     * @param ref      Git引用
     * @param filePath 文件路径
     * @return CompilationUnit (AST 根节点),如果文件不存在或解析失败返回 null
     */
    public CompilationUnit parseJavaFile(String ref, String filePath) {
        try {
            // 步骤1: 使用 GitUtil 从 Git引用 读取文件内容
            String fileContent = gitReader.readJavaFile(ref, filePath);
            if (fileContent == null) {
                return null;
            }

            // 步骤2: 使用配置了 Symbol Solver 的 JavaParser 解析文件
            return javaParser.parse(fileContent).getResult().orElse(null);
        } catch (Exception e) {
            log.error("解析失败: {} from {} - {}", filePath, ref, e.getMessage());
            return null;
        }
    }

    /**
     * 对比两个 Git引用 中的同一个文件(语义级别)
     *
     * @param oldRef   旧 Git引用
     * @param newRef   新 Git引用
     * @param filePath 文件路径
     * @return FileDiff 对象
     */
    public FileDiff compareFile(String oldRef, String newRef, String filePath) {
        // 解析两个版本的 AST 语法树
        CompilationUnit oldAst = parseJavaFile(oldRef, filePath);
        CompilationUnit newAst = parseJavaFile(newRef, filePath);

        // 创建差异对象
        FileDiff diff = new FileDiff(filePath, oldRef, newRef, oldAst, newAst);

        // 分析新旧语法树
        AstUtil.compareAst(diff, oldAst, newAst);

        return diff;
    }
}

3.6、ImpactAnalyzer

通过多轮迭代分析找出代码变更的完整影响范围,注意该功能目前不完整,无法处理通过依赖注入方式导致的影响范围

java 复制代码
package com.bilibili;

import com.bilibili.enums.ChangeType;
import com.bilibili.models.ClassDiff;
import com.bilibili.models.FileDiff;
import com.bilibili.utils.ast.ControllerUtil;
import com.bilibili.utils.ast.ExtractUtil;
import com.bilibili.utils.ast.ImportUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.*;

/**
 * 影响范围分析器
 * <p>
 * 通过多轮迭代分析找出代码变更的完整影响范围,核心功能:
 * <ul>
 *     <li>识别直接依赖变更类的文件</li>
 *     <li>递归分析间接影响的文件(依赖的依赖)</li>
 *     <li>标识受影响的 Controller 文件</li>
 * </ul>
 * </p>
 * <p>
 * 分析策略:
 * <ol>
 *     <li>提取变更文件中所有修改的类</li>
 *     <li>加载目标版本的所有 Java 文件到内存</li>
 *     <li>通过字符串匹配+AST 导入分析,迭代查找受影响的文件</li>
 *     <li>停止条件:某一轮没有发现新的受影响类</li>
 * </ol>
 * </p>
 * <p>
 * <b>注意</b>:此类会将所有 Java 文件加载到内存,对大型项目可能有性能问题。
 * </p>
 */
@Slf4j
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ImpactAnalyzer {
    private GitReader gitReader;
    private JavaParser javaParser;

    /**
     * 分析变更文件的影响范围(多轮迭代分析)
     * <p>
     * 核心流程:
     * <ol>
     *     <li>提取所有变更的类(包括新增、删除、修改的类)</li>
     *     <li>加载目标版本的所有 Java 文件到内存</li>
     *     <li>迭代分析影响范围,直到没有新的受影响类为止:
     *         <ul>
     *             <li>第1轮:找出直接依赖变更类的文件</li>
     *             <li>第2轮:找出依赖第1轮受影响类的文件</li>
     *             <li>第N轮:找出依赖第N-1轮受影响类的文件</li>
     *         </ul>
     *     </li>
     *     <li>返回完整的影响范围映射(变更类 -> 受影响文件列表)</li>
     * </ol>
     * </p>
     * <p>
     * <b>性能考虑</b>:此方法会加载项目的所有 Java 文件到内存,
     * 对于大型项目(数千个文件)可能会导致内存占用过高。
     * </p>
     *
     * @param ref         目标 Git 引用(分支名或 commit hash)
     * @param changeFiles 变更文件映射(文件路径 -> FileDiff 对象)
     * @return 影响范围映射(变更类的完全限定名 -> 受影响文件列表)
     */
    public Map<String, ArrayList<String>> analyzeChangeFiles(String ref, Map<String, FileDiff> changeFiles) throws IOException {
        Map<String, ArrayList<String>> impactMap = new HashMap<>();
        ObjectMapper objectMapper = new ObjectMapper();

        Map<String, String> changedClasses = new HashMap<>();
        for (String changeFilePath : changeFiles.keySet()) {
            FileDiff fileDiff = changeFiles.get(changeFilePath);
            for (ClassDiff classDiff : fileDiff.getClassDiffs()) {
                if (classDiff.getChangeType() != ChangeType.NONE) {
                    changedClasses.put(classDiff.getResolvedClassName(), fileDiff.getFilePath());
                }
            }
        }
        Map<String, String> javaFiles = gitReader.javaFiles(ref, path -> true);
        log.info("全量 {} 文件加载完成!", javaFiles.size());

        Set<String> allFilePaths = new HashSet<>(changeFiles.keySet());
        int i = 0;
        while (!changedClasses.isEmpty()) {
            i++;
            log.info("第{}轮影响分析开始,变化类为: {}", i, objectMapper.writeValueAsString(changedClasses));
            changedClasses = analyzeChangeClasses(allFilePaths, javaFiles, changedClasses, impactMap);
        }
        log.info("全部影响分析完成,所有影响的文件为:{}", objectMapper.writeValueAsString(impactMap));

        return impactMap;
    }

    /**
     * 单轮影响范围分析(查找依赖指定类的文件)
     * <p>
     * 分析流程:
     * <ol>
     *     <li>为每个变更类提取简单类名和包名</li>
     *     <li>遍历所有 Java 文件,通过字符串匹配识别可能受影响的文件:
     *         <ul>
     *             <li>文件内容包含变更类的简单类名(如 "User")</li>
     *             <li>文件内容包含变更类的包名(如 "com.example")</li>
     *         </ul>
     *     </li>
     *     <li>解析受影响文件的 AST,检查是否通过 import 导入了变更类</li>
     *     <li>记录受影响的文件,并标识是否为 Controller</li>
     *     <li>提取受影响文件中定义的所有类,作为下一轮分析的输入</li>
     * </ol>
     * </p>
     * <p>
     * <b>已知问题</b>:使用字符串匹配识别受影响文件,会导致误报。
     * 例如,类名 "User" 会匹配到注释、字符串字面量中的 "User"。
     * </p>
     *
     * @param allFilePaths   所有已分析的文件路径集合(用于去重,避免重复分析)
     * @param javaFiles      所有 Java 文件映射(文件路径 -> 文件内容字符串)
     * @param changedClasses 本轮要分析的变更类映射(类的完全限定名 -> 变更类所在文件路径)
     * @param impactMap      影响范围映射(累积结果,会被修改)
     * @return 下一轮要分析的变更类映射(新发现的受影响类 -> 所在文件路径)
     */
    public Map<String, String> analyzeChangeClasses(Set<String> allFilePaths, Map<String, String> javaFiles, Map<String, String> changedClasses, Map<String, ArrayList<String>> impactMap) {
        Map<String, CompilationUnit> compilationUnitMap = new LinkedHashMap<>();
        Map<String, String> ccs = new HashMap<>();
        // 初始化 Map,为每个变化的类创建空集合
        for (String changedClass : changedClasses.keySet()) {
            impactMap.put(changedClass, new ArrayList<>());
        }

        // 拆解完全限定类名
        Set<String> classNames = ExtractUtil.extractClassNames(changedClasses.keySet());
        Set<String> PackageNames = ExtractUtil.extractPackageNames(changedClasses.keySet());

        // 使用拆解后的包名和类名快速过滤目标文件
        // - 1.全量解析文件很慢
        // - 2.全量调用图生成很慢
        for (String filePath : javaFiles.keySet()) {
            boolean f = false;
            for (String simpleClassName : classNames) {
                if (javaFiles.get(filePath).contains(simpleClassName)) {
                    f = true;
                }
            }
            for (String packageName : PackageNames) {
                if (javaFiles.get(filePath).contains(packageName)) {
                    f = true;
                }
            }
            if (f) {
                CompilationUnit compilationUnit = javaParser.parse(javaFiles.get(filePath)).getResult().orElse(null);
                compilationUnitMap.put(filePath, compilationUnit);
            }
        }

        // 分析目标ast是否真的导入了变化的类
        for (String filePath : compilationUnitMap.keySet()) {
            // 不要重复分析导致死循环
            if (allFilePaths.contains(filePath)) {
                continue;
            }
            CompilationUnit ast = compilationUnitMap.get(filePath);
            Set<String> imports = ExtractUtil.extractImports(ast);
            for (String changedClass : changedClasses.keySet()) {
                if (ImportUtil.isClassImported(changedClass, imports, ast)) {
                    Map<String, ClassOrInterfaceDeclaration> classes = ExtractUtil.extractClasses(ast);
                    boolean isController = ControllerUtil.isControllerAST(ast);
                    if (!isController) {
                        for (String s : classes.keySet()) {
                            ccs.put(s, filePath);
                        }
                    }
                    impactMap.get(changedClass).add(filePath);
                }
            }
        }
        return ccs;
    }
}

3.7、main

java 复制代码
package com.bilibili;

import com.bilibili.enums.ChangeType;
import com.bilibili.models.*;
import com.bilibili.utils.ast.CallGraphUtil;
import com.bilibili.utils.git.GitUtil;
import com.bilibili.utils.jp.JavaParserUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.javaparser.JavaParser;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import picocli.CommandLine;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;

/**
 * Java 代码变更影响分析工具 - 主入口类
 * <p>
 * 功能概述:
 * <ol>
 *     <li>对比两个 Git 版本之间的 Java 代码变更</li>
 *     <li>通过 AST 语义分析识别类/方法/字段的变化</li>
 * </ol>
 * </p>
 * <p>
 * 使用示例:
 * <pre>
 * java -jar target/analyzer.jar -p /Users/xumeng03/IdeaProject/crm -r1 6e54b5053ecd5b8f1388b76d29fe38338efc6cee -r2 8d21d8fa33fe96e4ab2d9accd169dba4ffa9511c
 * </pre>
 * </p>
 */
@Slf4j
@CommandLine.Command(name = "java_analyzer", description = "分析Java代码变更对接口的影响", mixinStandardHelpOptions = true, version = "0.0.1")
public class Main implements Callable<Integer> {
    @CommandLine.Option(names = {"-p", "--project"}, required = true, description = "Java项目根目录路径")
    private String projectPath;

    @CommandLine.Option(names = {"-r1", "--ref1"}, description = "引用1 (如: main、release/uat、2b5e7cd5、4deccf982ee42680c878e32a5a1d10601a0495c8)")
    private String ref1;

    @CommandLine.Option(names = {"-r2", "--ref2"}, description = "引用2 (如: main、release/uat、2b5e7cd5、4deccf982ee42680c878e32a5a1d10601a0495c8)")
    private String ref2;

    @CommandLine.Option(names = {"-v", "--verbose"}, description = "详细输出模式", defaultValue = "false")
    private boolean verbose;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Integer call() throws IOException {
        // 参数验证
        File projectDir = new File(projectPath);
        if (!projectDir.exists() || !projectDir.isDirectory()) {
            log.error("项目路径不存在或不是目录: {}", projectPath);
            return 1;
        }
        if (ref1 == null || ref2 == null) {
            log.error("引用信息缺失: {} {}", ref1, ref2);
            return 1;
        }

        // 初始化
        Git git = GitUtil.openRepository(projectPath);
        if (git == null) {
            log.error("OpenRepository failed!");
            throw new RuntimeException("OpenRepository failed!");
        }
        Repository repository = git.getRepository();
        GitReader gitReader = new GitReader(repository, ref1, ref2);
        JavaParser javaParser = JavaParserUtil.javaParserSymbolSolver(projectPath);
        AstAnalyzer astAnalyzer = new AstAnalyzer(gitReader, javaParser);
        ImpactAnalyzer impactAnalyzer = new ImpactAnalyzer(gitReader, javaParser);


        Set<String> changeJavaFilePaths = gitReader.changeJavaFilePaths(path -> true);
        if (verbose) {
            log.info("变动文件信息:{}", objectMapper.writeValueAsString(changeJavaFilePaths));
        }

        Map<String, FileDiff> changeFiles = new HashMap<>();
        for (String changeFilePath : changeJavaFilePaths) {
            FileDiff fileDiff = astAnalyzer.compareFile(ref1, ref2, changeFilePath);
            if (fileDiff.getChangeType() != ChangeType.NONE) {
                changeFiles.put(fileDiff.getFilePath(), fileDiff);
            }
        }
        if (verbose) {
            log.info("变动文件解析结果: {}", objectMapper.writeValueAsString(changeFiles));
        }

        Map<String, ArrayList<String>> impactFilePaths = impactAnalyzer.analyzeChangeFiles(ref2, changeFiles);
        if (verbose) {
            log.info("影响范围: {}", objectMapper.writeValueAsString(impactFilePaths));
        }

        // 关闭资源
        GitUtil.closeGit(git);

        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Main()).execute(args);
        System.exit(exitCode);
    }
}
相关推荐
Yvonne爱编码2 小时前
JAVA数据结构 DAY4-ArrayList
java·开发语言·数据结构
阿猿收手吧!2 小时前
【C++】C++原子操作:compare_exchange_weak详解
java·jvm·c++
Next_Tech_AI2 小时前
别用 JS 惯坏了鸿蒙
开发语言·前端·javascript·个人开发·ai编程·harmonyos
chillxiaohan2 小时前
GO学习记录——多文件调用
开发语言·学习·golang
2301_822366352 小时前
C++中的命令模式变体
开发语言·c++·算法
一刻钟.2 小时前
C#高级语法之线程与任务
开发语言·c#
csdn2015_3 小时前
MyBatis Generator 核心配置文件 generatorConfig.xml 完整配置项说明
java·mybatis
追逐梦想的张小年3 小时前
JUC编程03
java·开发语言·idea
派葛穆3 小时前
Python-PyQt5 安装与配置教程
开发语言·python·qt