
1. 一份配置文件三种工程师浅谈 sonar-project.properties 里的工程素养分水岭深夜十一点半同事在群里发了一张 CI 日志截图配了一句“我明明配了 sonar-project.properties为什么扫描结果还是空的”我点开大图一眼就看到了 root 目录下那份文件的内容sonar.projectKeyabc、sonar.sources.、sonar.host.urlhttp://localhost:9000没了。这份一共三行的配置恰恰是问题所在。很多团队在引入 SonarQube 做代码质量门禁时把sonar-project.properties当成一个“填完交差”的任务只要扫描器能跑出个报告就算万事大吉。但实际上这份文件是整个静态分析流程的地基地基里藏着的每一个参数选择、每一段路径写法都在悄悄暴露配置者对 SonarQube 的理解深度——是只停留在“配了能用”还是真正把它当作工程体系的一环。这篇文章我不打算念官方文档而是从我这几年在 CI 里折腾 SonarQube 的实际经验出发讲讲一份sonar-project.properties里那些看着不起眼、真出事时能把人逼疯的细节以及我眼中不同段位的工程师在这份文件上会踩出什么不一样的脚印。先说结论放这里能跑通扫描的配置是及格线能控制扫描边界的是进阶级能在出问题时靠一份配置文件快速定位根因、并让质量门禁真正服务于团队的才算是把这份文件用明白了。2. 低级错误集中营host、路径和 projectKey 的三宗罪2.1 host.url 配成了 localhostCI 上永远连不上我见过最普遍的低级错误是把sonar.host.url写成http://localhost:9000。本地开发机上跑Scanner 确实能连上报告也正常。但一旦配置提交到 CI 流水线跑扫描的 Runner 和 SonarQube 服务器根本不在同一台机器上这个 localhost 指向的是 Runner 自己端口 9000 上什么都没有于是报错INFO: Scanner configuration file: /opt/sonar-scanner/conf/sonar-scanner.properties INFO: Project root configuration file: /workspace/sonar-project.properties ERROR: Error during SonarScanner execution ERROR: Failed to request http://localhost:9000/api/...很多人看到这个报错就去怀疑网络、怀疑防火墙、怀疑容器网络模式折腾一圈才发现就是配置文件里这个 localhost 在作祟。这事的根因是混淆了“Scanner 进程所在的地方”和“SonarQube 服务端所在的地方”。sonar.host.url是给 Scanner 看的它要告诉 Scanner 把分析结果提交到哪里所以这个地址必须是从 Runner 视角能访问到的地址。在 Docker 化的 CI 环境里常见做法是用服务名比如http://sonarqube:9000或者内网域名而不是 localhost。提示如果改了sonar.host.url还是连不上先别急着查网络在 Runner 上直接 curl 一下这个地址的/api/system/status返回值里有status: UP才说明物理链路通。我排过太多莫名其妙的扫描失败最后都是这个 curl 先给出答案。2.2 projectKey 重名两个项目的代码数据悄悄搅在一起sonar.projectKey是 SonarQube 里项目的唯一标识这个 key 一旦和已存在的项目重复Scanner 不会报错而是直接把当前这次分析的数据并入已有项目。后果就是A 项目的 issues 统计里混进了 B 项目的代码行数质量门禁的结果张冠李戴整个报表都变成一笔糊涂账。我印象很深的一次事故是有个团队把微服务拆成了十几个仓库每个仓库的sonar-project.properties都是从第一个仓库复制过来的唯独忘了改projectKey。结果一个月后复盘时发现所有仓库的扫描结果都堆在同一个项目名下而真正的十几个服务各自的质量曲线全是平的。规范做法是让projectKey具有仓库级别的唯一性。一般建议格式是组织名_仓库名或者干脆跟 CI 里的环境变量绑定。比如在 Jenkins Pipeline 里这样写sonar.projectKey${PROJECT_KEY} sonar.projectName${PROJECT_NAME}然后PROJECT_KEY在 Jenkins 任务里按仓库配置从源头杜绝复制粘贴导致的 key 撞车。2.3 sources 的路径玄学.不是万能的sonar.sources指定的是源码目录。新手最爱的写法是sonar.sources.意思是把整个项目根目录都交给扫描器。这样写在简单工程里没问题但一旦项目里有生成代码、有第三方库源码、有大段大段的测试资源文件问题就来了。Scanner 会老老实实地把所有能识别的文件都拉进分析范围。生成代码里那些自动产生的 getter/setter 会被当成坏味道检出来target目录里编译产物如果被当作源码扫描规则误报能多到让人怀疑人生。而且分析范围越大扫描时间越长增量分析的优势被彻底浪费。正确的姿势是显式列出源码目录并且支持 Ant 风格的路径通配sonar.sourcessrc/main/java,src/main/resources sonar.testssrc/test/java注意sonar.sources和sonar.tests是两套独立配置。很多人只配 sources忘了配 tests导致测试目录里的文件全被当成生产代码扫描测试相关的规则比如测试覆盖率、测试命名规范全部失效。3. 中段位的分水岭二进制依赖、编码和模块化这三个魔鬼3.1 java.binaries 缺失时规则误报的雪崩效应Java 项目里如果你只配了sonar.sources而没配sonar.java.binariesScanner 会打出这样的警告WARN: No binaries found in the project. The following issues might be false positives.这个警告的杀伤力被绝大多数人低估了。SonarQube 的分析不是简单的正则匹配它要做语义级别的分析比如检查空指针、检查类型一致性、检查继承关系。这些分析都需要编译后的字节码作为支撑。没有二进制文件分析器只能做文本层面的猜猜出来的结果里大量误报。我之前帮一个团队排查过一个诡异的现象他们项目里明明没有空指针风险但 Sonar 筛出了十几个Possible null pointer dereference。后来发现他们的 CI 流程里编译步骤在扫描步骤之后执行sonar.java.binaries指向的target/classes目录是空的。正确的配置非常直接sonar.java.binariestarget/classes sonar.java.librariestarget/dependency/*.jarsonar.java.libraries用来告诉分析器项目的第三方依赖在哪里有它和没它的区别在于分析器能不能准确判断某个方法可能抛出的异常类型。在 Maven 构建里这一步一般不需要手动维护因为 sonar-maven-plugin 会自动填充。但如果你用的是 sonar-scanner 命令行模式这两个参数就得自己盯好。3.2 sourceEncoding 引发的乱码误报连锁反应sonar.sourceEncoding是我见过被跳过最多的参数。不写它Scanner 默认按平台编码解析Windows 上的 GBK 编码源码传上去在 SonarQube 服务端按 UTF-8 解码中文注释全部变成乱码。乱码本身不算问题但注释乱码会影响注释相关规则判断字符串字面量乱码会导致字符相关的规则误报最要命的是硬编码密码、硬编码 IP 这类规则会因为你源码里中文字符被错误解码而漏掉或误伤。正确姿势就一行sonar.sourceEncodingUTF-8但真正专业的做法是不仅在配置文件里声明还要保证整个工具链从 Git 提交到文件编码再到 CI 环境变量都是 UTF-8 一以贯之。我在 CI 脚本里一般会加一句export LANGen_US.UTF-8 export LC_ALLen_US.UTF-8别小看这个环境变量它决定了 JVM 在读取文件时的默认编码行为。有时候配置文件写对了但 JVM 的 file.encoding 不对Scanner 照样解析出乱码。3.3 多模块项目的 modules 配置一份文件如何管理一堆子工程当一个仓库里有多个 Maven 模块比如 common、api、web用 sonar-scanner 命令行跑分析时如果只在根目录配一个sonar.sources分析结果会把所有模块的代码揉成一个整体模块级别的指标统计全部缺失。稍微有经验的工程师会这样配sonar.modulescommon-module,api-module,web-module common-module.sonar.projectNameCommon Module common-module.sonar.sourcescommon/src/main/java api-module.sonar.projectNameAPI Module api-module.sonar.sourcesapi/src/main/java这种写法的本质是用模块名.参数名的命名空间划分作用域。它能解决模块归属问题但我要提醒一个坑如果你用 Maven 插件而不是命令行 scannersonar.modules根本不用手写插件会自动从 POM 的模块结构推断出来。手工配置 modules 反而容易和 Maven 的结构产生冲突导致分析范围出现重复或遗漏。我的建议是能用 Maven 插件 / Gradle 插件做分析的就不要手写 scanner 配置。插件能自动推导的东西比人肉维护可靠得多。手写配置的场景是那些没有标准构建工具的项目或者构建产物不在本地、需要独立扫描的 CI 阶段。4. 高段位玩家的底牌日志、增量分析与质量门禁实战4.1 verbose 日志里藏着的真相sonar.verbosetrue这个参数平时用不上出问题时它是第一手的诊断依据。Scanner 执行时在控制台输出的日志默认只到 INFO 级别很多关键的内部决策根本看不到。开了 verbose 之后你能看到每个 sensor 的执行时长、每条规则对每个文件的处理结果、加载了哪些配置文件、走了哪些默认值。我处理过一个非常典型的案例有一段时间 CI 上扫描时间从 3 分钟暴涨到 15 分钟查了很多方向都没头绪。最后开了 verbose 日志发现在分析 JavaScript 文件时SonarJS插件反复对同一个文件跑了三轮缓存重试原因是资源目录下的.min.js文件太大分析器里有个默认的上限判断超过之后自动回退到旧路径。verbose 日志里一行Retrying... due to memory limit直接给出了答案。提示sonar.verbosetrue只建议在排查问题时开平时开着会把日志量放大好几倍对 CI 日志存储不友好。4.2 增量分析让质量门禁在提交阶段就发挥作用很多人对 SonarQube 的印象还停留在“每天晚上定时跑一次全量扫描”。但真正让质量门禁产生约束力的用法是把它嵌进 MR / PR 的流水线里做增量分析。增量分析的关键配置参数并不是写在sonar-project.properties里的而是通过 Scanner 命令行传入sonar-scanner \ -Dsonar.projectKeymy-service \ -Dsonar.branch.name${CI_COMMIT_REF_NAME} \ -Dsonar.analysis.commitId${CI_COMMIT_SHA} \ -Dsonar.qualitygate.waittrue \ -Dsonar.qualitygate.timeout300sonar.qualitygate.waittrue是让扫描进程等到质量门禁结果返回后才退出这样流水线才能根据退出码决定构建是否继续。sonar.branch.name配合 SonarQube Developer Edition 以上的分支分析功能能按分支区分问题归属新代码引入的问题和新代码覆盖率才有意义。用参数而不是配置文件传入是为了让同一份sonar-project.properties适配不同的运行环境。配置文件里只放仓库级不变量环境相关的东西分支名、commit id、token全部从 CI 变量注入这样一份配置多个流水线共用不会因为分支切换而改来改去。4.3 收紧 exclusions 与 coverage.exclusions用户数据和生产代码的取舍每个 SonarQube 项目都会有那么几个目录用户协议模板、自动生成的 SDK 模型、本地化资源文件。这些代码的质量指标没有实际参考意义但不排除的话它们会持续拉低整体评分导致开发对门禁指标失去信任。高段位的做法是分两类排除sonar.exclusions**/generated/**,**/third_party/** sonar.coverage.exclusions**/models/**,**/dto/**注意这两个参数的区别。sonar.exclusions是完全不分析文件不会进入任何指标统计。sonar.coverage.exclusions是参与问题分析但不纳入覆盖率分母。我见过有人为了刷覆盖率把一半业务代码都塞进了coverage.exclusions结果覆盖率数字好看线上 Bug 一个没少。正确的逻辑应该是覆盖率排除只针对那些不值得测的代码比如纯 DTO、配置文件加载器业务核心逻辑必须留在覆盖率统计里。这个边界需要团队自己定义但定义得越清楚门禁的可信度越高。5. 一次真实的上线事故复盘从 CI 红灯到根因定位的 40 分钟5.1 事故现场事情发生在一个周五下午某个服务在发布流水线里突然挂掉卡在 Sonar 扫描这一步。报错内容非常经典ERROR: You must first install the License plugin. ERROR: Please install it and restart SonarQube.第一反应是 SonarQube 服务端的问题。毕竟报错指向 License 插件缺失运维查了一轮插件列表所有付费插件都正常安装。这时候群里已经有人开始怀疑是新版 Scanner 和服务端版本不兼容。5.2 排查链路我打开那条失败任务的完整日志注意到一个细节报错之前有一行不起眼的 INFOINFO: SonarQube server 9.9.0 | Scanner 6.2版本的组合看起来没问题。继续往上翻看到 Scanner 加载时提示INFO: Load project settings INFO: Load project settings (DONE) INFO: Load quality profiles到这里一切正常。直到出现INFO: Load plugins WARN: Plugin license is not compatible with SonarQube version 9.9问题来了。License 插件是服务端的但 Scanner 在解析sonar-project.properties时如果配置里有sonar.license.secured之类的参数Scanner 会尝试加载对应的扩展插件机制。翻到那个服务的配置文件sonar.license.secured${SONAR_TOKEN}这行配置的本意是把 Token 传给服务端校验但因为参数名带了license前缀Scanner 误以为是针对 License 插件的专用参数走了插件加载逻辑然后发现插件版本不匹配直接抛错。5.3 修复方案定位到根因后修复就很简单了。把这个参数改成标准写法sonar.login${SONAR_TOKEN}或者更安全的做法是用sonar.token新版 Scanner 推荐sonar-scanner -Dsonar.token${SONAR_TOKEN}这次事故让我总结了三条排查经验扫描报错时先看 Scanner 的插件加载日志它远比报错信息本身有细节配置文件里任何参数都别乱起名字sonar.*前缀的参数有命名空间约束不是随心所欲的团队里有人复制了别人的.properties文件去改最常见的坑就是带着上一个人的参数习惯改完 key 忘了清掉无关联的配置提示sonar.verbosetrue在这种场景下其实也有用但如果你不想重启流水线直接把sonar-project.properties里可疑参数逐行注释掉、逐项排查往往比看日志更快。二分法注释参数是排查配置问题最朴素也最有效的手段。6. 珍藏的模板一份生产级 sonar-project.properties 该长什么样说了这么多反面案例最后给一份我在多个团队里推广过的生产级配置模板每一行都注释清楚为什么存在# 项目唯一标识格式: 组织_仓库整个 SonarQube 实例内唯一 sonar.projectKeytechshare_payment-service # 项目展示名称只影响 UI 显示 sonar.projectNamePayment Service # 项目版本号建议直接绑定 CI 的构建号 sonar.projectVersion${BUILD_NUMBER:-1.0.0} # 服务端地址绝对不允许 localhost sonar.host.urlhttp://sonarqube.internal.example.com:9000 # 源码与测试目录显式列出避免误扫 sonar.sourcessrc/main/java,src/main/resources sonar.testssrc/test/java sonar.java.source11 sonar.java.target11 # 编译产物目录Java 语义分析的基础 sonar.java.binariestarget/classes sonar.java.librariestarget/dependency/*.jar # 编码统一 UTF-8 sonar.sourceEncodingUTF-8 # 排除生成代码和第三方代码 sonar.exclusions**/generated/**,**/build/**,**/resources/static/vendor/** # 覆盖率分母排除项仅限不值得测的死代码 sonar.coverage.exclusions**/dto/**,**/domain/entity/** # 调试选项平时保持关闭 sonar.verbosefalse # 文件大小上限超过的生成文件不参与分析防止内存溢出 sonar.analysis.maxFileSize5000 # 自定义参数可以在 SonarQube 的 Webhook 或 API 里读取 sonar.analysis.buildTimestamp${BUILD_TIMESTAMP}这份模板里有两个参数值得多说两句。sonar.analysis.maxFileSize的单位是 KB超过大小的文件会被忽略。这个参数不写在官方推荐列表的前排但对某些前端项目非常有用——打包后的 vendor.js 动辄一两兆不限制的话分析器会在这一个文件上消耗大量内存甚至直接 OOM。sonar.analysis.*前缀的参数是自定义属性你可以把任意上下文信息塞进去比如构建时间、发布负责人、变更单号。这些信息会随分析结果一起提交到服务端在项目页面和 Webhook 回调里能取到。我见过有团队利用这个机制在告警通知里自动带上“这次是谁发布的”省去了翻 CI 记录的麻烦。如果项目是多模块结构优先用构建工具的插件不要在手写配置里维护sonar.modules。上面这份模板对应的是单模块项目的标准场景多模块项目请把同样内容分散到各子模块的配置里或者干脆切到sonar-maven-plugin/sonar-gradle-plugin让插件去处理模块关系。7. 我在生产环境里反复踩过的三个附加提醒这份配置文件还有一个经常被忽略的属性它的加载优先级。Scanner 启动时会依次加载默认配置、$SONAR_SCANNER_HOME/conf/sonar-scanner.properties、项目根目录的sonar-project.properties、命令行-D参数。优先级是后加载覆盖前加载。命令行参数永远高于文件配置这在 CI 里是件好事但也意味着如果你在 Jenkins 任务里留了旧的-Dsonar.host.url参数它会静默覆盖文件里的新地址排查问题时容易一脸懵。另外要提醒的是SonarQube 服务端和 Scanner 的版本兼容矩阵是需要定期检查的。Scanner 7.x 配 SonarQube 9.x 一般没问题但有些新 Scanner 版本会弃用旧参数旧的 Scanner 会不支持新服务端的特性。每次升级前先翻一眼官方兼容性表格别让版本差异成为下一个“报错找不到根因”的素材。最后一条是关于团队协作的sonar-project.properties应该在代码评审里被认真对待。这份文件平均二十几行但它决定了代码质量工具的数据基础。我见过太多团队把这份文件的修改当成“无关紧要的配置变更”随手合并、不评审结果一两个月后扫描数据偏离实际再想纠正就要重跑全量历史数据了。提示如果在 PR 里看到有人改了sonar.exclusions多问一句“为什么排除这些文件”。合理的排除有清晰的业务理由不合理的排除往往是为了让失败的门禁变绿。这行代码的变更比很多业务代码的变更都更需要 review 关注。最后说点实在的写这份配置的过程中我自己也吃过不少亏。从最开始把sonar.login写成明文密码提交到仓库被人扫出来到后来在exclusions里手滑多写了一个**导致整个src目录被跳过、门禁全绿但实际什么都没分析——都是在具体环境里交了学费才记住的教训。如果只能从这篇文章里带走一件事我希望是把sonar-project.properties当作代码来写。它有语法、有边界、有调试方法也有评审价值。不要把它当成一份填完就忘的表单而是当成和pom.xml、package.json同级的基础设施配置来对待。这样当你看到扫描结果异常的时候才会本能地想起先去看一眼这份文件——而很多时候答案真的就在那里。