前言

良好的代码习惯是一个优秀程序员应该具备的品质,但靠人的习惯与记忆来保证代码质量,始终不是一件靠谱的事。在计算机行业应该深知,只要是人为的,都会有操作风险。本文讲解如何通过Docker搭建代码检测平台SonarQube,并使用它来检测项目的代码。

安装Sonarqube

这里选择免费的社区版Community(对应docker版本:community),另外还有DeveloperEnterprise等收费版本,功能更强大,具体差别如下:

注:

v8.9支持nodejs <= 15.x,jdk 8

v10支持nodejs 14.17+,jdk 11 or jdk17

先启动容器

docker-compose文件参考:https://github.com/SonarSource/docker-sonarqube/tree/master/example-compose-files

镜像参考:https://hub.docker.com/_/sonarqube/tags

7.9之后官方支持内嵌数据库H2

但是使用H2数据库有会以下限制:

内嵌数据库无法扩展,也无法升级到新版本的SonarQube,并且不能支持将数据迁移至其他数据库引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3"
services:
sonarqube:
image: sonarqube:10.0.0-community
volumes:
- ./sonarqube_data:/opt/sonarqube/data
- ./sonarqube_extensions:/opt/sonarqube/extensions
- ./sonarqube_logs:/opt/sonarqube/logs
- ./sonarqube_conf:/opt/sonarqube/conf
ports:
- "9000:9000"
# 这里使用外部network docker_default
networks:
docker_default:
aliases:
- sonarqube-alias
networks:
docker_default:
external: true

使用postgres安装可参考:https://github.com/SonarSource/docker-sonarqube/tree/master/example-compose-files/sq-with-postgres

注:可能会遇到如下问题

  1. java.lang.IllegalStateException: Unable to access 'path.data'

    参考:https://github.com/SonarSource/docker-sonarqube/issues/282#issuecomment-513064345,对宿主机挂载目录执行赋权操作:`chmod 777 -R ./`

  2. max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]

    参考:https://github.com/SonarSource/docker-sonarqube/issues/282#issuecomment-511703732,执行`sysctl -w vm.max_map_count=262144,并添加command参数:command: "-Dsonar.search.javaAdditionalOpts=-Dnode.store.allow_mmap=false"`

将配置拷贝出来

执行 docker cp <id>:/opt/sonarqube/conf ./conf

修改配置,重新启动

将注释打开:- ./sonarqube_conf:/opt/sonarqube/conf

执行:docker-compose up -d

访问

地址:http://ip:9000/

默认管理员用户和密码为:admin/admin

插件安装

SonarQube提供了强大的插件管理功能,以中文语言包为示例,讲解如何安装插件:

Administration-Marketplace-Plugins,在搜索框输入Chinese就可以选择安装了。

当状态显示为Install Pending时,说明插件安装完成,点击Restart Server即可生效。

Scanner

安装

下载地址:https://docs.sonarqube.org/latest/analyzing-source-code/scanners/sonarscanner/

执行:cd /usr/local/src/ && curl -L https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.8.0.2856-linux.zip -o sonarqube-scnanner.zip && unzip sonarqube-scnanner.zip && mv sonar-scanner-* sonar-scanner

赋权:chmod +x /usr/local/src/sonar-scanner/bin/sonar-scanner && chmod +x /usr/local/src/sonar-scanner/bin/sonar-scanner-debug

代码分析

参考地址:https://docs.sonarqube.org/latest/analyzing-source-code/languages/overview/

有两种配置方式

配置文件方式

在项目根目录创建sonar-project.properties文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# must be unique in a given SonarQube instance
sonar.projectKey=my:project

# --- optional properties ---

# defaults to project key
#sonar.projectName=My project
# defaults to 'not provided'
#sonar.projectVersion=1.0

# Path is relative to the sonar-project.properties file. Defaults to .
#sonar.sources=.

# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8

指令方式

在scanner指令后添加参数

  • -Dsonar.qualitygate.wait=true【当希望该pipeline的结果会影响到接下来的操作时可以添加此参数】:轮询该SonarQube实例的结果,会增加pipeline持续时间,且导致分析步骤在质量检测时因某个规则未通过而失败。

-Dsonar.projectKey=${CI_PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${SONAR_HOST} -Dsonar.token=${SONAR_TOKEN}

通过令牌/账户密码使用

通过账号密码使用

指定SonarQube平台的地址,并指定用户名和密码,就能检测代码了,具体命令如下:

1
/usr/local/src/sonar-scanner/bin/sonar-scanner -Dsonar.host.url=http://localhost:9000 -Dsonar.token=${SONAR_TOKEN}

通过Token令牌使用

当然,直接使用admin并暴露密码并不是一个好的习惯,可以通过配置-权限-用户来创建用户,并创建令牌。

复制令牌:9656c84090b2481db6ea97b6d14d87d546bff619

这样,就可以通过令牌来操作了:

1
/usr/local/src/sonar-scanner/bin/sonar-scanner -Dsonar.host.url=http://localhost:9000 -Dsonar.token=9656c84090b2481db6ea97b6d14d87d546bff619

代码整合

Js

安装好对应Nodejs版本

  • 安装nodejs(略)
  • 需保证当前执行ci的账户能正确执行node指令,否则会报错:Error when running: 'node -v'. Is Node.js available during analysis? org.sonarsource.nodejs.NodeCommandException: Error when running: 'node -v'. Is Node.js available during analysis?

整合在.gitlab-ci.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
stages:
- check

######################代码质量检查########################
sonarqube-check:
stage: check
script:
- npm i -g eslint
- npm i -D eslint-config-airbnb-base@^15.0.0 eslint-plugin-import@^2.23.4
- eslint ./ --ext .ts,.js -f json -o eslint_report.json; exit 0
- /usr/local/src/sonar-scanner/bin/sonar-scanner -Dsonar.projectKey=${CI_PROJECT_NAME} -Dsonar.eslint.reportPaths=eslint_report.json -Dsonar.sources=. -Dsonar.host.url=${SONAR_HOST} -Dsonar.token=9656c84090b2481db6ea97b6d14d87d546bff619
tags:
- check
only:
- master

整合在Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
# sonar scan code files
RUN cd /usr/local/src/ \
&& curl -L https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.8.0.2856-linux.zip -o sonarqube-scnanner.zip \
&& unzip sonarqube-scnanner.zip \
&& mv sonar-scanner-* sonar-scanner \
&& chmod +x /usr/local/src/sonar-scanner/bin/sonar-scanner \
&& chmod +x /usr/local/src/sonar-scanner/bin/sonar-scanner-debug
ARG SONAR_HOST=""
ARG SONAR_LOGIN=""
ARG CI_PROJECT_NAME=""
RUN npm i -g eslint
COPY . .
RUN eslint ./ --ext .ts,.js -f json -o eslint_report.json; exit 0 \
&& /usr/local/src/sonar-scanner/bin/sonar-scanner -Dsonar.projectKey=${CI_PROJECT_NAME} -Dsonar.sources=. -Dsonar.eslint.reportPaths=eslint_report.json -Dsonar.host.url=${SONAR_HOST} -Dsonar.token=${SONAR_LOGIN}
...

Java

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
......
<properties>
<java.version>1.8</java.version>
<redisson.version>3.17.5</redisson.version>
<sonar.maven.plugin.version>3.9.1.2184</sonar.maven.plugin.version>
<jacoco.maven.plugin.version>0.8.10</jacoco.maven.plugin.version>
<sonar.host.url>${env.SONAR_HOST}</sonar.host.url>
<sonar.login>${env.SONAR_LOGIN}</sonar.login>
<sonar.projectKey>${project.name}</sonar.projectKey>
</properties>
......
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>${sonar.maven.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.maven.plugin.version}</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
......
WORKDIR /user/src/app
RUN rm -rf ./src
ARG IS_MASTER=false
ARG SONAR_HOST=""
ARG SONAR_LOGIN=""
ENV SONAR_HOST=${SONAR_HOST} \
SONAR_LOGIN=${SONAR_LOGIN}
COPY . .
# 我们使用的是社区版,只能进行单分支检测。所以可以在这里进行判断,仅分析master分支的代码
RUN mvn clean package -DskipTests && \
if [ "$IS_MASTER" = "true" ]; then \
# 如果sonarqube和项目的jdk版本不匹配,可以在执行mvn sonar:sonar命令前重新指定JAVA_HOME
# curl -L https://download.java.net/java/GA/jdk11/13/GPL/openjdk-11.0.1_linux-x64_bin.tar.gz -o /usr/local/src/openjdk11.tar.gz && \
# cd /usr/local/src && tar -zxvf openjdk11.tar.gz && rm -f openjdk11.tar.gz && mv jdk* openjdk11 && export JAVA_HOME=/usr/local/src/openjdk11 && \
cd /user/src/app && mvn sonar:sonar; \
fi
......

.gitlab-ci.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
......
build-master:
stage: build
script:
- echo "Building application..."
# 构建阶段添加build-arg IS_MASTER=true
- sudo docker build --build-arg IS_MASTER=true --build-arg SONAR_HOST=${SONAR_HOST} --build-arg SONAR_LOGIN=${SONAR_LOGIN} -t ${REGISTRY_ADDRESS}/${IMAGE} .
tags:
- build-runner
only:
- master
- /^master-.*/
......

结果

执行命令后,就会在界面上自动新建了一个项目,并给出检测结果:

Sonar提供了许多指标如测试覆盖率、复杂度等,这能大大帮助我们写出更好的代码:

SonarQube Community 实现多分支扫描分析

背景

同一个 Git 项目,需要分析多个分支的代码扫描。

说明

SonarQube Community 版本不支持多分支扫描,

SonarQube Developer Edition 及以上版本是支持多分支扫描的,扫描时指定分支参数-Dsonar.branch=develop即可,就可以实现多分支代码扫描。

1
$ mvn clean verify sonar:sonar -Dmaven.test.skip=true -Dsonar.branch=master

社区版多分支扫描

目前有2种方式可以实现。

  • 开源插件:sonarqube-community-branch-plugin
  • 替换 sonar.projectKeyporjectKey 相等于 Sonar 中每个项目的主键 ID,替换后就会以新项目创建

开源插件

插件地址:https://github.com/mc1arke/sonarqube-community-branch-plugin

大致操作步骤:

  • 参考:https://github.com/mc1arke/sonarqube-community-branch-plugin/tree/master?tab=readme-ov-file#installation

  • 下载插件放到${SONAR_HOME}/extensions/plugins目录下

  • 修改配置文件,添加如下两个环境变量

    • SONAR_WEB_JAVAADDITIONALOPTS: "-javaagent:/opt/sonarqube/extensions/plugins/sonarqube-community-branch-plugin-{VERSION}.jar=web"
    • SONAR_CE_JAVAADDITIONALOPTS: "-javaagent:/opt/sonarqube/extensions/plugins/sonarqube-community-branch-plugin-{VERSION}.jar=ce"
  • 重启Sonarqube

  • 扫描时,增加-Dsonar.branch.name=${GIT_BRANCH}即可。

    • Jenkins:${GIT_BRANCH}
    • GitLab-Runner:${CI_COMMIT_REF_NAME}

替换 sonar.projectKey

扫描时,指定不同的 sonar.projectKey 即可。

1
2
3
# jenkins 设置 projectName,projectKey 为 job 名称
# job 名称规范: 工程名称-分支名称
$ clean verify sonar:sonar -Dmaven.test.skip=true -Dsonar.projectName=${JOB_NAME} -Dsonar.projectKey=${JOB_NAME} -Dsonar.token=${SONAR_TOKEN}

结合gitlab-ci

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
stages:
- sonarqube-check

######################代码质量检查【镜像已支持16/18,sonar-scanner对应5.0版本】########################
sonarqube-check:
stage: sonarqube-check
image:
# name: wangxiaowu950330/sonar-scanner-cli-with-node18:5.0
name: wangxiaowu950330/sonar-scanner-cli-with-node16:5.0
entrypoint: [""]
rules:
- if: '$SONAR_CHECK == "false"'
when: never
- if: '$CI_COMMIT_REF_NAME =~ /^dev.*/'
when: always
- if: '$CI_COMMIT_REF_NAME == "master"'
when: always
- if: '$CI_COMMIT_REF_NAME =~ /^release-.*/'
when: always
before_script:
# - npm18 install --registry=https://registry.npmmirror.com
# - npm18 run sonar
- npm16 install --registry=https://registry.npmmirror.com
- npm16 run sonar
script:
- sonar-scanner -Dsonar.projectKey=${CI_PROJECT_NAME} -Dsonar.sources=. -Dsonar.exclusions=eslint_report.json -Dsonar.eslint.reportPaths=eslint_report.json -Dsonar.host.url=$SONAR_HOST -Dsonar.token=$SONAR_TOKEN -Dsonar.branch.name=${CI_COMMIT_REF_NAME}
tags:
- sonarqube-check
allow_failure: false