Bootstrap

代码质量分析利器之SonarQube【史上最全】

写在前面

随着业务的发展,程序员维护的系统会越来越庞大。原本一个简单稳定的功能,可能在迭代几次后复杂度上升,导致潜在的风险随之暴露,最终导致服务不稳定,造成业务价值的损失。因此,各位大佬们一致认为需要从源头抓起,从个人技术成长到工作流程标准化去提供系统稳定性。与此同时,我也在代码质量这个方向进行挖掘,试图通过代码质量分析去反向要求程序员提高代码编程能力,降低错误风险,这才有了今天的主角-SonarQube

背景

代码质量分析的痛点

  1. 程序员代码风格不同,导致编写代码五花八门,整体代码可读性较差
  2. 代码审查难度大,代码缺陷、漏洞不易发现,且审查工作随着代码量增加而增加,审查效率低
  3. 代码复用性低,重复率高,影响开发效率
  4. 代码复杂度高,存在未知风险

调研

从以下几个方向调研:

  1. 社区大小
  2. 是否大众化
  3. 接入成本
  4. 运维管理成本
  5. 是否免费

调研结果

Sonarqube社区庞大,主流的VCS也在使用,是一款主流的代码质量分析产品,主要调研方向往Sonarqube开始展开
在这里插入图片描述

  • 社区版兼容17种语言(java/scala/js/go/phthon/php/html/css/xml等),几乎涵盖数据部编程语言
  • 社区版提供漏洞检查、代码规则检查、坏味道分析(并提供修改意见)、质量配置以及质量阈(反馈项目代码健壮性)
  • 社区版不支持多分支分析,所以只能分析你的主分支。从Developer Edition开始,您可以分析多个分支和拉取请求。

总结:社区版基本能满足日常工作需要,比起收费版本,我们可能要考虑下如何在master分支做代码分析,取得利益最大化。

Sonarqube介绍

SonarQube 是一个用于代码质量管理的开源平台,用于管理源代码的质量。 通过插件形式,可以支持包括 java, C#, C/C++, PL/SQL,
Cobol, JavaScrip, Groovy
等等二十几种编程语言的代码质量管理与检测。
SonarQube可以从以下七个维度检测代码质量,而作为开发人员至少需要处理前5种代码质量问题。

  1. 不遵循代码标准
    sonar可以通过PMD,CheckStyle,Findbugs等等代码规则检测工具规范代码编写。
  2. 潜在的缺陷
    sonar可以通过PMD,CheckStyle,Findbugs等等代码规则检测工具检 测出潜在的缺陷。
  3. 糟糕的复杂度分布
    文件、类、方法等,如果复杂度过高将难以改变,这会使得开发人员 难以理解它们, 且如果没有自动化的单元测试,对于程序中的任何组件的改变都将可能导致需要全面的回归测试。
  4. 重复
    显然程序中包含大量复制粘贴的代码是质量低下的,sonar可以展示 源码中重复严重的地方。
  5. 注释不足或者过多
    没有注释将使代码可读性变差,特别是当不可避免地出现人员变动 时,程序的可读性将大幅下降 而过多的注释又会使得开发人员将精力过多地花费在阅读注释上,亦违背初衷。
  6. 缺乏单元测试
    sonar可以很方便地统计并展示单元测试覆盖率。
  7. 糟糕的设计
    通过sonar可以找出循环,展示包与包、类与类之间的相互依赖关系,可以检测自定义的架构规则 通过sonar可以管理第三方的jar包,可以利用LCOM4检测单个任务规则的应用情况, 检测耦合。

工作原理

官方给出了典型的开发过程:

  1. 开发人员在编写并提交代码(最好再IDE中集成sonarlint插件,并在本地进行代码分析检测,减少更多的缺陷代码被集成到SCM上)
  2. SCM通过webhook调用,触发CI持续集成,CI触发Sonar Scanner,将本地代码进行扫描分析,输出分析报告,并发送给SonarQube服务端
  3. SonarQube接受分析报告并处理,最终渲染到UI界面

在这里插入图片描述

SonarQube工作流程包含3个组件:

  1. Scanner:扫描器,负责将源文件进行代码分析,并将分析后的报告发送给SonarQube服务器
  2. SonarQube Server:SonarQube服务器,负责处理分析报告,后台管理等
  3. Database server:数据库服务器,负责存储数据

在这里插入图片描述

功能介绍

项目

在这里插入图片描述

项目-总览

在这里插入图片描述

项目-问题

在这里插入图片描述

问题共分为三种类型:

  1. Bug:潜在的代码缺陷,可能引起系统执行异常(比如空指针、魔法值等)
  2. 漏洞:潜在的安全漏洞,比如sql注入、cors、xss攻击等
  3. 异味:代码的坏味道。比如不遵循代码标准、糟糕的复杂度分布、注释不足或注释过多、糟糕的设计等
项目-问题-问题列表
  1. 问题类型(不推荐修改)

在这里插入图片描述

  1. 问题的重要程度(不推荐修改)
    在这里插入图片描述

  2. 问题状态,分为5个状态

打开:问题被质量分析后的初始状态(SonarQube)
确认:问题修复后,需要开发人员手动指定,表示该问题已修复(开发人员)
误判:标记问题为误判,表示此问题不应该标记为问题,无需处理(管理员)
标记为不会修复:表示此问题不做处理(管理员)
重新打开:当SonarQube再次分析报告时,若问题再次暴露则显示为重新打开(SonarQube)
在这里插入图片描述

  1. 问题指派

可以将问题分配给指定用户。
在这里插入图片描述

当问题分配给其他用户后,若该用户有启动提醒功能,则SonarQube会发送邮件进行告警
在这里插入图片描述

当Sonar分析报告后,会根据SCM的相关记录找到对应的用户,进行自动指派。
在这里插入图片描述

  1. 其他
    在这里插入图片描述
项目-安全热点

安全热点是SonarQube检测出来可能存在安全问题,需要项目管理员进行复审,确认是否存在问题。
在这里插入图片描述

项目-安全热点-复审

首先确保当前用户具有管理安全热点的权限
在这里插入图片描述

具有安全热点管理权限后,按钮将显示为蓝色
在这里插入图片描述

复审状态总共有三个,分别为

  1. 需要复审:默认状态
  2. 已修复:表示该安全代码已经修复
  3. 安全:代码风险,无需修改

在这里插入图片描述

项目-安全热点-SonarQube提醒/建议

在这个板块,SonarQube会解释为何是安全代码,且告诉你怎么修复。(英语不好的人建议翻译一下)
在这里插入图片描述

翻译后
在这里插入图片描述在这里插入图片描述在这里插入图片描述

项目-指标

观测当前项目的代码质量
在这里插入图片描述

项目-代码

即SCM更新下来的代码,没啥区别,SonarQube自己也会存储一份
在这里插入图片描述

项目-活动

分析记录的日志
在这里插入图片描述

项目-项目配置

在这里插入图片描述

管理项目的基本配置,如

  1. 设置

设置项目的基本配置信息(不推荐)
在这里插入图片描述

  1. 新代码周期

设置新代码周期的定义,默认按通用配置(即按上个版本的分析开始计算)。
在这里插入图片描述

也可以指定其他选项,如
“天数”:可以指定距离上次分析多少天的数据,作为新代码
在这里插入图片描述

“指定分析”:未知…(没看懂,跳过)

  1. 质量配置

质量配置:表示项目适配的质量配置,质量配置都是基于语言的,一个质量配置下面会存在多个代码规则。
下图表示的项目qx_whale的质量配置基于java、xml两种语言,别分启用了452和11条代码规则。
在这里插入图片描述

  1. 质量阈

质量阈:它是项目质量是否合格的标准,通过设置质量阈来判断项目的代码质量是否达标。
在这里插入图片描述

默认使用SonarQube内置的质量阈配置(Sonar way)
在这里插入图片描述

也可以自定义质量阈,如笔者定义的My Sonar Way,只要出现阻断违规问题,就达不到质量阈的要求
在这里插入图片描述

  1. 自定义指标

即项目-指标板块,展示项目的代码质量。 官方表示未来会废弃,所以不推荐大家使用
在这里插入图片描述

  1. 链接

配置一些跳转链接,比如项目的首页地址。建议在SonarScanner运行时指定sonar.links.homepage去配置首页地址
在这里插入图片描述

  1. 权限

当前项目的权限配置,后面用户与权限模块会特别介绍,这里就不赘述了
在这里插入图片描述

  1. 后台任务

可以查看项目最近分析的记录
在这里插入图片描述

  1. 更新标识

SonarQube每个项目的标识都是唯一的,请确保使用者的项目标识唯一!!!
在这里插入图片描述

  1. 网络调用

俗称web hook,懂得都懂(可以与jenkins集成,将分析报告的质量阈状态回传,以便jenkins判断是否继续执行pipeline任务)
在这里插入图片描述

  1. 删除

谨慎操作,点击删除后有二次确认操作。
在这里插入图片描述

问题

问题菜单跟“项目-问题”展示的基本一致。不同的是,问题菜单展示的是个人参与项目的所有问题,所以数量上相对比较多。
在这里插入图片描述

代码规则

代码规则,用来分析代码是否有问题。
在这里插入图片描述在这里插入图片描述

质量配置

质量配置,用于定义编程语言的代码分析集合。一个质量配置可以配置多个代码规则
在这里插入图片描述

举个例子,我们看下质量配置/java下的代码规则详情
下图表示Java语言的质量配置模板Sonar way,共配置了452个激活的规则,187个未激活的规则,一共配置了639个规则。
在这里插入图片描述

内置质量模板Sonar way不允许修改,若用户想要自定义质量模板,必须拥有“质量配置管理员”的权限才可以进行操作。
在这里插入图片描述
在这里插入图片描述

举个例子,新建一个Java语言的质量模板
在这里插入图片描述在这里插入图片描述

在左边的卡片“规则”中,我们点击“更多激活规则”
在这里插入图片描述
在这里插入图片描述

回到质量模板MyJavaRule,可以看到我们配置了一条代码规则。那么质量配置就介绍到这里了。
在这里插入图片描述

什么情况下会使用到质量配置呢?
一般使用官方内置的Sonar way即可,如果有特殊需求的话,可以自己去定义质量模板。

质量阈

质量阈是一系列基于指标的布尔表达式。它可以帮助我们实时了解项目是否已经满足生产要求了。理想情况下,所有项目使用相同的质量阈。每个项目的质量阈状态都会展示在首页。
在这里插入图片描述

上图表示,所有项目默认的质量阈均为Sonar way,指标解释:
新代码分析时,若覆盖率小于80% 或者 重复行大于3% 或者…安全比率劣于A时,判定为质量阈失败。

质量阈-自定义质量阈

自定义质量阈,首先要有质量阈管理员的权限,可以参考下图设置(建议技术经理或者项目负责人设置此权限)
在这里插入图片描述
在这里插入图片描述

新建一个自定义质量阈MySonarWay,只添加一个条件,当阻断违规的问题大于0时,判定为质量阈失败。
同时,还可以指定哪些项目使用此质量阈
在这里插入图片描述

配置

用户与权限
用户管理

用户管理
在这里插入图片描述

群组管理

可以通过群组,来进行用户的统一授权
在这里插入图片描述

项目权限

项目权限,用于设置公开或者私有
公开项目所有人都可以访问
私有项目只有访问权限的人可以访问(推荐)
在这里插入图片描述

设置项目权限范围,管理员等
在这里插入图片描述

用户与权限-最佳实践
最佳实践-角色分工

角色大致上可以分为三种:系统管理员、技术经理、开发人员

最佳实践-角色分工-系统管理员

系统管理员:只负责sonar平台的管理,不负责项目相关操作
在这里插入图片描述

最佳实践-角色分工-技术经理

技术经理:负责项目成员定义,项目组定义,权限分配,质量配置、质量阈等配置
在这里插入图片描述

最佳实践-角色分工-开发人员

开发人员:默认项目创建项目执行分析,当开发人员创建项目时,该人员成为项目的管理者,拥有项目的所有权
在这里插入图片描述

最佳实践-权限模板

配置默认权限模板(Default template),项目创建人拥有项目的所有权。
在这里插入图片描述

若技术经理也需要同等权力的话,可以主动申请成为项目的管理员(详见下图)
在这里插入图片描述

提醒(邮件告警)

系统邮件设置

看操作截图,还是比较容易理解的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

项目提醒设置

只有订阅了项目提醒,用户才会收到项目推送的邮件。在管理员进行问题分配给用户时,特别有用。

以下介绍如何进行项目提醒设置

方式一
  1. 进入项目
    在这里插入图片描述

  2. 点击项目信息,设置提醒

在这里插入图片描述

  1. 全部勾选

在这里插入图片描述

方式二(推荐)

一步到位,设置也简单。在个人账号设置自己的
在这里插入图片描述

ALM集成

SonarQube集成Gitlab账号

  1. 在gitlab上设置应用访问权限
    其中Redirect URI配置为:http://{sonarqube服务端地址}/oauth2/callback/gitlab

在这里插入图片描述

  1. 保存好ApplicationId和SecretKey,待会要去SonarQube上配置的
    在这里插入图片描述

  2. 回到SonarQube配置界面上,打开Gitlab认证功能,配置Gitlab服务地址,设置ApplicationId和SecretKey
    在这里插入图片描述

  3. 开启用户注册同步功能

在这里插入图片描述

  1. 回到SonarQube登录页。至此,即可通过gitlab实现登录操作。(建议上述操作用户使用gitlab的管理员账号)

在这里插入图片描述

SonarQube集成Gitlab项目

SonarQube登录时,可以使用Gitlab进行授权登录,配置如下

  1. 先在gitlab上配置个人访问令牌
    在这里插入图片描述
    在这里插入图片描述

  2. 回到SonarQube上,设置项目配置

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

安装

环境准备

服务端:SonarQube:8.9.2(LTS)(最低JDK11)

扫描器:SonarScanner(最低JDK8)

索引库:ElasticSearch(内置,无需安装)

数据库:Progresql(最低9.6)

内存要求:至少2GB RAM

SonarQube服务端安装

下载点这里官方链接,建议使用版本8.9(LTS)
安装后进行解压,会有一个SonarQube文件夹,以下简称SONAR_HOME

服务期配置

特别注意,官方文档有说明SonarQube8.9+要使用jdk11的版本
在这里插入图片描述

  1. 编辑{SONAR_HOME}/conf/wrapper.conf,指定JAVA_HOME路径
# Path to JVM executable. By default it must be available in PATH.
# Can be an absolute path, for example:
#wrapper.java.command=/path/to/my/jdk/bin/java
# 配置sonarqube启动时的jdk版本
wrapper.java.command=/usr/local/webserver/jdk-11.0.13/bin/java
...
  1. 编辑conf/sonar.properties,基本配置如下
### web
sonar.web.host=10.10.10.15
sonar.web.port=9000
### 数据库(如果不配置,则启用内置的H2数据库)
sonar.jdbc.username=admin
sonar.jdbc.password=admin
sonar.jdbc.url=jdbc:postgresql://localhost/sonarqube?currentSchema=my_schema
### 日志
sonar.path.logs=/www/sonarqube/logs/
日志
  • sonar.log - 主进程的日志。保存有关启动和关闭的一般信息。您将在此处获得整体状态,但不会获得详细信息。查看其他日志。
  • web.log - 有关与数据库的初始连接、数据库迁移和重新索引以及 HTTP 请求处理的信息。这包括与这些请求相关的数据库和搜索引擎日志。
  • ce.log - 有关后台任务处理以及与这些任务相关的数据库和搜索引擎日志的信息。
  • es.log - 来自搜索引擎的操作信息,例如 Elasticsearch 启动、健康状态变化、集群、节点和索引级别的操作等。
相关脚本脚本:

服务端相关命令

# 启动
sh SONAR_HOME/bin/linux-x86-64/sonar.sh start
# 停止
sh SONAR_HOME/bin/linux-x86-64/sonar.sh stop
# 重启
sh SONAR_HOME/bin/linux-x86-64/sonar.sh restart

执行分析

执行分析需要使用前面介绍的扫描器scanner

JAVA语言

java语言一般都是maven管理的项目,无需安装scanner,可以通过命令行直接分析报告

mvn clean package sonar:sonar \
    -Dsonar.login={sonar个人凭证} \
    -Dsonar.projectKey=qx_whale \
    -Dsonar.projectName=qx_whale \
    -Dsonar.host.url=http://10.10.10.15:9000

其他语言

在官方安装扫描器SonarScanner

/Users/admin/Downloads/sonar-scanner-4.7.0.2747-macosx/bin/sonar-scanner \
    -Dsonar.host.url=http://10.10.10.15:9000 \
    -Dsonar.login={sonar个人凭证} \
    -Dsonar.language=php \
    -Dsonar.projectKey=qx_whale2 \
    -Dsonar.projectName=qx_whale2 \
    -Dsonar.projectBaseDir=/code/myProject

Dsonar.projectBaseDir 指定代码分析路径

Dsonar.language 指定分析语言(默认根据扫描文件进行分析)

插件

sonarqube-ce有非常多的插件可以使用,详情可以参考官方支持的插件
在这里插入图片描述

分支插件

Sonarqube早在7.x版本是支持分支功能的,可以从分支进行代码分析,后来因为商业化演进,逐渐把分支功能给deprecated掉,合并到DE(开发者版本)收费。
分支功能可以让开发人员在测试分支尽早发现问题,设置收费这个很让人挺失望的。
好在国外大神把分支插件开发出来了,功能同7.x之前的基本一致,所以想要白嫖分支功能的话,大家可以参考以下示例去进行安装

  1. 版本介绍
    在这里插入图片描述

  2. 点这里,跳转到插件地址,根据您的版本进行插件选择。因为我使用的是8.9(LTS),所以我会选择1.8.1的插件去使用

在这里插入图片描述
在这里插入图片描述

  1. 移动到Sonarqube服务器的插件目录
cd {SONARQUBE_HOME}/extensions/plugins
wget https://github.com/mc1arke/sonarqube-community-branch-plugin/releases/download/1.8.1/sonarqube-community-branch-plugin-1.8.1.jar
### 记得修改权限,因为sonarqube需要通过sonar这个用户去启动
chown -R sonar:sonar sonarqube-community-branch-plugin-1.8.1.jar
  1. 编辑conf/sonar.properties,在最上面添加两行信息
sonar.web.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.1.jar=web
sonar.ce.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.1.jar=ce
  1. 重启sonar服务器,插件安装完成
### 切记,要切换sonar用户
su sonar
cd {SONARQUBE_HOME}
./bin/linux-x86-64/sonar.sh restart

如图显示,表示插件安装成功,接下来就可以愉快地使用分支功能了,点个赞吧
在这里插入图片描述

SonarQube集成Jenkins

  1. 在Jenkins上安装Sonarqube插件
    在这里插入图片描述

  2. 配置server信息

路径:系统管理-系统配置-SonarQube servers
注意,一般不配置凭证。
在这里插入图片描述

  1. 在Jenkins上配置Scanner信息

路径:系统管理-全局工具配置-SonarQube Scanner
配置scanner组件
在这里插入图片描述

  1. 在Sonarqube上配置令牌,用于Jenkins。
    在这里插入图片描述

令牌:03e27c52459e3c2156760fb1d8f9d24d59c7bf9e
5. 在Jenkins上构建一个流水线任务

Jenkinsfile

pipeline {
    agent any
    tools {
        maven 'maven' // jenkins配置的maven名称
    }
    stages {
        stage('SCM') {
            steps {
                git url: 'https://github.com/foo/bar.git'
            }
        }
        stage('build && SonarQube analysis') {
            steps {
                withSonarQubeEnv('My SonarQube Server') {
                    sh 'mvn clean package sonar:sonar -Dsonar.login=03e27c52459e3c2156760fb1d8f9d24d59c7bf9e'
                }
            }
        }
        stage("Quality Gate") {
            steps {
                timeout(time: 1, unit: 'HOURS') {
                    // Parameter indicates whether to set pipeline to UNSTABLE if Quality Gate fails
                    // true = set pipeline to UNSTABLE, false = don't
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }
}

相关术语解释:
-Dsonar.login:用户令牌(Sonar用户)

官方推荐使用流水线来进行代码质量分析
withSonarQubeEnv(‘My SonarQube Server’):表示引入’My SonarQube Server’这个环境
waitForQualityGate abortPipeline: true 表示等待sonarqube服务端返回质量阈报告,若质量阈不通过,则终止pipeline任务
插件介绍 https://www.jenkins.io/doc/pipeline/steps/sonar/
在这里插入图片描述

其他语言(通用)
通过流水线,对非maven项目进行代码质量分析,需要引入全局工具sonar scanner
在这里插入图片描述

Jenkinsfile

pipeline {
    agent any
    tools {
        maven 'maven' // jenkins配置的maven名称
    }
    stages {
        stage('SCM') {
            steps {
                git url: 'https://github.com/foo/bar.git'
            }
        }
        stage('build && SonarQube analysis') {
            steps {
                withSonarQubeEnv('My SonarQube Server') {
                    // 引用jenkins的全局工具defaultSonarScanner
                         def scannerHome = tool 'defaultSonarScanner';
                        sh "${scannerHome}/bin/sonar-scanner -Dsonar.login=03e27c52459e3c2156760fb1d8f9d24d59c7bf9e"
                }
            }
        }
        stage("Quality Gate") {
            steps {
                timeout(time: 1, unit: 'HOURS') {
                    // Parameter indicates whether to set pipeline to UNSTABLE if Quality Gate fails
                    // true = set pipeline to UNSTABLE, false = don't
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }
}

问题排查

Error 404 on http://10.14.8.157:9000/api/ce/task?id=AYBlkgJ_0a2bDva9JOF9

org.sonarqube.ws.client.HttpException: Error 404 on http://10.14.8.157:9000/api/ce/task?id=AYBlkgJ_0a2bDva9JOF9 : {"errors":[{"msg":"No activity found for task \u0027AYBlkgJ_0a2bDva9JOF9\u0027"}]}
	at org.sonarqube.ws.client.BaseResponse.failIfNotSuccessful(BaseResponse.java:36)
	at hudson.plugins.sonar.client.HttpClient.getHttp(HttpClient.java:38)
	at hudson.plugins.sonar.client.WsClient.getCETask(WsClient.java:51)

出现这个问题,不要害怕,应该是在sonar上删除项目并重建导致的,jenkins会在本地缓存报告的taskid,查看jenkins日志,你会发现出现了多个report-task.txt
在这里插入图片描述

解决方案:登录jenkins服务器,将.scannerwork/report-task.txt删除即可,一般target/sonar/report-task.txt存储的是最新的taskid。

Error 401 on http://10.14.8.157:9000/api/ce/task?id=AYCYfR4av8Ot_eXr5Wqi

org.sonarqube.ws.client.HttpException: Error 401 on http://10.14.8.157:9000/api/ce/task?id=AYCYfR4av8Ot_eXr5Wqi : 
	at org.sonarqube.ws.client.BaseResponse.failIfNotSuccessful(BaseResponse.java:36)
	at hudson.plugins.sonar.client.HttpClient.getHttp(HttpClient.java:38)
	at hudson.plugins.sonar.client.WsClient.getCETask(WsClient.java:51)

认证问题,一般调用waitForQualityGate()会遇到这样的问题。这个是插件的缺陷来的。
理论上,我们在withSonarQubeEnv()设置sonar.login表示通过这个用户去执行分析,并且希望在waitForQualityGate()执行时拿到sonar.login去获取回调结果,但结果是直接返回了401没有认证。

解决方案:通过sonarqube管理员生成令牌,然后去jenkins上生成secret key凭证,最后绑定到jenkins-系统管理-sonarqube servers-凭证。这样waitForQualityGate()在进行回调时就会使用sonar-admin的令牌去认证,401的问题就解决了。

有一点需要特别说明下,这是个比较粗糙的做法,但无意外它确实能帮助我们解决问题。

在项目组,我们要求每个sonar scanner运行时都必须指定sonar.login参数
在这里插入图片描述

小结

恰逢公司进行标准化建设,许多公共组件开始崛起,围绕着团队的工作效率展开。有幸地,我参与了本次代码质量分析调研,对我而言既是挑战,也是成长。从3月份底开始接触,到5月份初产出调研报告,中间各种工作堆积断断续续,估摸着总耗时72小时吧,大多数调研工作都是在夜里完成的,凌晨1~2点更不在话下,有辛酸。但往好处想,倘若通过这双手,将优秀工具的使用方法普及给团队,从而让团队前进一小步,这何尝不是一种奖励呢。

;