简介:Simian(Similarity Analyser)是一款代码重复检测工具,适合Java、C#、C/C++、JavaScript等多语言项目开发者,用于定位冗余代码与复制粘贴片段,降低维护成本与缺陷风险。压缩包为工具完整发布包,共59个文件、3.43MB,以可执行JAR/EXE和运行依赖DLL为核心,同时包含官方HTML帮助页、PDF授权说明、DTD/XSL报告格式定义、界面图标等,便于快速接入Maven或Gradle构建流程并设定检测规则。已有1410人学习下载。包内附有JDK 1.5.0_13相关日志、版本更新、安装指南及客户案例页面,适合在持续集成场景中实施重复代码检查的团队参考。借助Simian生成的重复代码报告,开发者可以精准定位问题模块并重构,持续保障代码整洁度与可维护性。 做代码评审的时候,我最不想看到的不是那种逻辑绕三圈的烂代码,而是一看就知道是复制粘贴过来的重复代码。逻辑复杂最多是让人头大,重复代码是真的埋雷——修 A 处的时候忘掉 B 处,线上问题就是这么来的。后来我开始在项目里引入simian这个轻量级的代码重复检测工具,每周扫描一次,把重复点直接列成清单丢给相关人员去处理。今天就把这套用法和踩过的坑整理出来,给也在搞代码治理的朋友一个参考。
simian 的定位很纯粹:它就是一个命令行下的重复代码检测器,基于 Java 运行,整个工具只有一个 jar 包,不需要装数据库、不需要启动服务、不需要引入一堆插件。你给它一堆源码文件,它告诉你哪些地方重复了,重复了多少行,严重程度有多高。适合谁用?技术负责人、做工程质量改进的工程师、搭 CI 流水线的同学,以及所有被重复代码坑过、想给代码库做一次“体检”的人。
1. 为什么我会盯上 simian 这个重复代码检测工具
1.1 重复代码到底有多大杀伤力
先说说我为什么对重复代码这么敏感。以前维护过一个订单模块,里面有几个方法做金额格式化,逻辑几乎一样,只是字段名不同。有一次业务规则调整,要求金额保留两位小数,我在主流程方法里改了,以为完事了,结果测试还报错。查了半天,发现同一个格式化逻辑散落在四个方法里,我只改了其中一个。这种问题靠人工 review 很难根治,因为人总是会漏,而自动化工具不会。
重复代码的代价是叠加的:可读性上,读代码的人要反复跳过相似片段,理解成本翻倍;维护上,一处逻辑更新,其他复制点容易成为漏网之鱼,形成“改了这一处、漏了那一处”的经典事故;测试上,相似代码往往没有独立测试覆盖,出问题要靠线上用户帮你发现。更隐性的是,重复往往是设计问题的信号——该抽方法没抽、该建父类没建、该用策略模式却图省事直接复制。所以重复检测不只是“找重”,更是帮团队发现代码结构上的坏味道。
1.2 同类工具不少,为什么先用 simian 试刀
代码重复检测这个领域其实不算冷门,我当时先列了几个备选:PMD 自带的 CPD、SonarQube 的重复度分析、jscpd、还有 .NET 圈的 dupFinder。纠结了半天,最后先用 simian 跑了半个月,原因是它足够轻,接入成本无限接近于零。
| 工具 | 运行方式 | 跨语言 | 接入成本 | 适用场景 |
|---|---|---|---|---|
| simian | 单 jar,Java 环境即可 | 支持 C/C++、Java、C#、JS 等十几种 | 极低,命令行直接跑 | CI 快速反馈、轻量扫描 |
| PMD CPD | 随 PMD 分发 | 多语言 | 较低,但规则体系较重 | Java 项目已有 PMD 时 |
| SonarQube | 独立服务端 | 多语言 | 高,要建服务、配库 | 全链路质量平台 |
| jscpd | Node CLI | 多语言 | 中,依赖 Node 生态 | 前端项目常用 |
| dupFinder | JetBrains 工具链 | C# 为主 | 中 | .NET 项目 |
对一个以 Java 为主、CI 已经用 Jenkins 的老项目来说,simian 那种“拿 jar 就能跑”的风格太适合了,不需要改动现有构建体系,不用引入数据库,想扫哪几个模块就扫哪几个模块。就算你项目里全是 C++、Python 或者 JavaScript,它也能识别。这正是我推荐先试它的核心理由:用最低成本把重复代码的底裤扒出来。
2. simian 的工作机制与检测逻辑
2.1 基于行与 Token 的比较方式
很多第一次接触 simian 的人会误以为它跟编译器看 AST(抽象语法树)一样,能分析语义。其实不是,simian 的做法要原始得多,但也正因为原始,它才能做到跨语言通吃。
它把源文件按行拆开,再把每一行解析成一串 Token(标识符、关键字、操作符、字面量等),过程中会抹掉空格、空行、注释这些对重复判定无意义的干扰项。然后,它会用一个滑动窗口在 Token 序列上移动,窗口大小由 threshold 参数控制,一旦发现两段 Token 序列完全一致,就把这段位置记下来作为重复候选。你可以理解成两个人各拿一沓代码逐行对,但允许变量名和字符串内容不同。这种机制的好处是简单、快,不依赖任何语言的语法规则;坏处是它不会“理解”代码,两个逻辑等价但写法形态完全不同的方法,它可能判断不出来。
这里有个关键点:simian 检测的是“相似片段”,不是“复制粘贴”的严谨证据。所以看到结果别急着当结论,它只是给你指路,最后判定还是得人来看。
2.2 阈值设置是核心中的核心
simian 最核心也最影响结果的参数是-threshold,它决定“连续多少行算重复”。这个值设得太低,比如 3 或者 4,报告会爆炸,到处都是“疑似重复”,人工根本看不过来;设得太高,比如 20,普通的方法级重复几乎全被漏掉,扫描失去意义。
我的经验是:Java 项目一般从-threshold=6开始试,跑完看报告数量再调。如果报告里太多 getter/setter、DTO 字段这类结构性重复,就往上调;如果明显重复的代码没被抓出来,就往下调。以我个人实践,8 到 10 是一个比较舒服的区间,既能抓住方法级重复,又不会把样板代码全部翻出来。当然,不同项目风格差异大,最好先用一两个模块跑几轮,找到适合自己的阈值,再铺开到全项目。
3. 环境准备与命令行实操
3.1 安装与版本信息
simian 是一个发布多年的老工具,官网下载后拿到的就是一个 jar 文件。运行前提是机器上有 Java,实测 Java 8 就能稳定跑,不需要多新的版本。下载完,我习惯把它放进一个统一目录,比如~/tools/simian/,然后加一个 shell 别名,方便随手调用。
alias simian='java -jar ~/tools/simian-2.5.10.jar'顺手先看一眼帮助文档,确认参数无误:
simian -help输出会列出支持的语言、报告格式和所有可选参数,建议第一次用的人花五分钟读一遍,比到处查资料管用。这个工具体积小、启动快,扫描一个中等规模的 Java 项目大概只要几十秒,CI 里直接调用完全没问题。
3.2 最基本的一条命令怎么跑
先拿一个小模块试水。假设项目在/data/build/service,想扫src/main/java下的所有 Java 文件,执行:
java -jar ~/tools/simian-2.5.10.jar \ -threshold=8 \ "/data/build/service/src/main/java/**/*.java"注意路径里的**/*.java,这是 simian 自己识别的递归通配符,不是交给 shell 展开的,所以一定要用引号包住。运行完,控制台会输出类似这样的摘要:
Found 3 duplicate code blocks in 2 files. Total duplicate lines: 42它会把每个重复块的起始行列号、涉及文件、重复行数列出来。第一次跑的时候,那种“原来这里也有一份”的感觉会特别强烈。
3.3 整个项目扫描的完整示例
真实项目中,我不会只看控制台文本,而是生成一份 HTML 报告存到指定目录,方便发给团队看。下面这条命令基本是我的标准用法:
java -jar ~/tools/simian-2.5.10.jar \ -threshold=10 \ -failOnDuplicates=true \ -reportFormatter=html \ -reportFile=target/simian-report.html \ "/data/build/service/src/main/**/*.java" \ "/data/build/service/src/test/**/*.java"-failOnDuplicates=true这行非常关键。把它加到 CI 脚本里,一旦检测到重复就返回非零退出码,构建就失败了,等于让机器自动卡住重复代码入库。第一次接入时别急着开这个开关,等重复量降到可接受范围再开,否则流水线会红得没法看。报告中会按文件维度展示重复块,点击就能跳到重复位置,做整改会议时直接投影出来,比口头说一百句都有效。
4. 参数配置全解析与实用建议
4.1 常用参数一览表
simian 的参数不少,但我实测下来真正能派上用场的就那么几个,整理成表格如下,方便大家直接抄作业。
| 参数 | 作用 | 我的建议 |
|---|---|---|
-threshold | 连续多少行判定为重复 | Java 项目从 8 起步 |
-language | 指定扫描语言,如 Java/C++/C# 等 | 多语言项目建议分别指定 |
-failOnDuplicates | 检测到重复时让进程返回非零码 | CI 里设为 true |
-reportFormatter | 报告格式:text / xml / html | 需要分享时用 html |
-reportFile | 报告输出路径 | 固定到 target 目录 |
-ignoreCurlyBraces | 忽略花括号差异 | 建议 true,减少误报 |
-ignoreIdentifiers | 忽略标识符(变量名、方法名等)差异 | 默认关,开启后会更宽容 |
-ignoreLiterals | 忽略数字、字符等字面量差异 | 按需开启 |
-ignoreStrings | 忽略字符串内容差异 | 按需开启,常用于日志代码多的情况 |
-ignoreCharacters | 忽略指定字符(如 BOM 头) | 编码不干净时很有用 |
这里重点提醒一下:别一下子把所有ignore*参数都打开。开得越多,匹配就越宽松,漏报风险越大。默认配置检测出来的是“完全复制”为主,开启-ignoreIdentifiers之后,两个方法只是变量名不同,其他一样,也会被标记为重复,这正是很多人想要的“逻辑重复”识别。但也有副作用:比如两个方法本来只是巧合结构相似,开完这个参数也会被列进去,所以每次调整参数后要拿几个已知重复点做回归,看结果是否符合预期。
4.2 规则集配置:怎么用正则控制忽略内容
有人会问,simian 能不能配置“忽略某些目录或文件”或者“忽略某种特定结构的重复”。它不像 SonarQube 那样有丰富的规则集,但可以通过 glob 表达式和少数几个 ignore 参数实现类似效果。
想排除测试代码、生成的代码,就在传入路径层面控制:
java -jar ~/tools/simian-2.5.10.jar \ -threshold=8 \ "/data/build/service/src/main/java/**/*.java" \ "!/data/build/service/src/main/java/com/example/generated/**/*.java"在路径前加!表示排除,这个方式在处理 Java 项目里的 generated 目录时非常管用。如果用 Lombok 生成了一遍 getter/setter,自己又手写了一遍,或者用 OpenAPI 生成的模型特别多,扫描结果会惨不忍睹,必须先把这些噪音排除掉。
4.3 与 CI 的集成方式
simian 接入 CI 最直接的方式就是把上面那条命令放到流水线脚本里。我平时在 Jenkins 项目下会建一个quality.groovy,里面有一个专门的 stage:
stage('重复代码扫描') { sh ''' java -jar tools/simian-2.5.10.jar \ -threshold=10 \ -failOnDuplicates=true \ -reportFormatter=xml \ -reportFile=build/reports/simian.xml \ "src/main/java/**/*.java" ''' }如果团队用的是 Ant 构建,可以走 simian 自带的 Ant Task,将 taskdef 配置好就能在构建里直接调用。Maven 项目则可以用 exec-maven-plugin 执行 jar,或者找社区维护的 simian maven 插件。我不太建议为了引入 simian 专门换个构建工具,命令行方式已经足够轻量,再包一层反而增加维护成本。
CI 集成的目标是让重复率成为门禁的一部分,而不是辅助参考。刚开始可以先只生成报告、不阻断构建,跑两个迭代周期让大家熟悉规则,之后再打开-failOnDuplicates。
5. 实际踩坑记录与排查经验
5.1 误报与漏报的平衡
我接入 simian 的第一周,报告炸了。最典型的一类误报是大量 DTO 类之间的字段定义几乎一样,比如private String userName;这种,每个类都有 20 多行相似代码,被标记为重复。这种不是业务意义上的坏味道,就是实体结构的正常相似。解决方式很简单:扫描范围排除 DTO/VO 包,或者把 threshold 从 8 调到 12,让这类短重复直接过滤掉。
反过来,漏报的问题也遇到过。有些重复发生在不同的文件里,但方法签名、变量名不同,完全复制行数可能只有 4 行,不达阈值。这种就需要开-ignoreIdentifiers,让 simian 忽略标识符差异后再判断。不过开了之后,类似for (int i=0; i<n; i++)这种循环结构也会被算进去,报告会再次膨胀。我的习惯是分两层跑:第一次用默认配置抓“直接复制”的重复,第二次开-ignoreIdentifiers抓“换个变量名的重复”,两边报告对比着看,兼顾漏报和误报。
5.2 编码与中文注释问题
simian 对文件编码比较敏感。有一次扫描一个老模块,报告里把两个实际上完全一样的文件标记为“不重复”,细看发现差异行全是乱码。查了一圈,是文件编码不统一惹的祸——部分文件是 UTF-8,部分是 GBK,同一段中文注释在不同编码下解析出的字节不同,被当成了内容差异。
遇到这种情况,我建议先统一项目源码编码,或者至少在做扫描之前,用-ignoreCharacters把 BOM 等特殊字符忽略掉。比如文件头携带有 BOM 时,加上:
-ignoreCharacters="\uFEFF"我后来干脆把仓库里的源码全部转成 UTF-8,不光是让 simian 结果更准,也免得其他工具跟着踩坑。编码问题排查起来特别烦,因为它不会报错,只会让结果悄悄变得不可信。
5.3 大仓库扫描性能优化
一个几百万行的老仓库,扫一次可能要两三分钟,看起来还行,但如果每次提交都全量扫,开发同学会有意见。我试过几种优化方案:第一,按模块拆开扫描,每个子模块单独出报告,定位问题也方便;第二,用 glob 排除掉 generated、resource、前端构建产物这类目录,扫描体量能减少一大截;第三,把 threshold 适度调高,因为阈值越高需要匹配的行越长,候选集合越小,速度也会更快。
还有个小技巧,-verbose参数可以输出扫描进度,看到底卡在哪个文件上。我遇到过一种情况:某个 Java 文件外观看很普通,但因为整行只有一个超长字符串,simian 解析时很慢。这种文件不用特别处理,但知道原因之后就不至于瞎调参数了。总体而言,simian 的性能是足够日常使用的,真正的瓶颈往往不是工具本身,而是扫了一堆没必要扫的文件。
6. 一些真实的使用体会
6.1 门禁阈值设多少才合理
很多团队第一次引入 simian 就问:“阈值到底设成多少合适?”这个问题没有标准答案,但我试过几个项目后有一个大致规律:Java 后端项目通常 8 到 12 比较合理,前端 TS/JS 项目可以设低一点,比如 6,因为前端代码方法普遍短一些,设太高容易漏。老项目第一次接入时建议设 15 甚至 20,让报告看一眼能处理完,后续每轮迭代再逐步下调,给团队留出重构缓冲期。
最怕的一种情况是:老板拍脑袋定了个 5,开发同学每天被海量误报淹没,两天之后大家直接把工具卸了。门禁阈值一定要和团队当前代码质量、重构能力匹配,先让机器抓住明显问题,再慢慢收紧,这才是可落地的节奏。
6.2 从重复率报告到重构落地
simian 给出报告只是第一步,怎么把报告变成代码改动才是关键。我的实践是:每次扫描后,把重复块按模块分派给对应的开发同学,要求一周内提交整改说明,要么抽公共方法,要么提取父类,要么用策略模式,至少也要在代码注释里写明“此处与某处重复,改动时需同步”。这个强制动作让重复数量在三个月内降了一半以上。
不过我始终觉得,simian 这类工具的价值不是追求“零重复”的数字好看。有些看起来重复的结构,比如两个参数类型不同的重载方法,硬抽到一个函数反而牺牲可读性,这种就没必要动。工具的价值是把重复暴露出来,让人去判断哪些是坏味道、哪些是无害相似,而不是机械地消灭所有相同代码。
最后分享一个让我印象深刻的场景:接入 simian 三个月后,新同事入职看报告,指着一个重复块问“为什么这两个方法要各写一遍”,那一刻我就知道,这套工具带来的不只是重复率下降,更像是在团队里建立了一种“遇到重复就要顺手清理”的潜意识。如果你也被复制粘贴代码折磨过,真的可以试试拿 simian 给你的代码库做一次全身检查,说不定收获比预期大得多。
本文还有配套的精品资源,点击获取