我们做了一款静态代码分析程序

459 阅读14分钟

大家好,我是王有志。一个分享硬核 Java 技术的金融摸鱼侠。

各位小伙伴,好久不见啊,我王有志又回来了!今天开始我会回归到正常的更新节奏中,当下的计划是对之前的文章做一些修改和勘误,直到找到新的分享主题为止。

不过今天我先向大家汇报下最近的工作成果,就是之前在公众号中提到过的,和朋友一起搞的用于分析程序不同分支之间的改动,并基于改动生成方法调用链路的程序,我们叫它静态代码分析(CodeAnalysis,简称 CA)。

目前我们已经实现了核心功能,已经可以简单的使用了,所以先将 CA 开源出来(项目地址:gitee.com/wyz-A2/Code…),欢迎大家批评指正,提出宝贵意见。

免责声明

CA 是我们利用业余时间做的,没有进行过完整的测试和验证,程序中难免会存在着一些不完善的地方和 BUG.,遇到这类情况大家可以提 issue。需要注意,由于 CA 分析的是源码,所以提 issue 时不要泄漏生产环境的源代码。

另外,由于本人水平和精力有限,程序中难免会有设计不合理,实现不够优秀的地方,欢迎大家基于项目本身提出自己的想法和不同的见解,互相交流学习

最后,CA 处于早期版本,无法做到面面俱到,目前仅能够支持分析通过 Git 进行版本控制,基于 Maven 构建的 Java 项目

CA 是如何诞生的

最初,和朋友聊天的时候提到,我们的“屎山”终于要重构了,但是由于经年累月的积累和人员的变动,公司内部已经没有人能够摸透“屎山”的脉络了,这给我们在分析项目时带来了极大的困扰,如果能够通过系统自动分析接口中方法的调用链路就好了。

朋友也提到,他们最近出现了很多因代码改动分析不足导致的问题,举个例子,假如方法 A,方法 B,方法 C 都调用了方法 D,这次修改了方法 D,但是由于分析不充分,只分析到了方法 D 对方法 A 和方法 B 的影响,忽略了方法 C,这可能会导致方法 C 执行异常,对于这种场景,通过人去保障是不可靠的,如果能够通过系统去分析就好了。

针对于上述两种场景,我们提出了一个想法,利用系统去分析项目中方法的调用链路。在我们的设想中,这个系统应该具备两个核心功能:

  • 根据分支间的差异,解析到本次修改的方法,并分析出方法的调用链路;
  • 通过指定类型和方法,分析该类型中的指定方法,在程序中的调用链路。

我们先尝试着搜索了网上已经开源的项目,确实发现了几款类似的开源工具,但整体看下来不太能够满足我们的需求,于是我们决定自己做一个。

CA 的定位与能力

在我们的设想中,CA 应该是作为公司 DevoOps 自动化流程中的一环,能够在开发前由开发人员制定方法分析方法的调用链路以确定程序修改的影响范围,在 Code Revie 环节和 QA 环节前,系统自动分析修程序修改后所影响的方法调用链路,辅助 Code Review 环节以及验证测试案例的覆盖是否全面。

明确了 CA 的定位后,我们开始思考 CA 应该具备哪些能力。不同的公司在技术栈的应用上会有所差异,最基础的如版本控制软件的选择上,互联网企业中最主流的应该就是 Git 了,但 SVN 在传统企业中占比也不少;另外,在编程语言上,虽然我们的主要业务都是跑在 Java 上的,但是我也需要写一些 Python 处理数据,公司其它的门也有其它语言的应用。

因此,在我们的设想中,CA 应该要能够支持多种版本控制软件,多种编程语言,多种项目构建方式。基于我们设想的 CA 的定位与能力,我们又提出了一些要求:

  • 不侵入业务系统,CA 作为代码分析工具,它不应该侵入到业务系统中
  • 自动执行,能够支持多种项目管理工具,并集成到项目管理的流程中
  • 多种结果输出形式,能够支持不同的结果输出形式,如,文本,思维导图等形式

因为 CA 不涉及到实际业务,仅仅只是 DevoOps 中的一环,所以对性能的要求非常宽松,甚至可以说是没有性能要求,我们只需要保证 CA 别太“慢”就好了。

当我们基于上述的定位与能力对 CA 进行设计时,我们发现我们把 CA 想的太“大”了,因为要支持各种各样的场景,所以在设计环节要考量的东西非常多了,导致我们的开发进度非常缓慢且痛苦。这也给了我们一个警醒,当实力与“野心”不匹配的时候,步子迈太大,容易扯着蛋~~

不过,在经历了各种痛苦的挣扎后,CA 已经初具雏形了,也算是能够拿出来献丑了。目前 CA 能够支持使用 Git 进行版本控制的,通过 Maven 构建的 Java 程序的两个分支间的差异分析,并生成类似于 Maven 解析依赖树的文字描述版的方法调用链路,如下图所示:

Tips: 关于各种字符的含义,我会在下文中详细解释。

CA 要如何使用

CA 的使用非常简单,只需要安装一些常用的软件,做一些简单的数据库配置,并调用接口即可,下面我们就一步一步的来配置并使用 CA。

关于软件部分,CA 目前依赖 Git,Maven 和 Java,CA 自身使用 Java 17 进行开发,使用 Maven 进行构建,除了 CA 自身使用的软件外,还需要根据待分析项目的软件环境安装对应的软件,例如,使用 CA 分析基于 Java 8 开发的项目,就需要额外安装 Java 8。不过这些基本上是每个 Java 开发者必备的软件了,因此我这里默认你已经具备了使用 CA 的基本软件环境了。

数据库配置

首先进行数据库配置,CA 使用的数据库是 MySQL,因为 SQL 语句非常简单,也没有使用高级特性,所以无论是 5.X 版本还是 8.X 版本都能够使用。目前 CA 依赖了 3 张表(未来会拓展输出形式,会额外添加相关的配置表),分别是:项目配置表,版本控制配置表,编译工具表。

项目配置表

用于配置项目的基本信息,建表 SQL 语句如下:

create table project_config (
    project_id           varchar(50)  not null primary key,
    project_name         varchar(50)  not null comment '项目名称',
    project_language     varchar(50)  null comment '项目使用的语言',
    language_version     varchar(50)  null comment '语言版本',
    build_tool           varchar(50)  not null comment '项目构建工具',
    build_tool_version   varchar(50)  null comment '构建工具版本',
    build_tool_config    varchar(500) null comment '编译工具配置文件绝对路径',
    project_build_config varchar(500) null comment '醒目中编译工具配置文件的相对路径',
    target_path          varchar(500) null comment '编译后项目相对路径',
    create_user          varchar(50)  not null,
    create_date          datetime     not null,
    update_user          varchar(50)  not null,
    update_date          timestamp    not null
) comment '项目配置';

解释下主要字段的作用:

  • project_id 字段和 project_name 字段,项目的 ID 和名称,这个根据实际情况配置即可。
  • project_language 字段,项目使用的语言,名称与枚举类 LanguageMapper 保持一致,用于确定对应语言的文件处理器和解析处理器;
  • language_version 字段,项目使用的语言的版本,编译环节会使用到,另外会通过 project_language 字段和 language_version 字段确定编译工具表中的工具;
  • build_tool 字段,项目使用的编译工具,名称与枚举类 BuildToolMapper 保持一致,用于确定项目使用的编译工具;
  • build_tool_version 字段,项目使用的编译工具的版本,通过 build_tool 字段和 build_tool_version 字段确定编译工具表中的工具;
  • build_tool_config 字段,编译工具配置文件的绝对路径,在使用 Maven 构建的项目中指的是 Maven 的 settings.xml 文件的绝对路径;
  • project_build_config 字段,项目中编译工具配置文件的相对路径,在 Maven 项目中,指的是 POM 文件相对于项目的路径,多模块的项目中,使用主项目的 POM 文件;
  • target_path 字段,项目编译后的输出文件的相对路径,在 Java 项目中,指的是 class 文件的相对路径,多模块项目中,指的是针对于每个模块的 class 文件的相对路径。

配置示例如下:

Tips:以上字段是基于分析通过 Maven 构建的 Java 项目设置的,会存在不合理的地方,这点我们会在引入其它语言,编译工具时进行修改~~

版本控制配置表

用于配置项目的版本控制信息,建表 SQL 语句如下:

create table vcs_config (
    vcs_config_id int auto_increment primary key,
    project_id    varchar(50)  not null comment '项目id',
    vcs_type      varchar(50)  not null comment '版本控制类型',
    vcs_url       varchar(255) not null comment '版本控制地址',
    vcs_user      varchar(50)  not null comment '版本控制用户名',
    vcs_password  varchar(50)  not null comment '版本控制密码',
    create_user   varchar(50)  not null,
    create_date   datetime     not null,
    update_user   varchar(50)  not null,
    update_date   timestamp    not null
) comment '版本控制配置';

解释下主要字段的作用:

  • vcs_config_id 字段,版本控制配置表的主键
  • project_id 字段,项目 ID 字段,与 project_config 表的 project_id 字段保持一致;
  • vcs_type 字段,项目使用的版本控制软件的类型,名称与枚举类 VcsMapper 保持一致,用于确定项目对应的版本控制处理器;
  • vcs_url 字段,项目远程仓库地址
  • vcs_user 字段,版本控制工具的账户
  • vcs_password 字段,版本控制工具的密码(私人令牌)

配置示例如下:

Tips:以上字段是基于 Git 设置的,会存在不合理的地方,这点我们会在引入其它版本控制工具时进行修改~~

编译工具表

用于配置构建(编译)项目时所使用的编译工具(软件),建表 SQL 语句如下:

create table compiler_tool (
    compiler_tool_id int auto_increment primary key,
    type             varchar(50)  not null comment '编译工具类型',
    name             varchar(50)  null comment '编译工具名称',
    version          varchar(50)  null comment '编译工具版本',
    home             varchar(255) null comment '编译工具地址',
    create_user      varchar(50)  null,
    create_date      datetime     null,
    update_user      varchar(50)  null,
    update_date      timestamp    null
) comment '编译工具表';

解释下主要字段的作用:

  • compiler_tool_id 字段,编译工具表的主键
  • type 字段,编译工具的类型,这里不仅仅会存储编译工具的配置,还会存储编程语言的配置,名称分别与枚举类 BuildToolMapper 和枚举类 LanguageMapper 保持一致;
  • name 字段,编译工具的名称,如果是编程语言的话,要和 枚举类 LanguageMapper 的名称保持一致,如果是编译工具的话,要和枚举类 BuildToolMapper 的名称保持一致;
  • version 字段,编译工具的版本
  • home 字段,编译工具的绝对路径

配置示例如下:

Tips:关于以上数据库的配置,请根据你的实际情况修改路径,版本,GIt 信息等。

修改程序配置

接着我们要修改 CA 项目中的数据库配置,配置文件位于 ca-app 模块中的 resources 目录下。

只需要修改数据库地址,用户名和密码即可。

执行分析

最后我们启动 CA 项目,启动类是 ca-app 模块中 CodeAnalysisApplication 类,当然你也可以部署后再启动,不过由于是早期版本我就没部署,直接在 IDEA 中启动了。CA 目前只提供了一个 HTTP 接口CodeAnalysisRest#analysis,接口声明位于 ca-api 模块中,实现位于 ca-core 模块中,请求地址为:/ca/rest/analysis。

请求报文

CodeAnalysisRest#analysis接口只需要 3 个参数:

  • 参数 projectId,是配置在 project_config 表中的 project_id 字段;
  • 参数 targetBranch,是需求分支名称,也就是发生变更的分支名称;
  • 参数 sourceBranch,是主分支名称。

示例如下:

{
  "projectId": "CA-001",
  "targetBranch": "test",
  "sourceBranch": "master"
}

响应报文

响应报文示例如下:

{
  "callChains": [
    {
      "branch": "test",
      "rootNodes": [
        {
          "callType": "O",
          "callee": [
            {
              "callType": "C",
              "callee": [],
              "changeType": "U",
              "classFullQualifiedName": "com.wyz.ca.infra.util.FileUtil",
              "descriptor": "(String,String)File",
              "methodFullName": "com.wyz.ca.infra.util.FileUtil#write(String,String)File",
              "methodName": "write",
              "methodType": "N",
              "recursion": false
            }
          ],
          "changeType": "C",
          "classFullQualifiedName": "com.wyz.ca.core.rest.CodeAnalysisRestImpl",
          "descriptor": "(String)void",
          "methodFullName": "com.wyz.ca.core.rest.CodeAnalysisRestImpl#test(String)void",
          "methodName": "test",
          "methodType": "N",
          "recursion": false
        }
      ]
    }
  ],
  "projectId": "CA-001",
  "projectLanguage": "Java",
  "projectName": "CodeAnalysis"
}

其中 callChains 是发生变更的方法所在的调用链路,callChains 是以多叉树的形式呈现的,下文会对多叉树的每个节点对的含义以及取值做详细的解释。除了接口响应数据外,还会生成类似于 Maven 分析依赖树风格的文字描述版文本文件。

如果将文字描述版的结果添加到接口响应中,会导致响应报文过大,因此我们将文字描述结果生成文件,计划是将文件上传到文件存储服务中,响应报文中添加文件地址,通过文件存储服务的地址进行访问。

调用链节点解析

再来解释下响应报文里,调用链的节点中每个字段的含义以及取值范围:

{
  "classFullQualifiedName": "com.wyz.ca.infra.util.FileUtil",
  "methodName": "write",
  "descriptor": "(String,String)File",
  "methodFullName": "com.wyz.ca.infra.util.FileUtil#write(String,String)File",
  "callType": "C",
  "changeType": "U",
  "methodType": "N",
  "recursion": false,
  "callee": []
}
  • classFullQualifiedName 字段,方法所在类型的全限名
  • methodName 字段,方法的名称
  • descriptor 字段,方法描述,括号内为参数列表,最后是返回值类型,无参数,无返回值时为“()void”;
  • methodFullName 字段,方法的全名,是上面 3 个字段合并后的结果;
  • callType 字段,表示该方法的被调用方式,目前有 3 个值(均为大写英文字母):
    • O,origin 的缩写,表示原点,即调用链路的起点,这个起点可以是 HTTP 接口,RPC 接口以及定时任务等等;
    • C,call 的缩写,表示方法调用,即上游方法中直接调用了该方法
    • I,implement 的缩写,表示接口实现,即该方法是接口实现,因为是静态代码分析,无法分析到运行时的动态调用,特别是通过配置实现的策略模式。
  • changeType 字段,表示该方法否发生修改,目前有 2 个值(均为大写英文字母):
    • U,unchanged 的缩写,表示未发生改变
    • C,changed 的缩写,表示发生改变
  • methodType 字段,表示方法的类型,目前有 3 个值(均为大写英文字母):
    • A,abstract 的缩写,表示该方法是抽象方法,如,接口中的方法,抽象类中的方法;
    • I,implement 的缩写,表示该方法是某个方法的实现,或者是某个方法的重写方法;
    • N,normal 的缩写,表示该方法是一个普通方法
  • recursion 字段,表示是否有递归调用
  • callee 字段,表示调用的方法,包括直接调用的方法(即 callType 为 C)和方法实现(即 callType 为 I)的。

最后,我再解释下文字版描述中的每个字段的含义:

绿色的部分都是分隔符,蓝色的部分除了开头的“+-”表示下游还有调用的方法,“-”表示下游没有调用的方法外,其余均与上面的响应报文中的字段的含义和取值保持一致。

好了今天的内容就到这里了,下一篇文章中,我会和大家分享 CA 的设计以及实现。我是分享硬核 Java 技术的金融摸鱼侠王有志,我们下回见!


尾图(无二维码).png