简介:Simian是一款广受欢迎的代码重复检测工具,这份资源包基于其2.3.35版本,完整提供了跨平台运行所需的核心组件与配套资料,适合Java、C#、C/C++、JavaScript等语言的开发团队使用。通过检测源代码中的相似语句结构,开发者可以有效发现复制粘贴编程、冗余逻辑等问题,并将Simian集成到持续集成流水线中,防止新增重复代码。包内共59个文件,既有jar/exe主程序与DLL运行库,也有HTML官方文档、PDF许可证、DTD/XSL配置模板、GIF/PNG界面图标等,整体压缩包仅3.43MB,轻量且便于分发。目前已有1416人学习下载。借助包内的javadoc文档、安装指南、功能列表和变更说明,使用者不仅能快速完成Maven或Gradle环境集成并设定检测阈值,还能深入理解报告格式与规则参数;同时,附带的JDK日志和许可证文件有助于排查环境问题、规范商业使用。在日常开发中持续运用Simian,可有效优化代码结构、降低维护成本,提升项目长期可维护性。这一工具包非常适合正在搭建代码质量检查体系的技术团队,以及希望快速上手重复代码检测的个人开发者。
1. 只按文件路径和行号说话:simian是什么、能接住哪些场景
接手一个没人愿意碰的遗留模块时,最怕的不是代码风格乱,而是同一段逻辑被复制到了三个地方,你只改了其中一个入口,线上另一个入口还在走旧规则。干这种活的同事多半会给你推荐一个工具名:simian。它就是一个Java编写的命令行代码重复检测工具,扫一遍指定目录,把所有文本层面重复出现的代码区段标出来,精确到文件路径、起始行号和重复长度。它解决的是一类很具体的问题:在动手改任何一处代码之前,先知道哪些地方还藏着同一个逻辑的副本。适合在重构前画风险地图、在CI入口做硬拦截、在代码评审阶段定一条可量化的重复率红线。
2. 先立住判断:simian的检测原理、同类工具选型与最小跑通
2.1 重复检测靠的是token不是语义:原理与它的能力边界
simian扫描源文件时,会先把注释、空白和空行全部丢掉,再把代码切成一串词法单元,也就是token。它不看你的代码结构树,不做数据流分析,只在一个滑动窗口里找完全相同的token序列。只要两段代码的token序列一致,就判为重复。
这个设计带来两个直接后果。第一个是它对手工复制粘贴的“包装”非常敏感:把变量名改了、换行缩进打乱了、中间塞了几个空行,只要骨架课体一致,照样命中。第二个是它对“语义相同但写法不同”完全无能为力——同一个逻辑用for循环写一遍、用Stream流写一遍,token序列完全不同,它不会认为这两段相关。所以它擅长的是“机械副本检测器”,而不是“设计坏味道评审员”。你用它来找的是“谁和谁长得一样”,而不是“谁应该长得一样”。
能力边界还体现在输出粒度上:它报告的最小单位是“重复块”,不是“重复的方法名”或“重复的类”。单文件内部重复它能查,跨文件、跨目录重复也能查。对大工程来说,正因为不读AST,扫描速度才快得能塞进流水线。一个几千文件的模块在线下环境里用单JAR跑,通常也就是一到两分钟出报告。这个速度决定了它适合当门禁,而不是只在重构月被拿出来用一次。
2.2 和CPD、SonarQube、jscpd怎么选:一张对比表说清差异
做重复检测的可选工具不少,团队里最容易争的是“用自带的还是单独接一个”。我接触过的项目里,最常见的四个候选是simian、PMD自带的CPD、SonarQube平台里自带的重复度量,以及前端圈子里常用的jscpd。它们各有各的适用场景,不是简单的谁替代谁。
| 候选工具 | 运行形态 | 语言覆盖 | 与CI集成 | 最合适干什么 |
|---|---|---|---|---|
| simian | 单JAR命令行 | 多语言 | 退出码可直接当门禁 | 仓库级重复红线、重构前扫雷 |
| CPD(PMD自带) | 命令行/构建插件 | 多语言 | 需解析报告再判断 | 和PMD规则检查做成一套 |
| SonarQube重复度量 | 服务端平台 | 多语言 | 在平台体系内 | 团队持续看板、历史趋势 |
| jscpd | Node命令行 | 前后端脚本为主 | CLI可直接调用 | 前端仓库快速自查 |
选型逻辑其实很简单,看你们想要的是“一个平台”还是一个“硬门禁”。SonarQube是好东西,但它重,要部署服务端、要维护规则集、要和现有账号体系打通;而且它给的重复率是汇总指标,定位到具体哪个文件哪一行重复时,体验远不如专用工具细致。CPD能做类似的事,但它的输出格式和阈值控制没有simian直接。所以常见做法是:想在仓库里加一条“重复超过阈值就构建失败”的硬规则,又不想为此引入一套服务端基础设施,simian是最轻的选择。它不跟漏洞检查集成,不值得为它建平台;它只负责一个死磕的指标,那就是重复。
2.3 本地最小跑通:解压、写样例、跑出第一份报告
先把工具跑起来再说别的。simian的发布形态是zip包,解压后得到一个带版本号的JAR,目标机器只要装了Java运行环境就能用。下载来源通常是项目主页的release产物,我用的时候习惯把zip固定放到工具目录,而不是直接扔在/tmp,因为后面CI里要复用它,版本得锁住。
# 解压工具包,路径按你本地实际位置调整 mkdir -p ~/tools/simian unzip simian-*.zip -d ~/tools/simian # 确认Java环境可用 java -version # 准备一个带重复代码的示例工程 mkdir -p /tmp/simian-demo/src cp /tmp/simian-demo/src/OrderChecker.java /tmp/simian-demo/src/PayChecker.java # 两个文件内容故意保持一样,只改类名和几个变量名这段准备步骤里,最后一条cp不是生产技巧,是验证技巧:把同一个文件复制一份,改几个标识符,再让simian扫。它能命中,说明工具按token判重的机制是生效的;它扫不出来,多半是关键参数被调坏了。
# 最小扫描命令:指定阈值,指定扫描范围 java -jar ~/tools/simian/simian-*.jar \ -threshold=6 \ "$(pwd)/src/**/*.java"这里的-threshold=6表示“重复块超过6行才报告”,单位是代码行规模,不是字符数。阈值越小报告越碎,越大越容易漏。$(pwd)/src/**/*.java用的是simian自己的通配符语法,不是shell的glob,必须用双引号包起来,否则bash会先把它展开,simian就收不到真正的匹配规则。第一次跑通,看到输出里出现类似“Found duplicated ... blocks”的文字,说明环境没问题,后面所有调参都在这条命令的形态上做文章。
3. 从“能跑”到“能信”:核心参数与典型命令
3.1 threshold决定准心:不同阈值的误报与漏报规律
把simian跑通不难,难的是让报告“可信”。最影响可信度的就是-threshold。它的默认值是6,意思是重复内容达到或超过6行才被报告。这个数字不是玄学,它其实是扫描的焦距:焦距短,看到的东西多、噪声也多;焦距长,看到的都是大块重复,但小段漏网全放过去了。
| 阈值取值 | 报告特点 | 推荐场景 |
|---|---|---|
| 4 | 短块大量涌入,误报明显 | 上线前的专项排查 |
| 6(默认) | 敏感和噪声比较平衡 | 日常告警 |
| 8~10 | 只报大块,误报少 | CI流水线硬门禁 |
我一般建议第一次扫描用6,先把全貌拿回来:看报告里重复块的size分布。如果6行到10行的重复块占绝大多数,说明仓库里的重复以小段为主,这时候CI阈值定在8或10是合理的,不会天天误报;如果大量重复块在20行以上,说明存在“整个方法复制”级别的风险,CI阈值可以直接定到10以上,集中盯大块。反过来,如果扫完一个文件都没报,但肉眼明显看到两个方法就差一个参数名,那大概率是阈值定高了,把threshold降到4再看一眼。这个步骤是整套调参里最关键的动作:先看size分布,再定门禁线,而不是拍脑袋选一个数字。
3.2 language、include/exclude:多语言工程的文件范围控制
真实工程往往不止一种语言,simian的-language参数支持一次传多个语言,用花括号和逗号把它们包起来。常见写法和文件范围排除放在一起:
java -jar ~/tools/simian/simian-*.jar \ -threshold=8 \ -language="{java,kt}" \ -exclude="**/build/**" \ -exclude="**/generated/**" \ -exclude="**/test/**" \ "$(pwd)/src/**/*.java" "$(pwd)/src/**/*.kt"-language="{java,kt}"的花括号是simian自己的语法,不是shell通配符,必须在命令行里用双引号包住,否则bash会把{java,kt}拆成两个参数传进去,扫描范围就被改写了。-exclude用的也是simian内部的通配符规则,**可以跨目录匹配。这里建议至少排除三类目录:构建输出目录、生成的模型类目录、测试目录。测试代码里的重复有其特殊性,需要单独评估,不应该和业务代码混在同一个门禁里。
文件范围用通配符而不写死路径,是为了让命令在本地和CI里用同一份。常见做法是把这条命令写进脚本,相对路径基于仓库根目录展开。还有一种做法是按文件后缀重复写多个glob,比如src/**/*.java和src/**/*.kt各写一份,这比只靠-language猜文件要稳定,因为language控制的是“用什么规则解析”,glob控制的是“哪些文件参与扫描”,两者各管一摊。
3.3 让输出更适合CI的命令行开关:ignoreLiterals、failOnDuplication等
除了threshold和范围,还有几个开关值得在进CI之前先搞清楚。它们不是锦上添花,是直接决定门禁会不会天天误报。
-ignoreLiterals:忽略字符串和数字的差异。两个结构一样的日志方法,只有消息文案不同,默认会被判为重复;打开这个开关后,这类“模板重写”就不再打扰你。注意同一段SQL和取参逻辑因为字面量一字不差,即使打开了也还是会被报告。-ignoreIdentifierCase和-ignoreCharacterCase:分别忽略标识符大小写和字符大小写。适合代码库里有历史命名风格差异的场景。-ignoreModifiers:忽略public/private这类访问修饰符的差异。DTO的getter/setter经常只有访问级别不同,打开这个开关能让真正的结构重复暴露出来。-multiset:把零散、互相靠近的短重复片段聚合成一个大集合。想知道“这一整片到底重复了多少”,而不是看单个小块时,用这个。-failOnDuplication:扫描结束只要存在报告之外的重复块就返回非0退出码,这是CI门禁最直接的用法。-balanceParentheses:括号不平衡时代码解析容易出错,打开它可以让扫描器更宽容地处理括号层级变化。
组合起来,我通常会这样配一个适合周期性门禁的命令:
java -jar ~/tools/simian/simian-*.jar \ -threshold=8 \ -language="{java,kt}" \ -ignoreLiterals \ -ignoreModifiers \ -exclude="**/build/**" \ -exclude="**/generated/**" \ -reports=text,html \ -failOnDuplication \ "$(pwd)/src/**/*.java" "$(pwd)/src/**/*.kt" \ > simian-report.txt这里的顺序会影响排查体验:阈值放最前面,语言和排除紧跟其后,ignore开关调整判重规则,-reports同时输出文本和HTML,最后-failOnDuplication让脚本能拿到退出码。> simian-report.txt把终端输出落盘,既方便排查,又给后续解析留下原始素材。现在这组开关组合起来,报告里的重复块数量会明显下降,剩下来的基本都是“真重复”。值得说明的是,-ignoreModifiers和-multiset这类开关在大目录上会明显增加扫描耗时,不要在超大工程里一股脑全开,先跑一次不带multiset的版本,再看要不要叠加。
注意:
-language="{java,kt}"和-exclude里的通配符都是simian自己解析的,不是bash的。任何情况下都要用双引号包住,否则shell会先展开花括号和星号,命令行为和你预期完全不一样,而且不会报错,只会“少扫”或“多扫”。
4. 把报告读成重构清单:输出格式与代码定位
4.1 解读text报告的重复块:行号、长度和文件分布怎么对应
跑完扫描后,text报告是最直接的信息源。它是逐条列出重复块的,格式类似下面这样的结构:
Found duplicated 1 blocks of size 8 starting at line 12 of file src/main/java/com/order/OrderChecker.java and starting at line 31 of file src/main/java/com/pay/PayChecker.java第一行的“size 8”代表这个重复块的规模是8行代码,后面跟的是两个文件各自的起始行号。不同版本的simian输出措辞可能略有差异,但核心信息永远是三个东西:重复块多大、在哪个文件哪一行开始、和哪个文件哪一行重复。拿到这条信息后的动作,不是马上改代码,而是先在编辑器里打开这两个文件对照着看。
对照时有一个细节容易翻车:simian报告的起始行是token断点所在行,不一定是你肉眼看到的“代码块第一行”,因为注释和空行已经被忽略不计了。所以打开文件后,往上看几行往往才能看到完整的重复片段。确认“这是同一段逻辑”之后,再看重复块内部的差异点,比如参数名不同、日志文案不同、边界判断不同。这些差异决定了后续抽取方法时要不要带参数。
4.2 HTML报告适合做的事:把重复块变成评审议题
命令行跑的时候加上-reports=html,simian会额外生成一份可交互的HTML报告。它把每个重复块的两个入口做成交叉链接,点击后能在两份源码之间快速跳转。这东西在代码评审时特别好用:把生成目录配好、范围控制好之后,直接把HTML文件丢到评审群里,让两边模块的负责人坐在一起,按重复块的编号逐个过。
我遇到的实际情况是,评审时会问三个问题:这段代码两边谁会改得更频繁?如果只改一处,另一处会不会崩?能不能抽到一个公共方法里且不改变行为?这三个问题如果都能答上来,这个重复块才值得进重构清单。HTML报告的定位不是给机器看的,是给两个活人看的。它让“重复”从一个抽象指标变成一个可以逐条打勾的具体议题。
评审完的结论需要有人登记。常见做法是把遗漏的“故意重复”块记录在排除清单里,下一次扫描用exclude跳过;把“需要抽公共方法”的块登记为重构任务,直接关联到迭代排期。这个过程走完,报告才真正变成了可执行的重构清单,而不是跑完就压箱底的黑匣子。
4.3 扫描完不急着改:分类降噪、区别对待故意重复
扫出来的重复块不应该被一视同仁。我把它们分成三类处理。
第一类是值得抽的:两边逻辑完全一样,只是落的包不同。这类抽取后能直接减少后续多处长改的维护成本,是simian最有价值的部分。第二类是故意重复的:比如协议里的状态枚举、各端必须保持一致的配置常量。这类不是不处理,而是在代码里写清楚“此列表需与某某端同步”,并且将来同样结构再次出现时不再报。第三类是风格相似的重复:相同的try-catch骨架但业务内容完全不同,这类开-ignoreLiterals或其他参数降噪即可,盲抽成通用方法反而会让调用关系变复杂。
分类做完后再看要不要动代码。重复检测的目的不是把重复率压到零,那是表演;真正的目的是把“什么地方在重复”这件事变成一张可以排序的清单。按重复块大小排,改动成本低的先做;按改动风险排,两边都被频繁改动的优先处理。这张清单就是接下来一到两个迭代的重构排期来源。
5. simian使用中的常见问题与排查:误报、漏报与性能
5.1 误报集中爆发:先查阈值和ignore开关而不是改代码
现象:一次扫描出来几百个重复块,几乎每个Service文件都中标,代码评审根本没法看。 原因:最常见的是三个因素叠加——threshold设得太低,**/generated/**没有排除,以及没有打开-ignoreLiterals。有些工程的代码生成器输出目录比src/main还深,不排除的话生成代码会被当成“人工重复”扫出来。 解决:先把生成目录排除,再单独打开-ignoreLiterals,最后再把阈值从5调到6或8。记住一个原则:每次只动一个开关,扫一次对比一次结果,不要三个参数一起改,否则根本不知道是哪个参数起作用。误报并不可怕,可怕的是因为误报就直接关掉整个重复检测。
5.2 大段重复扫不出来:三个依次排查的方向
现象:肉眼明显看到两个Service方法几乎一样,simian却一条报告都不出。 原因:可能出在三个地方。第一个是threshold设得比实际重复块还大,重复块本身只有7行,阈值却在8,自然石沉大海。第二个是代码中间被插入了不重复的语句,比如一边多了一行日志,把token序列切成了前后两段,每段都低于阈值。第三个是文件编码不统一,文件里有乱码,token解析到乱码处直接被中断。 解决:按顺序排查。先把threshold降到4跑一遍,看有没有更小的命中;再看两个方法中间是否夹着差异语句;最后用file -bi检查两个文件的编码是否一致,统一成UTF-8并配置git自动转换换行符后再跑。这一步做完仍没有结果,那就是两段代码只在语义上相同、token层面完全不同,靠simian解决不了,得靠Code Review或人工快速比对。
5.3 工程大扫描慢:文件排除与分块扫描的取舍
现象:全仓库一次扫描耗时十几分钟,每次进流水线都卡在simian这一步,构建超时。 原因:扫描范围过大,最常见的是把依赖目录、构建输出目录、前端缓存目录都扫了一遍。这些目录里大量代码是生成的或复制进来的,扫它们不仅慢,结果还没有参考价值。 解决:优先排除三类目录:构建输出目录(如target、build、dist)、VCS目录、外部依赖目录。排完还慢,就按模块拆开扫,每个模块独立跑、独立定阈值,让常规重复在模块内就能被发现;跨模块的大块重复单独加一个汇总任务。把一个大扫描拆成流水线里多个并行小任务,比优化单次扫描参数更立竿见影。
5.4 行号对不上或报告打不开:编码、换行和生成文件的影响
现象:报告里说重复块起始于第40行,打开文件发现第40行是空行,实际代码在第42行,导致评审时反复定位错位。 原因:simian的行号是基于token解析的,不是基于编辑器显示的行号;文件里如果带BOM或者混用Windows与Unix换行符,行号会额外偏移。HTML报告打不开,多半是项目路径里有中文名或空格,浏览器对本地文件的路径转义处理不一致。 解决:统一仓库文件为UTF-8无BOM,并在git配置里开启换行符自动转换。生成的HTML报告路径不要放在带中文和空格的目录下;如果项目根目录本身带空格,把报告输出到一个临时目录再打开,比如/tmp/simian-report.html。这两个动作解决掉,报告的行号和浏览器展示基本就不再出幺蛾子。
6. 让它值回票价:把simian接进流水线并盯住趋势
6.1 版本和阈值锁进配置
把simian接进CI的第一步,不是写流水线脚本,而是把工具版本、阈值、语言和排除规则全部锁在一个配置里。常见做法是准备一个项目级的simian.properties或环境变量模板,流水线只读取它。工具版本锁住是趋势可比的前提:如果每次升级后判重口径变了,历史数据就不能再和本期对比。阈值也建议从配置读取,而不是散落在多个脚本里。
6.2 把重复块数变成趋势指标
流水线里跑simian,同时保留“失败”和“数量”两个信息,我一般用一个脚本完成:
#!/usr/bin/env bash set -euo pipefail SIMIAN_JAR="$1" THRESHOLD="${SIMIAN_THRESHOLD:-8}" java -jar "$SIMIAN_JAR" \ -threshold="$THRESHOLD" \ -language="{java,kt}" \ -exclude="**/build/**" \ -exclude="**/generated/**" \ -failOnDuplication \ "$(pwd)/src/**/*.java" "$(pwd)/src/**/*.kt" \ > report.txt 2>&1 || true COUNT=$(grep -o 'Found duplicated [0-9]*' report.txt | awk '{s += $3} END {print s+0}') echo "dup_blocks=$((${COUNT:-0}))" >> trend.csv这段脚本里|| true不是掩盖失败:脚本要先拿到报告落盘,后续再让专门的gate任务根据退出码决定是否阻塞合入,两者分开处理,一个负责记录趋势,一个负责门禁拦截。grep匹配的是simian报告里的固定措辞,正式接入前先在本仓库跑一次,确认这个模式的匹配结果正确。趋势文件每天追加一行重复块总数,几个迭代后就能看出重复是在下降还是又回潮了。
我现在接手一个新工程,第一件事不是部署什么平台,而是把simian跑一遍。不是想刷一个低重复率的数字,是想在相同代码块之间看出这个工程的历史包袱。之后改方法、抽公共逻辑、改签名,都会先翻出这份报告看一眼,确认没有另一半在等着改。这个习惯值回票价,希望帮到你。
本文还有配套的精品资源,点击获取