1. 底层基建层面该如何做好代码质量维护?

代码质量维护的范围很广,这件事情要做起来也是千头万绪,但是大的方向是可以明确的,我们可以把代码质量的管理,在基建层面,分为两个方向:

静态代码分析

单测覆盖率监测

针对所有代码,我们可以提供基本的代码分析,支持在开发中、上线前提供有依据、行业内认可的静态分析指标,可以有效避免一些低级错误,并且在开发过程中达到『指导』一线开发的作用,使团队整体写出更优质、更规范的代码,同时对团队内部有追求的一线开发,带来权威性的代码编写指导意见,在开发中就拦截一部分问题代码,避免流入到Code Review阶段。以上就是代码静态分析的价值。

而单测覆盖率监测是另一个重要的评价指标。这里对是否要做单测、如何做单测不做展开,只讨论该如何设立单测覆盖率的指标。首先,需要了解单测覆盖率的评价标准有哪些(参考Jacoco团队对于覆盖率的定义):

Instructions (C0 Coverage): 指令覆盖率,java字节码程度的指令覆盖率

Branches (C1 Coverage): 分支覆盖率,针对if和switch语句,这个指标对分支的覆盖情况进行统计

Cyclomatic Complexity: 圈复杂度是可以(线性)组合通过某种方法生成所有可能路径的最小路径数。因此,复杂度值可以作为完全覆盖某个软件的单元测试用例数量的指示。

Lines: 行覆盖率,指的是某一行生成的指令,如果在单测执行过程中被执行过,那么这一行就算被覆盖到了,然后在行维度计算得到的覆盖率指标

Methods: 方法覆盖率,当至少一条指令已执行时,方法被视为已执行。由于 JaCoCo 在字节码级别工作,因此构造函数和静态初始化程序也被视为方法。

Classes:当一个类至少有一个方法被执行时,该类就被视为已执行。请注意,JaCoCo 将构造函数和静态初始值设定项视为方法。由于 Java 接口类型可能包含静态初始化程序,因此此类接口也被视为可执行类。

根据内外网的相关文档,我们可以发现:

行覆盖率和分支覆盖率是最常使用的指标;

分支覆盖率通常比行覆盖率更难提升,更能反映单测覆盖的业务广度。

业界不存在一个统一标准,覆盖率越高并不代表代码质量越高,原则上要更加关注未被覆盖的代码的价值,重要的、被反复使用的代码,都应该被覆盖到

国内阿里的几篇文章反映出来,阿里内部认为6-70%的行覆盖率是有必要的

这一结论参考了以下来源:

https://mp.weixin.qq.com/s/8JC_vaFOgiJPIH7yfbP25A

https://mp.weixin.qq.com/s/e5gkhOyZZuLpjVyDUModQQ

https://mp.weixin.qq.com/s/wzGxqNv58Zig9_Izi3VhDg

https://mp.weixin.qq.com/s/okwW01oCtUxUya2XG_PugQ

https://www.baeldung.com/cs/code-coverage

https://testing.googleblog.com/2020/08/code-coverage-best-practices.html

2. 代码的静态分析工具选择

2023年年初的时候,我们曾经有这样的愿景:

当时,代码静态分析工具暂定为老牌的Java代码静态分析工具FindBugs。

但是事与愿违,FindBugs目前已经处于不维护的状态,不再适合作为我们的第一选择。

之后,我们又选择了CheckStyle工具来进行静态代码分析。它的地址:https://checkstyle.sourceforge.io/

选择分析工具的需求很简单:

最好是业界规范,具有权威性

可以和常见Ide很好的集成,便于开发时作为参考

可以集成到流水线中,对开发的部署产生实质影响,避免不达标的代码流入线上

最好是可以提供分级的监控标准,例如可以分为坏味道、警告、bug几个级别

最好可以对质量进行评分,动态评估代码库整体的质量分数

CheckStyle基本可以满足条件1-3,他提供了一套简单的配置、分析机制,用于简单的代码质量检测,google代码规范、阿里巴巴代码规范,都有相应的适配配置,可以开箱即用,并且idea\gradle等开发工具也可以和它很好的结合,在开发中、流水线里,获得一致的检测标准。

但是CheckStyle的缺点是:

无法应对复杂的检测场景:例如编译后文件的全局分析,多级分析

无法灵活的应对所有场景:缺乏分级机制、豁免机制死板

再后来,我们考虑使用baidu内部的bugbye工具。

BugBye工具也可以符合上述条件里的1、3、4、5。

但是致命缺陷是无法和ide进行集成,厂内的idea插件早就处于不可维护的状态了。虽然bugbye团队还在,但是插件早就不维护了,这本身就很奇怪,而且,当咨询bugbye团队时,他们依然还在推荐使用该插件。这种让我对使用厂内轮子的信心大打折扣,一个显然无法使用的轮子,怎么还有勇气让用户去使用,对自己内部工具的维护状态都没有感知么。

为了解决这么一个看似简单的需求,不得已,目光还是得看向开源社区的解决方案。

Sonar映入眼帘。

3. 单测的覆盖率指标维护

Java技术栈里单测覆盖率的计算可以使用Jacoco工具,Jacoco生成覆盖率报告的原理主要基于Java Agent和字节码技术,在编译后的字节码里进行埋点统计,最终得到覆盖率报告。

所有统计覆盖率的工具,运行模式无非就是:

提供环境运行代码库的单测,生成Jacoco的单测报告

以特定机制获取Jacoco的单测报告,进一步分析、统计、存储项目的覆盖率信息

更强大的工具,可以读取git等VCS的信息,提供增量的分析

基于历史的报告,提供新增代码的覆盖率分析

获取代码库某一时刻全局的覆盖率信息很容易,难的地方在于如何获取增量的代码覆盖率,这里面有一套复杂的算法,要么工具内维护一个项目状态的演进,要么工具要有能力分析代码库的版本信息。

依照惯例,还是首选厂内方案,但是再次令人失望,厂内覆盖率工具的文档更新频率很低,交互逻辑莫名其妙,与流水线集成的机制相对死板。

例如,我们希望实现『新增代码的覆盖率校验不通过时,禁止部署代码』这一功能,依托厂内工具,是做不到的。

Jacoco提供了覆盖率校验的机制,但是由于Gradle的jacoco task是无状态的,无法获取增量的覆盖率信息,所以也是做不到的。

这一点上,也只有Sonar可以做到。

4. SonarQube的选型依据

依据上述描述,SonarQube具有以下优点:

同时具备静态分析、覆盖率监测的能力

可以和流水线很好的集合,包括但不限于:gitlab\jenkins\自定义流水线

项目保持一定的活跃度,是业界常见的代码质量监测工具

作为静态分析工具,它:

ide友好,常见ide上都有插件,可以实时分析代码问题

提供了多级的监测机制

可以灵活豁免,避免误判干扰

作为单测覆盖率监测工具,它:

配置简单,容易上手

支持代码库的增量覆盖率计算

并且,还有一个重要的亮点,在和流水线结合时,提供了质量阈的概念,用户可以定义一套质量标准,对新入库代码进行校验,如果代码不符合校验要求,则进行拦截,不允许流水线继续执行。

5. 团队内部SonarQube服务的搭建与维护

简而言之,

部署一个PostGreSql服务、一个SonarQube服务,PostGreSql作为存储使用。

给PostGreSql、SonarQube外挂PVC存储,持久化配置与服务数据。

进行一些基础设置:配置中文插件、配置社区的多分支支持插件、设置用户权限

k8s的配置文件如下:

apiVersion: v1

kind: ConfigMap

metadata:

name: postgres-config

labels:

app: postgres

data:

POSTGRES_DB: sonarDB

POSTGRES_USER: postgresadmin

POSTGRES_PASSWORD: ****

---

kind: PersistentVolumeClaim

apiVersion: v1

metadata:

name: postgres-pv-claim

labels:

app: postgres

spec:

storageClassName: common-cfs

accessModes:

- ReadWriteMany

resources:

requests:

storage: 5Gi

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: postgres-deployment

spec:

strategy:

type: Recreate

selector:

matchLabels:

app: postgres

replicas: 1

template:

metadata:

labels:

app: postgres

spec:

containers:

- name: postgres

image: postgres:11.7

imagePullPolicy: "IfNotPresent"

ports:

- containerPort: 5432

envFrom:

- configMapRef:

name: postgres-config

volumeMounts:

- mountPath: /var/lib/postgresql/data

name: postgredb

volumes:

- name: postgredb

persistentVolumeClaim:

claimName: postgres-pv-claim

---

apiVersion: v1

kind: Service

metadata:

name: postgres-service

labels:

app: postgres

spec:

type: ClusterIP

ports:

- port: 5432

targetPort: 5432

protocol: TCP

selector:

app: postgres

---

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: sonarqube-data

spec:

accessModes:

- ReadWriteMany

storageClassName: common-cfs

resources:

requests:

storage: 10Gi

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: sonarqube

labels:

app: sonarqube

spec:

replicas: 1

selector:

matchLabels:

app: sonarqube

template:

metadata:

labels:

app: sonarqube

spec:

initContainers:

- name: init-sysctl

image: busybox:latest

imagePullPolicy: IfNotPresent

command: ["sysctl", "-w", "vm.max_map_count=262144"]

securityContext:

privileged: true

containers:

- name: sonarqube

image: sonarqube:lts

ports:

- containerPort: 9000

env:

- name: SONARQUBE_JDBC_USERNAME

value: "postgresadmin"

- name: SONARQUBE_JDBC_PASSWORD

value: "****"

- name: SONARQUBE_JDBC_URL

value: "jdbc:postgresql://postgres-service:5432/sonarDB"

livenessProbe:

httpGet:

path: /sessions/new

port: 9000

initialDelaySeconds: 60

periodSeconds: 30

readinessProbe:

httpGet:

path: /sessions/new

port: 9000

initialDelaySeconds: 60

periodSeconds: 30

failureThreshold: 6

resources:

limits:

cpu: 2000m

memory: 2048Mi

requests:

cpu: 1000m

memory: 1024Mi

volumeMounts:

- mountPath: /opt/sonarqube/conf

name: data

subPath: conf

- mountPath: /opt/sonarqube/data

name: data

subPath: data

- mountPath: /opt/sonarqube/extensions

name: data

subPath: extensions

imagePullSecrets:

- name: baidubceregistrykey

volumes:

- name: data

persistentVolumeClaim:

claimName: sonarqube-data

---

apiVersion: v1

kind: Service

metadata:

name: sonarqube

labels:

app: sonarqube

spec:

type: ClusterIP

ports:

- name: sonarqube

port: 9000

targetPort: 9000

protocol: TCP

selector:

app: sonarqube

6. 团队内部新项目如何接入SonarQube服务(仅针对Java+Gradle代码)

6.1 服务端配置

进入自己搭建的sonar-qube首页,点击右上角新增项目,

选择手工,设置项目基本信息,通常项目标识=代码库的名字

6.2 流水线设置

可以参考sonarqube自己提供的CICD命令,嵌入进流水线即可。

7. 本地如何使用SonarLint插件

idea插件市场搜索SonarLint安装

设置Settings->tools->SonarLint,设置远端的sonarQube服务地址

设置Settings->tools->SonarLint->project settings绑定远端服务

之后本地就可以看到代码分析结果。

浏览量: 4,066