01前言
敏感词过滤是确保平台合规运营的关键防线。
从新闻资讯、社交媒体到电商评价,各类平台都需要对发布内容进行严格筛查,以屏蔽低俗、违法等不良信息。
本文深度剖析如何借助自定义注解与 DFA 算法,
在 SpringBoot 项目中打造高效、灵活的敏感词过滤系统。
02 敏感词过滤服务架构精要
本方案以 自定义注解 与 DFA 算法(Deterministic Finite Automaton,确定性有限状态自动机) 为核心,构建高效敏感词过滤服务。
它具备两种注解应用模式,既能标注方法参数,逐个字段过滤;
又能作用于实体类,批量处理对象属性。
同时,提供两种用户体验模式,既可以直接告知用户存在哪些敏感词,方便用户自查修改;
也可以按预设规则自动替换敏感词为指定字符,实现内容的无缝净化展示。
03 核心代码实现深度解析
(一)敏感词 Redis 存储与初始化优化
package com.tb.sensitiveword.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.tb.sensitiveword.constant.GlobalConstants;
import com.tb.sensitiveword.service.ISensitiveWordFilterService;
import com.tb.sensitiveword.util.RedisCache;
import org.springframework.stereotype.Service;
@Service
publicclassSensitiveWordFilterServiceImplimplementsISensitiveWordFilterService {
@Resource
private RedisCache redisCache;
/**
* 初始化敏感词到 Redis
* @param words 敏感词列表
* @return 操作是否成功
*/
@Override
publicbooleaninitSensitiveWord2Redis(List<String > words) {
if (CollectionUtil.isEmpty(words)) {
returnfalse;
}
// 先清除旧的敏感词列表,确保数据准确性
redisCache.deleteObject(GlobalConstants.REDIS_KEY_PREFIX + GlobalConstants.SENSITIVE_WORD_KEY);
// 将新的敏感词列表存入 Redis,设置合理的过期时间可在此处添加
redisCache.setCacheList(GlobalConstants.REDIS_KEY_PREFIX + GlobalConstants.SENSITIVE_WORD_KEY, words);
returntrue;
}
/**
* 从 Redis 获取敏感词列表
* @return 敏感词列表
*/
@Override
public List<String > sensitiveWordsFromRedis() {
return redisCache.getCacheList(GlobalConstants.REDIS_KEY_PREFIX + GlobalConstants.SENSITIVE_WORD_KEY);
}
}
注解:
- 依赖注入优化 :通过 @Resource 注解明确指定 RedisCache 依赖注入,增强代码可读性与可维护性,便于后续单元测试与组件替换。
- 数据操作精细化 :在初始化敏感词时,先删除旧数据再写入新数据,避免数据冗余与不一致问题。
同时,可根据业务需求灵活设置 Redis 中敏感词列表的过期时间,保障敏感词数据的时效性与准确性。 - 健壮性提升 :对输入参数 words 进行非空校验,防止空列表导致的异常,提高服务的健壮性。
(二)DFA 算法驱动的敏感词处理工具升级
package com.tb.sensitiveword.util;
import cn.hutool.core.collection.CollectionUtil;
import com.tb.sensitiveword.constant.GlobalConstants;
import com.tb.sensitiveword.model.entity.TrieNode;
import com.tb.sensitiveword.service.ISensitiveWordFilterService;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
publicclassTrieOperateUtil {
@Autowired
private ISensitiveWordFilterService sensitiveWordFilterService;
privateTrieNoderootNode=newTrieNode();
/**
* 添加敏感词到前缀树
* @param word 敏感词
*/
publicvoidaddWord(String word) {
TrieNodetmpNode= rootNode;
for (inti=0; i < word.length(); i++) {
charc= word.charAt(i);
TrieNodenode= tmpNode.getSubNode(c);
if (node == null) {
node = newTrieNode();
tmpNode.addSubNode(c, node);
}
tmpNode = node;
// 标记单词结尾
if (i == word.length() - 1) {
tmpNode.setKeywordEnd(true);
}
}
}
/**
* 替换文本中的敏感词
* @param text 待处理文本
* @param afterReplace 替换后的字符
* @return 替换后的文本
*/
public String replace(String text, String afterReplace) {
if (StringUtils.isBlank(text)) {
returnnull;
}
StringBuilderresult=newStringBuilder();
TrieNodetmpNode= rootNode;
intbegin=0, pos = 0;
while (pos < text.length()) {
charc= text.charAt(pos);
// 判断是否为符号
if (isSymbol(c)) {
// 若当前处于根节点,直接追加符号
if (tmpNode == rootNode) {
result.append(c);
begin++;
}
pos++;
continue;
}
tmpNode = tmpNode.getSubNode(c);
if (tmpNode == null) {
// 未匹配到敏感词,追加字符并重置状态
result.append(text.charAt(begin));
pos = ++begin;
tmpNode = rootNode;
} elseif (tmpNode.isLastCharacter()) {
// 匹配到敏感词结尾,进行替换操作
result.append(StringUtils.isEmpty(afterReplace) ? GlobalConstants.REPLACEMENT : afterReplace);
begin = ++pos;
tmpNode = rootNode;
} else {
pos++;
}
}
// 追加剩余字符
result.append(text.substring(begin));
return result.toString();
}
/**
* 查找文本中的敏感词
* @param text 待检测文本
* @return 敏感词及其出现次数的映射
*/
public Map<String , Integer > find(String text) {
Map<String , Integer > resultMap = newHashMap<>(16);
TrieNodetmpNode= rootNode;
StringBuilderword=newStringBuilder();
intbegin=0, pos = 0;
while (pos < text.length()) {
charc= text.charAt(pos);
tmpNode = tmpNode.getSubNode(c);
if (tmpNode == null) {
pos = ++begin;
tmpNode = rootNode;
} elseif (tmpNode.isLastCharacter()) {
// 敏感词匹配成功,记录结果
Stringw= word.append(c).toString();
resultMap.put(w, resultMap.getOrDefault(w, 0) + 1);
begin = ++pos;
tmpNode = rootNode;
word = newStringBuilder();
} else {
word.append(c);
pos++;
}
}
return resultMap;
}
/**
* 从 Redis 获取敏感词并构建前缀树
* @return 构建前缀树所使用的敏感词列表
*/
public List<String > sensitiveWordsFromRedisAndSet() {
List<String > words = sensitiveWordFilterService.sensitiveWordsFromRedis();
if (CollectionUtil.isNotEmpty(words)) {
for (String word : words) {
addWord(word);
}
}
return words;
}
/**
* 判断字符是否为符号
* @param c 字符
* @return 是否为符号
*/
privatebooleanisSymbol(Character c) {
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
}
注解:
- 算法逻辑优化 :在敏感词查找与替换过程中,精准区分符号与敏感词字符,避免因符号干扰导致的敏感词匹配错误,提高算法准确性。
例如,当文本中出现 “敏感词!” 时,能准确匹配 “敏感词” 并进行相应处理,而不是将 “!” 误判为敏感词的一部分。
- 性能提升细节 :采用前缀树(Trie 树)数据结构存储敏感词,大幅提高敏感词匹配效率。
对于大量敏感词与长文本的处理场景,相比传统暴力匹配算法,性能提升显著。
前缀树的层级结构使得在匹配过程中,能够快速定位可能的敏感词路径,减少不必要的字符比对。
(三)自定义注解与 AOP 切面协同作战
package com.tb.sensitiveword.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidSensitiveWords {
boolean isValid() default false;
}
package com.tb.sensitiveword.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterSensitiveWords {
String replacement()default""; // 默认替换字符为空,可在使用时灵活指定
boolean isReplace()defaultfalse; // 默认不进行替换操作,仅用于检测
}
package com.tb.sensitiveword.aop;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson2.JSONObject;
import com.tb.sensitiveword.annotation.FilterSensitiveWords;
import com.tb.sensitiveword.annotation.ValidSensitiveWords;
import com.tb.sensitiveword.util.TrieOperateUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.*;
@Component
@Aspect
publicclassSensitiveWordsAspect {
privatestaticfinalLoggerlogger= LoggerFactory.getLogger(SensitiveWordsAspect.class );
@Autowired
private TrieOperateUtil trieOperateUtil;
@Pointcut("@annotation(com.tb.sensitiveword.annotation.ValidSensitiveWords)")
publicvoidpointcut() {
}
@Around("pointcut()")
public Object filterSensitiveWords(ProceedingJoinPoint joinPoint)throws Throwable {
Object[] args = joinPoint.getArgs();
if (joinPoint.getSignature() instanceof MethodSignature) {
MethodSignaturemethodSignature= (MethodSignature) joinPoint.getSignature();
ValidSensitiveWordsanno= methodSignature.getMethod().getAnnotation(ValidSensitiveWords.class );
if (!anno.isValid()) {
return joinPoint.proceed();
}
Parameter[] parameters = methodSignature.getMethod().getParameters();
for (inti=0; i < parameters.length; i++) {
Parameterparameter= parameters[i];
JSONObjectresult= fieldSensitiveWorldFilter(args, i, parameter);
if (result.containsKey("isExist") && result.getBoolean("isExist")) {
Stringwords= result.getString("words");
thrownewRuntimeException("存在敏感内容【" + words + "】,请重新输入!");
}
}
}
return joinPoint.proceed(args);
}
private JSONObject fieldSensitiveWorldFilter(Object[] args, int i, Parameter parameter)throws IllegalAccessException {
JSONObjectjsonObj=newJSONObject();
Set<String > sensitiveWordsSet = newHashSet<>();
Class<?> type = parameter.getType();
if (type == String .class ) {
FilterSensitiveWordsfilterSensitiveWords= parameter.getAnnotation(FilterSensitiveWords.class );
if (filterSensitiveWords != null) {
Stringtext= String .valueOf(args[i]);
if (filterSensitiveWords.isReplace()) {
StringnewText= replaceWord(filterSensitiveWords, text);
args[i] = newText;
} else {
JSONObjectresult= findWord(text);
if (result.getBoolean("isExist")) {
jsonObj.put("isExist", result.getBoolean("isExist"));
sensitiveWordsSet.addAll(result.getJSONObject("wordsMap").keySet());
}
}
}
}
if (type.getClassLoader() != null) {
Field[] declaredFields = type.getDeclaredFields();
Objectobj= args[i];
for (Field declaredField : declaredFields) {
if (declaredField.getAnnotation(FilterSensitiveWords.class ) != null) {
FilterSensitiveWordsfilterSensitiveWords= declaredField.getAnnotation(FilterSensitiveWords.class );
if (declaredField.getType() == String .class ) {
declaredField.setAccessible(true);
StringfieldValue= String .valueOf(declaredField.get(obj));
if (filterSensitiveWords.isReplace()) {
StringnewText= replaceWord(filterSensitiveWords, fieldValue);
declaredField.set(obj, newText);
} else {
JSONObjectresult= findWord(fieldValue);
if (result.getBoolean("isExist")) {
jsonObj.put("isExist", result.getBoolean("isExist"));
sensitiveWordsSet.addAll(result.getJSONObject("wordsMap").keySet());
}
}
}
}
}
}
jsonObj.put("words", String .join(",", sensitiveWordsSet));
return jsonObj;
}
private String replaceWord(FilterSensitiveWords filterSensitiveWords, String fieldValue) {
return trieOperateUtil.replace(fieldValue, filterSensitiveWords.replacement());
}
private JSONObject findWord(String fieldValue) {
JSONObjectresult=newJSONObject();
booleanisExist=false;
Map<String , Integer > wordsMap = newHashMap<>();
trieOperateUtil.sensitiveWordsFromRedisAndSet();
wordsMap = trieOperateUtil.find(fieldValue);
if (CollectionUtil.isNotEmpty(wordsMap)) {
isExist = true;
}
result.put("isExist", isExist);
result.put("wordsMap", newJSONObject(wordsMap));
return result;
}
}
注解:
- 注解设计灵活性 :ValidSensitiveWords 注解用于开启敏感词过滤功能,可作用于方法或类级别,方便全局或局部控制。
FilterSensitiveWords 注解则针对具体参数或字段,提供替换字符与是否替换的配置选项,实现细粒度的敏感词处理策略定制。
- AOP 切面逻辑严谨性 :通过 AOP 切面在方法执行前拦截请求,先检查方法参数是否开启敏感词过滤(ValidSensitiveWords.isValid()),若开启,则依据参数或字段上的 FilterSensitiveWords 注解配置,分别执行敏感词检测与替换操作。
对于检测到敏感词的情况,及时抛出异常反馈,保障业务数据的合规性流入。 - 对象属性深度处理 :在处理实体类对象时,利用反射机制遍历对象字段,精准定位带有 FilterSensitiveWords 注解的字符串字段,无论是简单对象还是复杂对象嵌套场景,均能有效实施敏感词过滤,满足多样化业务场景下的内容安全需求。
(四)控制器层接口实战示例
package com.tb.sensitiveword.controller;
import com.tb.sensitiveword.annotation.FilterSensitiveWords;
import com.tb.sensitiveword.annotation.ValidSensitiveWords;
import com.tb.sensitiveword.model.entity.News;
import com.tb.sensitiveword.model.entity.WordDTO;
import com.tb.sensitiveword.service.ISensitiveWordFilterService;
import com.tb.sensitiveword.util.ResponseResult;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/sensitive-word")
publicclassSensitiveWordFilterController {
@Resource
private ISensitiveWordFilterService sensitiveWordFilterService;
/**
* 初始化敏感词到 Redis
* @param wordDTO 敏感词数据传输对象
* @return 操作结果
*/
@PostMapping("/init")
public ResponseResult<Void> initSensitiveWords2Redis(@RequestBody WordDTO wordDTO) {
booleanresult= sensitiveWordFilterService.initSensitiveWord2Redis(wordDTO.getWords());
return result ? ResponseResult.okResult() : ResponseResult.failResult("敏感词初始化失败");
}
/**
* 保存新闻资讯(基于方法参数注解)
* @param content 新闻内容
* @return 操作结果
*/
@PostMapping("/save-news-method")
@ValidSensitiveWords(isValid = true)
public ResponseResult<Void> saveNewsByMethod(@RequestParam @FilterSensitiveWords(isReplace = true, replacement = "***") String content) {
// 业务逻辑处理
return ResponseResult.okResult();
}
/**
* 保存新闻资讯(基于实体类注解)
* @param news 新闻实体
* @return 操作结果
*/
@PostMapping("/save-news-entity")
public ResponseResult<News> saveNewsByEntity(@RequestBody News news) {
// 业务逻辑处理
return ResponseResult.okResult(news);
}
}
package com.tb.sensitiveword.model.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.tb.sensitiveword.annotation.FilterSensitiveWords;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclassNewsimplementsSerializable {
privatestaticfinallongserialVersionUID=1L;
private String id;
private String title;
@FilterSensitiveWords(isReplace = true, replacement = "***")
private String content;
private String author;
private String source;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime publishTime;
}
注解:
- 接口多样化设计 :提供两种新闻保存接口,分别演示基于方法参数注解与实体类注解的敏感词过滤方式,满足不同业务场景下对参数处理的灵活需求。
例如,在简单的文本提交场景使用方法参数注解快速过滤,对于复杂的业务对象则借助实体类注解进行全面字段筛查。 - 注解配置实用性 :在 News 实体类的 content 字段上添加 @FilterSensitiveWords 注解,指定 isReplace = true 表示对敏感词进行替换,replacement = “***” 定义替换后的字符为三个星号。
这样,在新闻内容保存前,系统会自动依据敏感词库将内容中的敏感词替换为 “***”,实现内容的自动净化,无需人工干预,高效且可靠。