在持续集成中利用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等外部工具集集成。
- 支持扩展插件。
架构
SonarQube平台由4个组件组成:
- SonarQube Server:
- SonarQube Web服务器:供开发者、管理人员浏览质量指标和进行SonarQube的配置
- 基于Elasticsearch的搜索
- Compute Engine:负责处理代码分析报告并将其保存在SonarQube数据库中
- SonarQube Database: 存储SonarQube的配置与质量报告,各种视图数据
- SonarQube Plugins:SonarQube插件支持,包括开发语言,SCM,持续集成,安全认证等
- SonarQube Scanner:在构建/持续集成服务器上运行一个或多个SonarScanner,以分析项目
工作流程
上图可以看出SonarQube各组件的工作流程:
- 开发者在IDE中编码,可以使用SonarLint执行本地代码分析
- 开发者向SCM(Git,SVN,TFVC等)提交代码
- 代码提交触发持续集成平台(如Jenkins等)自动构建,执行SonarQube Scanner进行分析
- 持续集成平台将分析报告发送到SonarQube Server进行处理
- SonarQube Server处理好的分析报告生成对应可视化的视图并保存数据到数据库
- 开发者可以在SonarQube UI进行查看,评论,通过解决问题来管理和减少技术债
- 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推动代码质量的持续优化,具体流程如下所示。
具体流程
- 工程师完成特性分支的开发,将代码推送到远程服务器上
- Git 服务器 根据配置的Webhook 回调通知Jenkins服务器
- Jenkins服务器SonarQube 质量分析Job执行(可以基于Git-flow的工作流,过滤特定的分支如test/develop提交时才进行执行)
- Jenkins 通过SonarQube Scanner 插件分析代码
- Jenkins将分析产生的报告结果发送到SonarQube服务器上
- SonarQube服务器对接收到的分析报告进行可视化,存储以及质量阈的校验
- SonarQube服务器回调通知Jenkins质量阈状态
- 当质量阈状态为失败时,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获取具体失败信息
}
}
}
}
}
}
}