ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Simian精准定位重复代码,在CI中守住代码质量门禁

用Simian精准定位重复代码,在CI中守住代码质量门禁 简介代码重复检测工具 SimianSimilarity Analyser的完整发行包支持 Java、C#、C、C、JavaScript 等多种语言面向开发与测试人员用于在持续集成与代码审查中快速定位重复代码、降低维护成本。压缩包共 59 个文件、约 3.43MB核心内容包含 jar/exe 可执行程序、38 个 HTML 帮助文档、DTD/XSL 报表样式定义、运行所需的 DLL 与图片资源以及许可证与说明文本其中帮助文档覆盖功能介绍、安装指南、客户案例等目录结构清晰便于本地部署查阅。目前已有 1410 人浏览/学习。通过本包可获得开箱即用的 Simian 工具及完整文档既可直接在命令行或构建脚本中使用也能借助 Javadoc 了解 API 与检测规则实际检测时可根据项目情况配置敏感度阈值并输出重复代码报告供团队整改。持续使用可有效减少冗余代码提升代码整洁度与软件稳定性。 维护过老项目的朋友大概率都经历过这样的事一个线上 bug 明明已经修好隔几天又冒出来排查到最后发现同一个逻辑在另一个文件里还有一份几乎一样的拷贝刚才的修复压根没覆盖到。这种问题的根子就出在重复代码上而我今天要聊的 simian就是专门用来定位这类问题的代码重复检测工具。simian 全名 Similarity Analyser是一个命令行工具目标很单纯扫一遍代码库把重复的代码块全部揪出来。不管是 Java、C#、JavaScript 还是 Python只要把源码喂给它它就会基于词法分析找出结构相似的片段输出一份可读的报告。它特别适合几类人天天跟遗留系统打交道的开发、准备做大规模重构的团队、想在 CI 里加一道自动质量门禁的运维。接下来我从原理、参数、CI 集成到落地经验完整走一遍。1. 为什么先要跟重复代码较劲1.1 重复代码的三个代价很多人对重复代码的第一反应是“不好看”但不好看只是最轻的代价。真正的代价在维护阶段才会爆发出来。第一个代价是修复漏洞要修多份线上环境里同一个逻辑出现两次就意味着你每次排查都要把所有副本找齐漏掉一个就等于没修。第二个代价是需求变更要在每个副本上重复实现加一个字段、改一个状态判断原来改一处的地方现在要改五处改完还得祈祷副本之间没有细微差异。第三个代价是认知负担新接手项目的人看到五六份“几乎一样但又不完全一样”的代码根本分不清哪一份才是可靠版本连改 bug 都不知道以哪份为准。这三个代价叠加在一起就形成了典型的“代码腐化”正循环重复越多越不敢改越不敢改重复越多。等到团队决定重构的时候才发现连重构的边界都很难划清楚因为同样的逻辑散落在不同的包和模块里。所以工具检测重复代码本质上是在给团队的“技术债”做体检把最值得还的那部分债务先找出来。1.2 simian 的检测原理比你想的更聪明simian 不是拿 diff 去逐行比较文本而是先把源代码切分成 token 流。token 就是代码里的最小语法单元比如关键字、变量名、数字、字符串、运算符每个都对应一个类型。切完之后simian 会在这些 token 序列里寻找重复出现的子序列只要模式足够相似就会被识别为重复块。这意味着什么意味着它抓的是“结构”而不是“字面”。举个例子下面两段代码变量名完全不同int a 10; a a 1; System.out.println(a);int b 20; b b 1; System.out.println(b);普通文本 diff 会认为这是一大段差异但 simian 在归一化变量名之后会认定这两段是重复代码。这正是我们真正需要关心的重复逻辑结构相同、只是名字不同日后改逻辑的时候还是得改多份。用通俗的话说它干的事情像用指纹比对去识别同一个人而不是去比对照片的清晰度。另外simian 是直接扫描源码的不需要先编译也不需要项目能跑起来。这对大型遗留系统特别友好很多老项目连依赖都拉不下来、根本编译不过但只要你手里的源码是完整的simian 就能扫。1.3 同类工具怎么选市面上能检测重复代码的工具不少我用得比较多的主要是这三个。对比项simianPMD CPDJSCPD支持语言40 种覆盖主流与冷门语言约 20 种偏后端生态主要针对 JavaScript / TypeScript分析方式token 级词法分析token 级词法分析AST / token 混合输出格式text / XML / JSON / HTML 等text / XML / CSV 等JSON / HTML 等CI 集成命令行 退出码容易接入命令行 Maven/Gradle 插件npm 脚本授权商业授权也有免费社区版开源免费Apache 2.0MIT 开源简单来说追求开源、团队预算敏感PMD CPD 是非常好的备选纯前端项目用 JSCPD 最省事但如果你的项目是个多语言仓库或者你希望一个 jar 包就能搞定所有语言、配置项又足够细simian 更省心。我下面的例子都以 simian 为准因为它在 CI 里的“一行命令 退出码”方案实在太方便了。2. 5分钟跑通第一份重复报告2.1 下载与准备simian 的官方发布形式是一个独立 jar 包不需要安装客户端。你只需要在机器上装好 Java 8 以上的运行环境然后去官网把 jar 包下载下来放到一个固定目录。我自己习惯放在~/tools/simian.jar这样所有项目都能共用。记得确认一下 Java 环境正常java -version能正常打印版本号就行。接着下载 simian 的 zip 包解压后找到里面的 jar 文件。我用的版本是 2.5.x命令行参数在这几个版本里变化不大你可以放心照着下面的命令操作。2.2 第一条扫描命令与文本报告进入你的项目根目录执行cd /path/to/your-project java -jar ~/tools/simian.jar -includes**/*.java -threshold6 -reportFormattertext这里有几个参数要解释一下。-includes指定要扫描的文件模式支持通配符**/*.java表示递归匹配所有子目录下的 Java 文件。-threshold6表示重复块小于 6 行的直接忽略这个默认值也是 6一般不建议一开始调太低否则报告会非常长。-reportFormattertext指定输出纯文本报告。在 macOS 或 Linux 的 shell 里引号是必须的不然 shell 会把**展开成具体文件列表导致参数走样Windows PowerShell 下同样建议保留引号。跑完之后文本输出大致是这个样子Simian 2.5.10 - Similarity Analyser Checking 1,245 files Found 24 duplicate blocks of repeated code across 38 blocks in the set * Duplicate 1 lines: 12 in file: src/main/java/com/example/OrderService.java starting at line 45 in file: src/main/java/com/example/PaymentService.java starting at line 130最后一行很关键它明确告诉你在哪个文件、哪一行到哪一行重复了。第一次跑完看到几十个 duplicate 很正常千万别慌这正好说明检测工具在发挥作用。2.3 XML 报告也能导出文本报告适合人看但如果你想把扫描结果归档、生成自定义图表或者交给其他工具消费建议导出 XML 格式。命令也很简单java -jar ~/tools/simian.jar -includes**/*.java -threshold6 -reportFormatterxml -outputFilesimian-result.xml-outputFile把报告写进文件不会刷屏。XML 里会包含每个重复块的文件路径、起始行、行数等结构化信息方便脚本解析。如果你喜欢更直观的网页展示也可以试试-reportFormatterhtml生成一份带样式 HTML直接扔给团队成员看比发一段命令行输出要友好得多。3. 参数调优让报告真正可执行3.1 threshold 阈值怎么定-threshold是我用下来影响最大的参数。设得越低报告越长误报也越多设得越高报告越短但那些“只有 10 行重复但影响很大”的问题可能会漏掉。我的经验是分项目类型来定。老项目、历史包袱重的先跑-threshold20或者-threshold15目的是把“巨型重复块”先暴露出来这类重复通常最值得重构。新项目、代码质量要求高的可以直接上-threshold6甚至压到-threshold4重点盯紧新增代码有没有走捷径复制粘贴。我更推荐的做法是“探测法”先跑一个更宽松的阈值比如 20看报告总量如果结果很少或者没有再把阈值降到 15、10、6直到报告数量达到一个“能看完但不会太少”的平衡点。对我而言这个平衡点通常是在 50 到 100 个重复块之间再多了团队看了就麻木无法形成行动力。3.2 忽略规则不是越多越好simian 提供了一批-ignore开头的参数用来忽略一些细节差异比如字符串、数字、变量名、花括号位置、标识符大小写等。我常用的组合是java -jar ~/tools/simian.jar -includes**/*.java -threshold8 -ignoreStringstrue -ignoreVariableNamestrue -ignoreCurlyBracestrue-ignoreStringstrue会忽略字符串字面量本身的差异比如一个项目里两段代码结构一样只是提示文案不同打开这个开关之后它们会被视为重复。-ignoreVariableNamestrue会忽略变量名的差异前面举的int a和int b的例子就是靠这个参数识别为重复的。-ignoreCurlyBracestrue用于忽略花括号换行位置的不同在 C 系语言里经常能减少一批不痛不痒的报告。但有一点要泼冷水忽略规则不是开得越多越好。比如你把-ignoreStringstrue开了那么“两段逻辑完全相同、只有硬编码文案不同”的重复就不会被报告出来而这恰恰是修改时最容易被漏掉的那部分。我通常的做法是先不开任何忽略规则跑一遍看误报的真实占比再针对性打开某几个。千万不要为了“让报告变绿”而把所有忽略开关都打开那等于白跑。3.3 针对不同语言的配置细节simian 对大部分语言是自动识别扩展名的但有些场景需要手动指定。比如 C 的头文件和源文件混在一起、或者自定义了文件后缀可以用-languagecpp强制指定避免识别失败或误判。支持的语言里我常用到的有java、csharp、cpp、python、javascript等。Python 项目要特别注意缩进敏感的问题。Python 代码块本来就靠缩进区分重复检测的“行数”和实际逻辑块在视觉上会有偏差经验是先不要把 threshold 压得太低8 行起步跑完再人工翻一下报告。C/C 项目里头文件里大量宏声明、函数声明很容易因为结构相似被误报这时候用-excludes**/*.h或者把外部依赖目录排除掉报告会干净很多。另外还有一个-ignoreBlocks参数可以跳过你指定的代码块类型。比如项目里有一段自动生成代码或序列化定义不想被纳入重复检测可以配置对应的块模式让 simian 在扫描时直接跳过。4. 把 simian 变成团队的质量门禁4.1 脚本与 Maven 里的落地方式命令行工具要变成团队都能用、能接进流程的东西第一步是把它封装成脚本。我通常会在项目根目录放一个simian-check.sh#!/usr/bin/env bash set -euo pipefail SIMIAN_JAR${SIMIAN_JAR:-$HOME/tools/simian.jar} CHECK_DIR${1:-src/main/java} THRESHOLD${THRESHOLD:-8} java -jar $SIMIAN_JAR \ -includes$CHECK_DIR/**/*.java \ -threshold$THRESHOLD \ -failOnDuplicationtrue \ -reportFormattertext这里的核心是-failOnDuplicationtrue。它会让 simian 在发现重复代码时返回非 0 的退出码这样set -e的脚本就会直接失败整个流程被卡住。这个开关是把它变成质量门禁的关键。Maven 项目的话可以用 exec-maven-plugin 在 verify 阶段调用。大致配置如下plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.1.0/version executions execution phaseverify/phase goals goalexec/goal /goals configuration executablejava/executable arguments argument-jar/argument argument${project.basedir}/tools/simian.jar/argument argument-includes${project.basedir}/src/main/java/**/*.java/argument argument-threshold8/argument argument-failOnDuplicationtrue/argument /arguments /configuration /execution /executions /plugin这样跑mvn verify的时候如果有重复代码构建就会红掉。不过 exec 插件的参数引号在不同环境下坑比较多我更推荐直接用脚本CI 里调脚本最省事。4.2 GitLab CI 里加一道闸接入 GitLab CI 时我的.gitlab-ci.yml里一般会多一个 jobsimian-check: stage: test script: - java -jar tools/simian.jar -includessrc/main/java/**/*.java -threshold8 -failOnDuplicationtrue -reportFormatterxml -outputFilesimian-report.xml artifacts: paths: - simian-report.xml when: on_failure跑失败时XML 报告会自动作为构建产物保存下来开发直接下载报告就能看到具体是哪几个文件、哪几行重复不用去翻 CI 日志。如果是 Jenkins思路完全一样执行一段 shell 命令再把输出文件归档就行。这里有一个实践上的建议如果你第一次在存量项目上开这道闸千万不要直接用最严格阈值并立刻 fail。先跑几天“仅扫描、不拦人”的巡检模式收集基线数据再根据基线逐步收紧。不然团队第一天的 CI 就是红的接下来就是无尽的豁免申请和抱怨。4.3 阶梯式目标别一刀切把重复检测作为长期门禁我建议分三个阶段走每个阶段对应一个明确的阈值和目标。阶段阈值目标门禁状态第一阶段30清理超大块重复解决最危险逻辑拷贝仅记录不强制失败第二阶段15消灭跨模块、跨服务重复推进核心抽象扫描 警告鼓励整改第三阶段6-8常规代码重复长期可控CI 强制失败第一阶段和第二阶段可以交替进行比如先把 30 行以上的重复清完再把阈值降到 15。这个过程其实是在用工具引导团队“由大到小”地还债比“一刀切必须低于某个指标”更现实也不会让团队觉得你在拿工具卡人。5. 常见问题与团队落地经验5.1 误报太多先别急着关参数使用 simian 最常见的抱怨就是误报。比如大量的 DTO、getter/setter、测试脚手架被判定为重复。我处理这类问题的顺序是先看报告分布再决定怎么处理。现象常见原因处理方式DTO / getter/setter 大量重复样板代码结构天然一致用-excludes排除相关目录或-ignoreBlocks跳过样板块测试数据、字符串常量重复测试里同一组数据出现多次提取公共测试常量或排除测试资源目录自动生成代码被报告生成的代码存在拷贝排除generated-sources、target等目录注释内容带来干扰相同注释被多处复制关注代码逻辑肉眼过滤或配置忽略注释块另外我强烈建议把第一次扫描的结果保存下来作为基线后续每优化一轮就重新扫一次对比重复块数量是涨是跌。只看“最近有没有新增重复”比看“总量还有多少”更有指导意义。5.2 从报告到重构的推进顺序拿到报告之后不要试图一次性把所有重复都清掉。我比较推荐的顺序是先处理同一文件内的重复再处理跨文件重复最后处理跨模块重复。同一文件内的重复风险最低抽个方法或者提取局部工具函数就能解决适合给团队建立信心。跨文件重复通常意味着需要抽取公共类或工具类风险中等。跨模块重复是优先级最高的因为它往往暴露了模块边界划分不合理这种也不建议在普通迭代里顺手改最好单独排重构任务。每次改完重新跑一遍 simian确认相关重复块从报告里消失并且重复数没有因为“复制出一个新变体”而增加。这个“改前跑一遍、改后再跑一遍”的习惯价值非常大。5.3 团队推行重复检测的三条建议第一条先出基线报告让大家对现状有共识。很多团队对“代码重复严重”没有量化概念你把第一份扫描结果贴到项目群里比嘴上反复强调管用得多。第二条门禁放在评审之前。在 CI 里把-failOnDuplicationtrue打开合并请求不通过重复代码就不会流到主干。注意要配合阈值阶段调整避免短时间内把存量项目全部卡死。第三条允许阶段性豁免但要有截止时间。对历史模块可以先放进白名单只要求新代码和被改动到的代码通过检测同时约定一个清理老债务的排期。这样团队不会因为“存量太重”产生对抗情绪新代码的质量也能持续守住。我个人在实际操作中的体会是simian 不会替你把重复代码改好但它能把藏了多年的卫生死角一次性拍在你面前。我第一次扫一个模块时发现一段五十行的事务处理逻辑被复制到了五个地方当时第一反应不是生气而是庆幸现在才知道。从那以后我每次大重构之前都会先跑一遍 simian把重复清单当作战地图用。建议你也找个不忙的下午把项目悄悄扫一遍然后再决定要不要让团队把这道门禁开起来。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进