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, 并
   127   128   129   130   131   132   133   134   135   136   137