在持续集成中利用SonarQube推动代码质量的持续优化的实践

DevOps

DevOps(Development和Operations的组合词)是一种重视“开发人员(Dev)”和“运维人员(Ops)”之间沟通合作的文化、运动或惯例。通过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

DevOps生命周期: 版本控制、持续集成、持续部署和持续监控贯穿于软件开发的整个生命周期。

图片

持续集成: 将所有软件工程师对于软件的工作副本持续集成到共享主线(mainline)的一种举措,例如将git的特性分支合并到主分支。持续集成的宗旨是避免集成问题, 组织在运用持续性集成(CI)一般会建立CI服务器来维护持续性并提供质量控制程序,一般包括自动化单元测试,自动化静态代码扫描,自动化代码风格检测….,意在提升软件质量以及减少交付的时间。

参考链接:https://zh.wikipedia.org/wiki/DevOps

消除技术债务危机

软件的质量通常分为两种,内部质量通常指代码和设计的质量。内部质量可以通过应用设计和编程达到最佳实践,也可以通过持续一致的开发和交付流程来提高。外部质量是通过查看和使用软件(例如验收测试)来度量的。从长远的角度看,内部质量不佳最终会影响外部质量,应用程序会持续不断地冒出新的bug,产生技术债务,而且开发时间会由于技术债务的增加而变长,由于初始鲁莽的设计决策,在未来的开发中我们需要付出更多努力,消耗更多的时间,这就是技术债务危机。

技术债务危机产生一般分为两类

  • 无意识的 - 由于经验的缺乏导致初级开发者编写了质量低劣的代码
  • 有意识的 - 团队根据当前而非未来进行设计选型,这种方式能很快解决当前的问题,但却很拙劣

现象主要围绕着下面几点:

  • 重复
  • 糟糕的复杂性分布
  • 意大利面式设计风格
  • 缺少单元测试
  • 缺乏代码标准
  • 潜在的bug
  • 注释不足或过多

技术债务的处理

在企业的DevOps建设过程中,我们可以基于持续集成的代码构建和自动化分析, 通过很多不同的维度评价代码设计的内部质量 。 可以通过质量度量平台(如SonarQube),实现代码设计质量的持续监控,包括对代码复杂度、重复度、代码风格、单元测试、测试覆盖率的监控等。促进技术债务的及时处理,从而提高软件系统的总体开发运维效益。

SonarQube

介绍

SonarQube是一个开源的代码质量管理系统。

特性

  • 支持超过25种编程语言。
  • 提供重复代码、编码标准、单元测试、代码覆盖率、代码复杂度、潜在Bug、注释和软件设计报告。
  • 提供了指标历史记录、计划图和微分查看。
  • 提供了完全自动化的分析:与Maven、Ant、Gradle和持续集成工具(例如Jenkins)。
  • 可以与Eclipse,IDEA等开发环境集成。
  • 可以与JIRA、Mantis、LDAP、Fortify等外部工具集集成。
  • 支持扩展插件。

架构

image

SonarQube平台由4个组件组成:

  1. SonarQube Server:
    • SonarQube Web服务器:供开发者、管理人员浏览质量指标和进行SonarQube的配置
    • 基于Elasticsearch的搜索
    • Compute Engine:负责处理代码分析报告并将其保存在SonarQube数据库中
  2. SonarQube Database: 存储SonarQube的配置与质量报告,各种视图数据
  3. SonarQube Plugins:SonarQube插件支持,包括开发语言,SCM,持续集成,安全认证等
  4. SonarQube Scanner:在构建/持续集成服务器上运行一个或多个SonarScanner,以分析项目

工作流程

image

上图可以看出SonarQube各组件的工作流程:

  1. 开发者在IDE中编码,可以使用SonarLint执行本地代码分析
  2. 开发者向SCM(Git,SVN,TFVC等)提交代码
  3. 代码提交触发持续集成平台(如Jenkins等)自动构建,执行SonarQube Scanner进行分析
  4. 持续集成平台将分析报告发送到SonarQube Server进行处理
  5. SonarQube Server处理好的分析报告生成对应可视化的视图并保存数据到数据库
  6. 开发者可以在SonarQube UI进行查看,评论,通过解决问题来管理和减少技术债
  7. SonarQube支持导出报表,使用API提取数据,基于JMX的监控

基础概念

  • 指标:指标的定义主要有复杂度,重复项,问题,可维护性,安全性,复杂度,测试覆盖率等。
  • 代码分析规则:SonarQube中可以通过插件提供的规则对代码进行分析并生成问题。规则中定义了修复问题的成本(时间),解决问题的代价以及技术债可以通过这些问题进行计算。规则一般有三种类型:可靠性(Bug),可维护性(坏味道),安全性(漏洞)。
  • 质量阈 : 质量阈是一系列对项目指标进行度量的条件形成的阈值。

部署

具体部署方式可以参考官方文档:https://docs.sonarqube.org/latest/

简单便捷,小规模使用可以基于docker compose部署,如下所示:

# 镜像拉取与基础目录创建
docker pull sonarqube:8.2-community
docker pull postgres:12
mkdir -p /opt/sonarqube-data/{conf,data,logs,extensions,postgresql,postgresql-data}
chmod -R 777 /opt/sonarqube-data
# 启动
docker-compose -f sonar-docker-compose.yml up -d
version: "3"

services:
  sonarqube:
    image: sonarqube:8.2-community
    ports:
      - "9000:9000"
    networks:
      - sonarnet
    environment:
      - sonar.jdbc.url=jdbc:postgresql://db:5432/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance&useSSL=false"
      - sonar.jdbc.username=sonar
      - sonar.jdbc.password=sonar
    restart: always
    volumes:
      - sonarqube_conf:/opt/sonarqube/conf
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions

  db:
    image: postgres:12
    networks:
      - sonarnet
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    restart: always
    volumes:
      - postgresql:/var/lib/postgresql
      - postgresql_data:/var/lib/postgresql/data

networks:
  sonarnet:
    driver: bridge

volumes:
  sonarqube_conf:
  sonarqube_data:
  sonarqube_extensions:
  postgresql:
  postgresql_data:

Jenkins集成SonarQube 实现对Java项目的质量控制

利用Jenkins集成SonarQube可以实现在持续集成过程中的利用SonarQube Scanner进行静态代码分析。

静态代码分析

前置准备

  • Jenkins安装插件:SonarQube Scanner for Jenkins
  • 系统设置—>配置—>SonarQube servers中对SonarQube server进行配置

图片

注意:server authentication token为SonarQube用户生成的token,类型为Secret text。

  • 系统管理—>全局工具配置—>SonarQube Scanner配置

图片

  • 代码库的根目录创建sonar-project.properties

这里使用的项目以美团开源的Leaf为例子,项目目录如下:

├── Leaf
│   ├── leaf-core
│   ├── leaf-server

sonar-project.properties配置文件配置如下:

sonar.projectKey=Leaf
sonar.projectName=Leaf
sonar.projectVersion=0.0.1
sonar.sources=src/main/java
sonar.java.binaries=target/classes
sonar.java.source=1.8
sonar.java.target=1.8
sonar.language=java
sonar.sourceEncoding=UTF-8
sonar.modules=leaf-core,leaf-server

参考SonarQube文档:https://docs.sonarqube.org/latest/analysis/languages/java/

Pipeline配置

stage('拉取代码,构建') {
    ...
} 
stage('静态代码扫描') {
         steps {
            script {scannerHome = tool 'SonarQube Scanner'}
            withSonarQubeEnv('SonarQube-Dev') {
                sh "${scannerHome}/bin/sonar-scanner"
            }
            
         }
      }

构建成功,SonarQube Scanner扫描完成后可以在SonarQube服务器看见对应项目的报表如下:

图片

测试覆盖率控制

关于Jacoco

jacoco是一个开源的覆盖率工具,可以嵌入到 Ant 、Maven 中使用,提供了Eclipse与IDEA插件,也可以使用 Java Agent 技术监控 Java 程序,第三方自动化工具如SonarQube,Jenkins也对jacoco提供了很好的支持。

代码覆盖(Code coverage) 是软件测试中的一种度量,描述程序中源代码被测试的比例和程度。

Jacoco 包含了多种尺度的覆盖率计数器,包含指令级(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes)。

  • Instructions:Jacoco 计算的最小单位是字节码指令。指令覆盖率表明了在所有的指令中,哪些被执行过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息。
  • Branches:Jacoco 对所有的 if 和 switch 指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内。
  • Cyclomatic Complexity:Jacoco 为每个非抽象方法计算圈复杂度,并也会计算每个类、包、组的复杂度。根据 McCabe 1996 的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效。
  • Lines:该项指数在有调试信息的情况下计算。
  • Methods:每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为 Jacoco 直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。
  • Classes:每个类中只要有一个方法被执行,这个类就被认定为被执行。同 Methods一样,有些没有在源码声明的方法被执行,也认定该类被执行。

前置准备

  • Jenkins安装插件JaCoCo plugin。
  • maven配置jacoco插件
<plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.5.201505241946</version>
                <executions>
                    <execution>
                        <id>pre-unit-test</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                        <configuration>
                            <destFile>
                                ${project.build.directory}/${project.artifactId}-jacoco.exec
                            </destFile>
                        </configuration>
                    </execution>
                    <execution>
                        <id>post-unit-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>
                                ${project.build.directory}/${project.artifactId}-jacoco.exec
                            </dataFile>
                            <outputDirectory>${project.build.directory}/jacoco</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

该配置会在当前目录下的target目录产生一个jacoco.exec文件,该文件就是覆盖率的文件,相关的报表输出文件为当前目录下的target/jacoco目录。由于绑定了maven test生命周期,可以执行如下命令进行覆盖率文件与报表生成: mvn clean test -Dmaven.test.failure.ignore=true

Pipeline配置

pipeline脚本如下所示,其中jacoco对覆盖率指标进行阈值设置,可参考pipeline-syntax进行配置,由于我们使用了SonarQube作为质量管理系统,对于覆盖率指标的把控也可以使用SonarQube的质量阈进行控制。

stage('代码覆盖率分析') {
         steps {
            
           withMaven(maven: 'maven3.6.3') {
            sh 'mvn clean test -Dmaven.test.failure.ignore=true'
            }
            
            jacoco(
                buildOverBuild: false,
                //未达到要求修改pipeline的status为fail
                changeBuildStatus: true, 
                //各种尺度指标阈值
                maximumBranchCoverage: '80', 
                maximumClassCoverage: '80', 
                maximumComplexityCoverage: '80', 
                maximumLineCoverage: '80', 
                maximumMethodCoverage: '80', 
                minimumBranchCoverage: '50', 
                minimumClassCoverage: '50', 
                minimumComplexityCoverage: '50', 
                minimumLineCoverage: '50', 
                minimumMethodCoverage: '50', 
                sourceInclusionPattern: '**/*.java'
            )
         }
      }

运行后在Jenkins上的Coverage Report可以查看到对应的覆盖率报告:

图片

单元测试覆盖率统计数据上报SonarQube

需要在sonar-project.properties配置文件里配置jacoco报表,surefire报表路径,如下所示:

# 配置junit单元测试源码与报表
sonar.tests=src/test
sonar.java.test.binaries=target/test-classes
sonar.junit.reportsPaths=target/surefire-reports
# 配置jacoco相关
sonar.java.coveragePlugin=jacoco
sonar.coverage.jacoco.xmlReportPaths=target/jacoco/jacoco.xml

# 如果仅需要扫描某个模块,可以设置为xmodule.sonar.surefire.reportsPath=target/surefire-reports...

参考sonarQube文档:https://docs.sonarqube.org/latest/analysis/coverage/

最终效果如下:

图片

SonarQube推动代码质量持续优化

上文说明了如何利用SonarQube结合Jenkins在pipeline中进行静态代码检测与测试覆盖率检测,在实际的开发中我们可以基于Git-Flow分支模型,通过Git的Webhook结合Jenkins,SonarQube质量阈,在CI中利用SonarQube推动代码质量的持续优化,具体流程如下所示。

具体流程

图片

  1. 工程师完成特性分支的开发,将代码推送到远程服务器上
  2. Git 服务器 根据配置的Webhook 回调通知Jenkins服务器
  3. Jenkins服务器SonarQube 质量分析Job执行(可以基于Git-flow的工作流,过滤特定的分支如test/develop提交时才进行执行)
  4. Jenkins 通过SonarQube Scanner 插件分析代码
  5. Jenkins将分析产生的报告结果发送到SonarQube服务器上
  6. SonarQube服务器对接收到的分析报告进行可视化,存储以及质量阈的校验
  7. SonarQube服务器回调通知Jenkins质量阈状态
  8. 当质量阈状态为失败时,Jenkins将结果对工程师进行通知

具体操作

  • Generic Webhook Trigger配置

Jenkins安装 Generic Webhook Trigger插件,该插件可以接收任何HTTP请求,然后从JSON或XML中提取任何值,并使用这些值作为变量来触发作业。一般用来配合GitHub,GitLab,Bitbucket,Jira等的Webhook一起使用。

job勾选构建触发器Generic Webhook Trigger选项,设置token,如图所示:

图片

以Gitlab,Gitee或者Github的代码仓库WebHooks配置Jenkins Generic Webhook Trigger的回调地址 (http://[JenkinsIP]/generic-webhook-trigger/invoke?token=gitee-leaf-sonar-token)。以码云为例子,可以选择如下几个触发事件,当事件触发时回调我们的Jenkins 地址,触发job执行。

图片

参考文档:https://plugins.jenkins.io/generic-webhook-trigger/

  • Generic Webhook Trigger指定分支触发

一般情况下,我们都会基于分支模型进行开发,例如常见的有git-flow分支模型。基于git-flow分支模型,我们可以在feature分支合并到develop/test的时候(特性分支提测的时候),触发Jenkins Job执行,确保在上线正式/灰度环境之前代码已经通过了SonarQube 质量阈。

以Gitee为例子,触发事件发送请求,请求的body里包含了本次提交的一些信息,其中 "ref": "refs/heads/test",表示本次提交的分支。有了这个信息,可以通过Generic Webhook Trigger的Post content parameters将请求体中的ref内容提取出来通过表达式Expression赋给$ref。

图片

在Optional filter中 Expression 填写正则,Text 填写我们刚刚设置的变量 $ref进行匹配,如下所示对test分支进行了匹配,实现指定分支触发job的目的。

图片

  • SonarQube配置Jenkins webhook

这里用于在Sonarqube完成扫描后,通知Jenkins扫描结果,参考文档:https://docs.sonarqube.org/latest/project-administration/webhooks/

图片

  • Jenkins配置Quality Gates - Sonarqube

Jenkins安装Sonar Quality Gates 插件,通过此插件可以让Jenkins等待SonarQube分析完成并返回质量阈状态 。当返回为Fail时,我们可以aborted 对应的job并进行通知操作。在 Jenkins的Manage Jenkins -> Configure System -> Quality Gates - Sonarqube,对其进行设置。

图片

  • 配置质量阈

在SonarQube质量阈界面对指标进行配置,并分配到项目上。

  • pipeline脚本如下
pipeline {
   agent any
   stages {
      stage('拉取代码') {
         steps {
            git branch: 'test', 
            credentialsId: '5c5a2fae-5468-472a-a4d5-0934dd311fd5', 
            url: 'git@gitee.com:**/***.git'
         }
      }
      
      stage('构建') {
         steps {
           withMaven(maven: 'maven3.6.3') {
            sh 'mvn clean package -Dmaven.test.skip=true'
            }
         }
      }
      
      stage('代码覆盖率分析') {
         steps {
           withMaven(maven: 'maven3.6.3') {
            sh 'mvn clean test -Dmaven.test.failure.ignore=true'
            }
         }
      }
      
      stage('静态代码扫描') {
         steps {
            script {scannerHome = tool 'SonarQube Scanner'}
            withSonarQubeEnv('SonarQube-Dev') {
                sh "${scannerHome}/bin/sonar-scanner"
            }
            
         }
      }
      
      stage("质量阈校验") {
            steps {
              timeout(time: 1, unit: 'HOURS') {
                 script{
                        // Wait for SonarQube analysis to be completed and return quality gate status
                        def qg = waitForQualityGate();
                        if (qg.status != 'OK') {
                             error "Pipeline aborted due to quality gate failure: ${qg.status}"
                             //进行消息通知,可以通过SonarQube Web API获取具体失败信息
                        }
                 }
            }
         }
      }
   }
}