Page 132 - 《软件学报》2025年第7期
P. 132
孙伟杰 等: Java 依赖异味的实证研究与统一检测技术 3053
度. 这对于诸如确定依赖库加载顺序等需求提供了重要的支持.
3) 依赖包集合. 依赖包集合是目标模块所有依赖库的具体构件组成的集合. 其中的每个元素对应了各个依赖
库的具体实现, 以 JAR 文件的形式存储. 相较于依赖树, 依赖包集合更加关注每个依赖库的具体实现, 而不是其层
次关系. 通过依赖包集合, 我们能够获取依赖库的具体实现, 进而深入分析其内容.
4) 调用图. 调用图表示目标模块对依赖库的使用情况. 在此, 我们仅关注目标模块对依赖库的调用, 其中调用
图中的节点对应依赖库. 如果模块中存在对某个依赖库的调用, 则会存在连接从目标模块到对应依赖库的边. 在我
们的调用图中, 存在 3 类调用边, 即构建调用、运行调用和测试调用, 它们分别代表目标模块在不同场景下对依赖
库的使用. 通过调用图我们可以从代码的角度分析依赖库的预期使用场景, 并将其与依赖库在配置文件中声明的
依赖范围所对应的实际使用场景进行对比. 值得注意的是, 调用图并不聚焦于具体的依赖范围, 而是关注 3 类主要
使用场景——构建、测试和运行. 这一选择源于 Maven 与 Gradle 在依赖范围定义上的差异. 为了将调用图与特定
的构建工具分离, 我们关注依赖范围的本质, 即对具体使用场景的划分. 例如, Maven 中的 provided 范围与 Gradle
的 compileOnly 范围虽然在名称上存在差异, 但二者均对应于构建和测试场景.
由于依赖模型中的数据之间存在一定依赖关系, 因此我们需要按照图 5 中的顺序来处理这些数据, 最终依赖
模型构建的流程如下. 首先, 我们提取配置文件, 分析其中对项目或模块的配置, 得到基本信息. 其次, 我们对配置
文件进行解析, 构建模块对应的依赖树. 然后, 我们对依赖树中的节点进行解析, 获取其对应的 JAR 包并组织成依
赖包集合. 接着, 根据基本信息中对应的项目, 找到模块对应的源代码、测试代码和编译后的字节码, 使用静态分
析来确定其中对外部库的调用, 这 3 类代码中对外部库的调用分别对应调用图中的构建调用、测试调用和运行调
用, 从而形成了调用图. 最后, 我们将这些数据综合并组织为依赖模型.
3.1.2 依赖异味分析
从图 5 中可以看出, 依赖异味分析流程主要分为两步: 首先, 我们在依赖模型之上运行依赖异味检测算法; 其
次, 我们对算法执行结果进行解析, 最终得到所有依赖异味. 我们针对每一类依赖异味设计了对应的依赖异味检测
算法, 它们有着不同的核心逻辑, 但都以依赖模型中的部分数据为输入. 我们将各类检测算法核心和所需依赖模型
中的数据列在表 8 中.
表 8 依赖异味检测算法简介
异味编号 核心逻辑 所需数据
1.1 判断模块构建产物和依赖库中的类是否存在交集 1, 3
1.2 判断依赖库之间类是否存在交集 3
1.3 判断依赖树中是否存在不同版本的同一依赖库 2
1.4 判断模块是否直接使用了传递依赖 2, 4
1.5 判断模块的直接依赖是否未被使用 2, 4
1.6 判断依赖库预期范围与实际范围是否一致 1, 2, 4
1.7 判断模块的直接依赖是否在依赖树中存在不同的范围 2
1.8 判断模块不同配置文件中依赖库版本是否一致 1, 2
2.1 判断项目对应路径下是否存在构建工具封装器脚本 1
2.2 判断项目对应路径下是否存在构建工具封装器 JAR 1
2.3 判断项目构建工具封装器 JAR 的校验和是否与官方一致 1
2.4 判断项目模块之间是否存在未统一管理版本的依赖库 1
2.5 判断项目模块之间是否声明不同版本的同一依赖库 1
由于篇幅限制, 我们以异味 1.6 依赖范围误用的检测算法 1 为例进行介绍, 更多检测算法见附录 A. 该算法接
受模块的依赖树 DT 和调用图 CG 作为输入, 并输出所有出现依赖范围误用的依赖库集合 JS. 算法的工作流程如
下: 首先, 初始化依赖库集合 JS, 并创建用于存储各个依赖库预期使用场景的哈希表 expectedScenesMap 以及模块
在调用图中对应的节点 source (第 1–3 行). 接下来, 算法遍历调用图 CG 中所有以 source 为起点的调用边 edge, 并

