使用SonarQube进行代码质量审查

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

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

  • 静态代码分析
  • 单测覆盖率监测

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

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

  • Instructions (C0 Coverage): 指令覆盖率,java字节码程度的指令覆盖率
  • Branches (C1 Coverage): 分支覆盖率,针对if和switch语句,这个指标对分支的覆盖情况进行统计
  • Cyclomatic Complexity: 圈复杂度是可以(线性)组合通过某种方法生成所有可能路径的最小路径数。因此,复杂度值可以作为完全覆盖某个软件的单元测试用例数量的指示。
  • Lines: 行覆盖率,指的是某一行生成的指令,如果在单测执行过程中被执行过,那么这一行就算被覆盖到了,然后在行维度计算得到的覆盖率指标
  • Methods: 方法覆盖率,当至少一条指令已执行时,方法被视为已执行。由于 JaCoCo 在字节码级别工作,因此构造函数和静态初始化程序也被视为方法。
  • Classes:当一个类至少有一个方法被执行时,该类就被视为已执行。请注意,JaCoCo 将构造函数和静态初始值设定项视为方法。由于 Java 接口类型可能包含静态初始化程序,因此此类接口也被视为可执行类。

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

  1. 行覆盖率和分支覆盖率是最常使用的指标;
  2. 分支覆盖率通常比行覆盖率更难提升,更能反映单测覆盖的业务广度。
  3. 业界不存在一个统一标准,覆盖率越高并不代表代码质量越高,原则上要更加关注未被覆盖的代码的价值,重要的、被反复使用的代码,都应该被覆盖到
  4. 国内阿里的几篇文章反映出来,阿里内部认为6-70%的行覆盖率是有必要的
    这一结论参考了以下来源:

file

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

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

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

  1. 最好是业界规范,具有权威性
  2. 可以和常见Ide很好的集成,便于开发时作为参考
  3. 可以集成到流水线中,对开发的部署产生实质影响,避免不达标的代码流入线上
  4. 最好是可以提供分级的监控标准,例如可以分为坏味道、警告、bug几个级别
  5. 最好可以对质量进行评分,动态评估代码库整体的质量分数

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

  1. 无法应对复杂的检测场景:例如编译后文件的全局分析,多级分析
  2. 无法灵活的应对所有场景:缺乏分级机制、豁免机制死板

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

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

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

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

  1. 提供环境运行代码库的单测,生成Jacoco的单测报告
  2. 以特定机制获取Jacoco的单测报告,进一步分析、统计、存储项目的覆盖率信息
  3. 更强大的工具,可以读取git等VCS的信息,提供增量的分析
  4. 基于历史的报告,提供新增代码的覆盖率分析

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

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

例如,我们希望实现『新增代码的覆盖率校验不通过时,禁止部署代码』这一功能,依托厂内工具,是做不到的。
Jacoco提供了覆盖率校验的机制,但是由于Gradle的jacoco task是无状态的,无法获取增量的覆盖率信息,所以也是做不到的。
这一点上,也只有Sonar可以做到。

4. SonarQube的选型依据

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

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

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

  3. 项目保持一定的活跃度,是业界常见的代码质量监测工具
    作为静态分析工具,它:

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

  5. 提供了多级的监测机制
    file

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

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

  1. 配置简单,容易上手
  2. 支持代码库的增量覆盖率计算
    并且,还有一个重要的亮点,在和流水线结合时,提供了质量阈的概念,用户可以定义一套质量标准,对新入库代码进行校验,如果代码不符合校验要求,则进行拦截,不允许流水线继续执行。
    file
    file

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

简而言之,

  1. 部署一个PostGreSql服务、一个SonarQube服务,PostGreSql作为存储使用。
  2. 给PostGreSql、SonarQube外挂PVC存储,持久化配置与服务数据。
  3. 进行一些基础设置:配置中文插件、配置社区的多分支支持插件、设置用户权限
    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首页,点击右上角新增项目,

file

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

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

7. 本地如何使用SonarLint插件

idea插件市场搜索SonarLint安装
设置Settings->tools->SonarLint,设置远端的sonarQube服务地址
设置Settings->tools->SonarLint->project settings绑定远端服务
之后本地就可以看到代码分析结果。


已发布

分类

,

来自

标签:

评论

发表回复