简介这是一套基于Java实现的敏感词筛选系统源码面向有一定Java基础、希望学习文本过滤与内容审核开发的开发者可用于论坛、社区、网站后台等场景的词库管理与文本匹配。源码包共1454个文件主要包含306个java核心业务类、458个js前端脚本、235个html页面以及css、图片、XML配置等资源整体约16.01MB涵盖完整前后端工程、数据库脚本与部署配置。已有267人学习下载。通过该工程可掌握敏感词库动态更新、Aho-Corasick与KMP等匹配算法、中文分词与正则表达式结合、Trie树优化及多线程并发处理等关键实现同时包含词库管理、敏感词检测、结果展示等模块并具备异常处理与日志记录机制适合作为实战项目研读、功能扩展与二次开发起点。1. Java敏感词筛选系统源码.zip先搞清楚它解决什么再决定要不要接手用户在社区里发了一段带敏感词的评论正常情况下内容审核会拦下来但审核是有成本的。敏感词筛选系统就放在审核前面用最短的时间把明显违规的词找出来、替换掉或者直接拦截避免每条消息都走到人工环节。标题里这个 Java敏感词筛选系统源码.zip就是一套已经成型的 Java 实现工程词库、匹配引擎、过滤接口全在里面解压出来就能跑。对还在学 Java 的人来说它是很完整的实战项目适合当 Java 课程设计或者毕业设计的底子对已经工作的工程师来说它是搞懂“多模式匹配怎么落地”的现成样本。但这类系统并不是万能的最常见的误解是把它当成 AI 审核来用实际上它只负责文本层面的快速命中语义理解、图片审核这些它管不了。搞清楚这个边界后面所有实现细节才有意义。2. 匹配算法选型DFA、AC 自动机与正则敏感词筛选用哪个才不翻车敏感词筛选系统的核心是匹配算法。词库量级从几百到几十万文本从昵称到长文章不同算法在延迟和内存上天差地别。我见过团队拿正则表达式把几百个词拼接成一个大 Pattern一到高峰期 CPU 直接拉满。这章把三种常见方案的原理和边界讲透方便你拿到源码后先判断它用的是什么思路、要不要换。2.1 DFA 状态机为什么敏感词源码里它最常出现DFA确定有穷自动机。敏感词过滤里说的 DFA落地时通常是一棵前缀树Trie词库里的每个词从根节点开始按字符走走到词尾节点就认为命中。比如词库里有“abc”那路径就是 root→a→b→c其中 c 节点被标记为结束。输入“xxabcyy”扫描到 a 时进入子树经过 b、c 后发现命中。用 Java 实现时节点一般长这样一个 HashMap 存子节点一个 boolean 标记是否词尾。这样做的直接好处是逐字符匹配平均命中速度极快替换时能准确知道命中的起止位置。需要留意的是纯 Trie 在“当前路径断掉”时会回到根节点重新开始严格来说单次扫描并不是 O(n)而是 O(n×L)L 是词库最长词的长度。大多数“DFA 敏感词过滤”实现其实就是这个 Trie 版本命中即停对业务足够如果词库特别大要求稳定的 O(n) 线性时间那就应该考虑 AC 自动机。选型时我一般看三个维度词库规模、单次输入长度、词库更新频率。词库在 1 万以内、更新不频繁DFA 够用词库上十万且文本普遍很长AC 自动机收益明显正则只适合临时脚本或者少量固定词扛不住高并发场景。2.2 AC 自动机与正则的边界什么时候该换方案AC 自动机是 Trie 加失败指针匹配失败时不回根节点而是沿失败指针跳到下一个可匹配的节点继续往下走。构建失败指针比构建 Trie 复杂容易在“失败指针指向根还是指向更长后缀”上翻车。如果拿到的源码里匹配部分用了 AC重点看两点失败指针构建时是否用 BFS 逐层生成匹配过程中遇到 null 是否回退到 root。这两点写错要么漏词要么死循环。正则适合的场景是少量固定词或者需要表达复杂上下文关系比如“前面不能出现某个词”。正则有个经典坑叫灾难性回溯模式里出现嵌套量词输入文本一长匹配引擎会在各个分支里反复尝试CPU 从毫秒级飙到秒级甚至超时。这正是为什么我不建议把正则当主过滤方案源码里如果主匹配逻辑是“词库拼接成正则”上线前最好先换掉。三种方案的横向对比维度TrieDFAAC 自动机正则拼接单次匹配耗时O(n×L)命中即停O(n)稳定不稳定可能灾难性回溯构建复杂度低遍历词库中高BFS 建失败指针无构建但拼接和转义麻烦内存占用每字符一个节点比 Trie 多失败指针词越多编译后内存越大词库更新重建整棵树重建整棵树重新拼接和编译适用场景中小词库、通用业务大词库、低延迟要求临时过滤、少量规则3. 拆开源码zip 解压后先看这四个文件再跑通最小链路拿到 Java敏感词筛选系统源码.zip 后先不要急着点运行。解压后用 IDE 打开按下面几步确认工程能不能接手再动手跑通最小链路。3.1 工程骨架与词库文件先确认它是不是你能接手的结构大多数这类工程的结构都很接近pom.xml 管依赖trie 包放核心匹配引擎service 包放业务封装controller 包放 HTTP 接口resources 下放词库和配置。先看这四样东西文件/目录作用打开后重点看pom.xml依赖与构建配置Java 版本、是否引入 Spring Boot、有没有 lomboksrc/main/java/…/trie匹配引擎节点结构、构建入口、匹配方法src/main/resources/sensitive-words.txt词库编码、行分隔符、是否去重application.yml运行参数端口、过滤开关、词库路径如果 pom.xml 里 Java 版本和本机不一致先改 maven.compiler 的 source/target 和 Spring Boot 版本避免一上来就编译不过。词库文件也要确认是不是“一行一个词”有些项目会把词库做成 CSV 或 Excel加载逻辑不一样接手成本会高一些。3.2 词库加载与状态机构建代码怎么写才不埋雷词库加载是最容易出问题的一步编码不统一后面全白做。常见做法是统一 UTF-8读取时显式指定字符集public class SensitiveWordLoader { public ListString loadFromFile(String path) throws IOException { ListString words new ArrayList(); try (BufferedReader reader Files.newBufferedReader( Paths.get(path), StandardCharsets.UTF_8)) { String line; while ((line reader.readLine()) ! null) { String w line.trim(); if (!w.isEmpty()) { words.add(w); } } } return words; } }这里有两个参数要留意一是 StandardCharsets.UTF_8 必须显式指定不要依赖平台默认编码二是 trim 会去掉行首行尾空白但不会处理词条中间的空格。如果词库里有“毒 品”这类中间带空格的写法需要业务侧先确认是词库格式还是故意的绕过策略。状态机构建的核心代码很直接public class SensitiveWordTrie { private static class Node { MapCharacter, Node children new HashMap(); boolean end; } private final Node root new Node(); public void build(ListString words) { for (String w : words) { Node cur root; for (char c : w.toCharArray()) { cur cur.children.computeIfAbsent(c, k - new Node()); } cur.end true; } } public Node root() { return root; } }computeIfAbsent 会在子节点不存在时自动创建省掉三段式的 if 判断。end 标记表示当前节点是某个词的结尾。注意 build 方法会覆盖已有结构实际工程里不要在请求线程里调用它后面热更新章节会专门说这个问题。提示Windows 记事本另存的 UTF-8 会带 BOMBufferedReader 读第一行时行首会多一个 \uFEFF 字符导致第一个敏感词永远匹配不上。处理方式是用 commons-io 的 BOMInputStream 包一层或者读第一行后手动去掉 \uFEFF。3.3 匹配与替换contains、findAll、replace 三个方法一次说清匹配引擎一般提供三个对外方法contains 做布尔判断findAll 返回所有命中词replace 做打码。我写一个教学版实现逻辑在注释里逐行说明public class SensitiveWordEngine { private final SensitiveWordTrie trie new SensitiveWordTrie(); public void build(ListString words) { trie.build(words); } public ListString findAll(String text) { ListString hits new ArrayList(); for (int i 0; i text.length(); i) { Node cur trie.root(); int hitEnd -1; for (int j i; j text.length(); j) { Node next cur.children.get(text.charAt(j)); if (next null) { break; // 当前路径断了换下一个起点 } cur next; if (cur.end) { hitEnd j; // 记录最近一次完整命中的结束位置 } } if (hitEnd 0) { hits.add(text.substring(i, hitEnd 1)); } } return hits; } public boolean contains(String text) { return !findAll(text).isEmpty(); } public String replace(String text, char mask) { ListString hits findAll(text); String result text; for (int i 0; i text.length(); i) { Node cur trie.root(); int hitEnd -1; for (int j i; j text.length(); j) { Node next cur.children.get(text.charAt(j)); if (next null) break; cur next; if (cur.end) hitEnd j; } if (hitEnd 0) { result result.substring(0, i) String.valueOf(mask).repeat(hitEnd - i 1) result.substring(hitEnd 1); } } return result; } }findAll 用的是“逐起点匹配”外层循环负责换起点内层循环沿着 Trie 往下走。hitEnd 会被反复覆盖这样如果词库里同时有“abc”和“abcd”文本里出现“abcd”时会记录最后命中的 d 的位置而不是停在 c。replace 方法为了讲解方便直接调用了两次遍历实际工程里建议先 findAll 再把命中区间合并最后一次性替换否则重叠命中时替换符数量会不对。mask 参数一般传 *想直接删除敏感词就传空字符但删除会导致上下文粘连慎用。4. 把筛选系统接入业务热更新、过滤策略与性能调参本地跑通只是第一步。接入真实业务后有三个躲不开的问题词库怎么在不重启的情况下更新、过滤策略怎么定、内存和并发怎么控制。逐一说清楚。4.1 词库热更新不重启就换词表的正确姿势业务方经常说“这个词现在不能用了”你不能每次都重启服务。常见做法是构建完一颗新树后整体发布而不是在旧树上改。用一个 Holder 类持有当前生效的 Triepublic class SensitiveWordHolder { private volatile SensitiveWordTrie trie new SensitiveWordTrie(); public void reload(ListString words) { SensitiveWordTrie newTrie new SensitiveWordTrie(); newTrie.build(words); this.trie newTrie; // 先构建后发布 } public SensitiveWordTrie getTrie() { return trie; } }volatile 保证其他线程能在发布后立刻看到新引用。reload 的时序是新词库加载进内存构建新 Trie整体替换引用旧 Trie 等 GC 回收。整个过程读线程要么拿到旧树要么拿到新树绝不会拿到半棵树。注意 reload 不要放在请求线程里。十万词库的构建可能要几百毫秒到几秒放请求线程会让单个请求卡死。常见做法是单独的管理接口比如 POST /admin/reload管理员上传词库后触发或者定时任务每分钟检查词库文件的 lastModified发现变化就调 reload。4.2 过滤策略与接口参数替换符、跳过字符与白名单真实输入不可能永远是干净的。用户会故意把敏感词中间插空格、加标点绕过滤所以过滤参数至少要支持这几项public class FilterOptions { private char maskChar *; private SetCharacter skipChars Set.of( , ., -, _); private SetString whiteList new HashSet(); public FilterOptions(char maskChar, SetCharacter skipChars, SetString whiteList) { this.maskChar maskChar; this.skipChars skipChars; this.whiteList whiteList; } // getter/setter 略 }skipChars 的作用是匹配时把这些字符跳过去但替换时要按原文本位置打码否则替换长度会错位。举个例子词库里有“敏感词”输入“敏.感.词”匹配时要识别为命中替换后输出“”如果按跳过的逻辑直接拼可能输出“..”这就翻车了。白名单的判定时机也有讲究。白名单几十个、词库几千个时先判白名单更省资源白名单数量很大就先匹配再判白。匹配结果里还要支持两种模式SHORTEST 和 LONGEST。词库里同时有“酒”和“白酒”文本出现“白酒”时SHORTEST 会命中“酒”LONGEST 会命中“白酒”。做过滤系统LONGEST 误伤更少因为短词往往是长词的子串。参数取值效果maskChar* 或空字符替换或删除命中词skipChars空格、点、横线、下划线匹配时跳过替换时按原位置打码whiteList正常词、品牌词命中白名单直接放行matchTypeSHORTEST / LONGEST决定重叠词命中优先级4.3 内存与并发节点估算与线程安全Trie 的内存占用比直觉大。每个节点是一个 Java 对象加一个 HashMapJVM 对象头加引用加对齐一个节点几十字节起步。粗略估算词库规模平均词长估算节点数内存量级1 万4 字约 4 万十几 MB10 万4 字约 40 万几十到上百 MB100 万5 字约 500 万数百 MB 以上这是粗估具体用 jmap -histo 确认不要凭感觉。如果内存超预期先看词库有没有去重再把 HashMap 的初始容量调小避免扩容浪费。并发方面Trie 构建完成后只读查询方法不修改 children 和 end所以不需要加锁。更新用 volatile 发布读线程拿到新引用直接查。如果代码里有“匹配到词后从 Trie 里移除”这类逻辑趁早删掉这是黑匣子级 bug并发环境下必然出问题。5. 敏感词筛选系统常见问题排查现象、原因与对策这块是血泪经验。下面每一条都是实际环境里出过的问题按现象、原因、解决的顺序写排查时可以直接对照。5.1 词库加载后一个词都匹配不上现象日志显示词库加载了 N 行但无论输入什么匹配结果恒为 false。 原因最常见是编码不一致。Windows 下用记事本把词库存成 ANSI程序里却用 UTF-8 读取中文全部变成乱码乱码字符自然匹配不到原词。 解决统一词库为 UTF-8 无 BOM代码里显式指定 StandardCharsets.UTF_8。Linux 上先用 file sensitive-words.txt 确认编码已经转成 GBK 的文件用 iconv -f GBK -t UTF-8 sensitive-words.txt 转回来再入库。5.2 词库 10 万启动一次要 30 秒现象服务启动后迟迟不就绪日志一直停在“构建敏感词树”。 原因构建逻辑写在静态块或启动监听器里每次启动全量重建词条没有去重存在大量子串重复路径。 解决先去重用 Set 过滤重复词删除被其他词包含的短词再把构建放到独立线程做预加载。如果业务要求启动后立即可用就主线程等构建完成再暴露接口绝不能让流量打到一个空树上。5.3 正则方案下 CPU 突然 100%现象某个长文本进来CPU 核数全部打满请求超时。 原因主匹配逻辑用的是正则词库被拼接成一个巨大的 Pattern长文本配合大量量词发生灾难性回溯。匹配引擎会在各个分支反复尝试耗时从毫秒级涨到秒级。 解决换 Trie 或 AC 自动机。如果确实要保留正则能力处理复杂上下文给每个规则加超时比如 Future.get(timeout)超时后按保守策略处理同时限制单次过滤文本长度避免一条几万字的输入拖垮服务。5.4 热更新后并发请求报错现象reload 之后部分请求出现 NPE 或命中结果不一致。 原因更新逻辑直接清空 root.children 再重新填充读线程拿到同一个 root 在读一个正在被修改的哈希表边写边读必然出问题。 解决改成先建后发新 Trie 构建完成再整体替换 Holder 里的引用。Holder 字段用 volatile 保证可见性旧树只读不写整体替换后不会产生中间状态。5.5 正常词被误伤不是 bug是策略没定现象上线后用户昵称大量被替换一查是正常词里的某几个字被单独命中。 原因默认子串匹配或者最短匹配词库里一个短词嵌在长词里也会被命中。 解决把 matchType 切到 LONGEST优先匹配更长的词维护白名单把不违规的品牌词、术语放进去如果业务要求敏感词必须独立成词匹配时检查命中位置前后的相邻字符是中文或字母就跳过避免子串级误伤。6. 进阶把命中率与性能变成可验证的数字6.1 测试用例集先行上线前先做一个敏感词测试集至少 100 条覆盖正常文本、命中词在开头/中间/结尾、带穿插字符、大小写混写、重复词、白名单词。JUnit 数据驱动一行一个用例ParameterizedTest CsvSource({ hello world, false, 这里包含敏感词内容, true, 敏.感.词, true, 这篇文章没有问题, false }) void filterCases(String input, boolean expected) { assertThat(engine.contains(input)).isEqualTo(expected); }这套用例是做回归用的。改词库、改过滤策略后跑一遍能省掉大量线上翻车。我最早做这类系统时只关心命中率没做回归测试一次词库更新把一批正常昵称全打了码线上被投诉才意识到先测试、再压测、后验证这三步一步不能省。6.2 压测与线上观察用 ab 或 wrk 压 filter 接口命令大致长这样ab -n 20000 -c 50 -p payload.json -T application/json http://localhost:8080/api/filter重点看三个指标QPS、P99 延迟、GC 停顿。压测时开 JFR 或 jstat 观察如果 GC 频繁说明节点对象太多回到第四章的内存估算去优化。敏感词筛选系统的代码量不大但把它当成一个完整工程来做收益远大于只写一个工具类。希望帮到你。本文还有配套的精品资源点击获取