评论系统是内容平台和社交应用的核心功能之一,而脏词过滤是保障内容安全、维护社区氛围的关键技术。本文将详细介绍如何在SpringBoot中实现一个高性能、支持动态更新的脏词过滤系统。
随着Web应用的发展,用户生成内容(UGC)面临着严峻的内容安全挑战。脏词过滤系统需要满足以下核心需求:
在脏词过滤场景下,DFA(确定有限状态自动机)算法相比传统方法有显著优势。
DFA算法通过前缀树(Trie树) 结构实现高效匹配:
以下是DFA算法的核心原理状态转移图:
系统采用分层架构,保证各模块职责单一:
脏词过滤系统架构
├── 数据存储层 (MySQL + Redis)
├── 算法核心层 (DFA引擎)
├── 业务服务层 (过滤服务、管理服务)
└── 应用接入层 (AOP切面、注解)
Trie树节点是DFA算法的基石,每个节点代表一个字符状态:
public class TrieNode {
// 子节点映射:Key为字符,Value为对应的子节点
private Map<Character, TrieNode> children = new HashMap<>();
// 标记当前节点是否为某个脏词的结尾
private boolean isEnd = false;
// 可选:用于记录到达此节点的完整脏词
private String keyword;
// Getter和Setter方法
public TrieNode getChild(char c) {
return children.get(c);
}
public TrieNode addChild(char c) {
return children.computeIfAbsent(c, k -> new TrieNode());
}
public boolean isEnd() {
return isEnd;
}
}
这个类负责构建Trie树并执行过滤逻辑:
@Component
public class SensitiveWordFilter {
private volatile TrieNode root; // 使用volatile保证多线程可见性
/**
* 构建Trie树
*/
private TrieNode buildTrie(Set<String> sensitiveWords) {
TrieNode root = new TrieNode();
for (String word : sensitiveWords) {
if (word == null || word.trim().isEmpty()) continue;
TrieNode currentNode = root;
for (char c : word.toCharArray()) {
currentNode = currentNode.addChild(c);
}
currentNode.setEnd(true);
currentNode.setKeyword(word);
}
return root;
}
/**
* 核心DFA匹配:判断是否包含脏词
*/
public boolean containsSensitiveWord(String text) {
if (text == null || text.length() < minWordLength) return false;
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++) {
TrieNode node = root;
for (int j = i; j < chars.length; j++) {
node = node.getChild(chars[j]);
if (node == null) break; // 转移失败,从i+1开始下一轮
if (node.isEnd()) return true; // 匹配到一个脏词
}
}
return false;
}
/**
* 替换文本中的脏词
*/
public String filter(String text, String replacement) {
if (text == null) return null;
StringBuilder result = new StringBuilder(text);
List<SensitiveWordResult> foundWords = findAllWords(text);
// 从后往前替换,避免影响索引
for (int i = foundWords.size() - 1; i >= 0; i--) {
SensitiveWordResult word = foundWords.get(i);
String replaceStr = String.valueOf(replacement)
.repeat(word.getEnd() - word.getStart() + 1);
result.replace(word.getStart(), word.getEnd() + 1, replaceStr);
}
return result.toString();
}
}
动态性是系统弹性的关键。通过结合数据库和缓存,可以实现在不重启服务的情况下更新脏词库。
建议设计两个表:
sensitive_word:存储脏词和基础配置sensitive_word_config:存储全局配置(如是否启用数字忽略等)@Service
public class SensitiveWordManager {
@Autowired
private SensitiveWordRepository wordRepository;
private volatile SensitiveWordFilter currentFilter;
private ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void init() {
loadWords();
// 每隔1小时自动刷新一次
scheduler.scheduleAtFixedRate(this::loadWords, 1, 1, TimeUnit.HOURS);
}
public void loadWords() {
try {
Set<String> newWordSet = wordRepository.findAllActiveWords();
SensitiveWordFilter newFilter = new SensitiveWordFilter();
newFilter.init(newWordSet);
this.currentFilter = newFilter; // 原子性切换引用
} catch (Exception e) {
// 记录日志,继续使用旧版本词库
}
}
// 提供手动刷新接口
@EventListener
public void handleRefreshEvent(RefreshEvent event) {
loadWords();
}
}
利用Spring AOP,我们可以优雅地将过滤逻辑织入到业务代码中:
@Aspect
@Component
public class SensitiveWordAspect {
@Autowired
private SensitiveWordFilter sensitiveWordFilter;
// 拦截所有带有@SensitiveFilter注解的方法
@Around("@annotation(sensitiveFilter)")
public Object filterSensitiveWords(ProceedingJoinPoint joinPoint,
SensitiveFilter sensitiveFilter) throws Throwable {
Object[] args = joinPoint.getArgs();
// 遍历方法参数,处理String类型参数
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
String originalText = (String) args[i];
if (sensitiveFilter.action() == Action.REPLACE) {
args[i] = sensitiveWordFilter.filter(originalText,
sensitiveFilter.replacement());
} else if (sensitiveFilter.action() == Action.REJECT) {
if (sensitiveWordFilter.containsSensitiveWord(originalText)) {
throw new IllegalArgumentException("内容包含违规词汇");
}
}
}
}
return joinPoint.proceed(args);
}
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveFilter {
Action action() default Action.REPLACE;
String replacement() default "***";
}
结合脏词过滤,实现完整的评论审核流程:
@Entity
public class Comment {
@Id
private Long id;
private String content;
private String author;
private LocalDateTime createTime;
private String status; // PENDING, APPROVED, REJECTED
// 审核后可见内容
public String getVisibleContent() {
return "APPROVED".equals(status) ? content : "该评论正在审核中";
}
}
当系统面临极高并发或海量脏词时,可以考虑以下高级优化:
使用双Trie树结构,实现零停机更新:一个用于当前查询,一个用于后台更新,更新完成后原子切换。
使用布隆过滤器快速判断一段文本"绝对不包含"脏词,避免不必要的DFA匹配。
本文介绍了基于SpringBoot和DFA算法的高性能脏词过滤系统完整实现方案。该方案具有以下特点:
| 特性 | 实现方案 | 优势 |
|---|---|---|
| 高性能 | DFA算法+Trie树 | O(n)时间复杂度,性能稳定 |
| 动态更新 | 数据库+定时任务+原子切换 | 支持热更新,无需重启 |
| 灵活集成 | 注解+AOP切面 | 非侵入式,业务代码无感知 |
| 可扩展性 | 多级缓存+分布式架构 | 支持水平扩展 |
实际项目中,建议根据具体业务场景选择合适的实现复杂度。对于中小型项目,可以从基础DFA实现开始;对于高并发大型平台,可以考虑引入更高级的优化策略。
此方案已在多个内容平台实践中验证,能有效平衡性能需求与内容安全要求,为社区健康氛围提供技术保障。