SpringBoot - 基于自定义注解+DFA算法,实现敏感词过滤

发布于:2025-06-24 ⋅ 阅读:(16) ⋅ 点赞:(0)

在这里插入图片描述

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);
    }
}

注解:

  1. 依赖注入优化 :通过 @Resource 注解明确指定 RedisCache 依赖注入,增强代码可读性与可维护性,便于后续单元测试与组件替换。
  2. 数据操作精细化 :在初始化敏感词时,先删除旧数据再写入新数据,避免数据冗余与不一致问题。
    同时,可根据业务需求灵活设置 Redis 中敏感词列表的过期时间,保障敏感词数据的时效性与准确性。
  3. 健壮性提升 :对输入参数 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);
    }
}

注解:

  1. 算法逻辑优化 :在敏感词查找与替换过程中,精准区分符号与敏感词字符,避免因符号干扰导致的敏感词匹配错误,提高算法准确性。

例如,当文本中出现 “敏感词!” 时,能准确匹配 “敏感词” 并进行相应处理,而不是将 “!” 误判为敏感词的一部分。

  1. 性能提升细节 :采用前缀树(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;
    }
}

注解:

  1. 注解设计灵活性 :ValidSensitiveWords 注解用于开启敏感词过滤功能,可作用于方法或类级别,方便全局或局部控制。

FilterSensitiveWords 注解则针对具体参数或字段,提供替换字符与是否替换的配置选项,实现细粒度的敏感词处理策略定制。

  1. AOP 切面逻辑严谨性 :通过 AOP 切面在方法执行前拦截请求,先检查方法参数是否开启敏感词过滤(ValidSensitiveWords.isValid()),若开启,则依据参数或字段上的 FilterSensitiveWords 注解配置,分别执行敏感词检测与替换操作。
    对于检测到敏感词的情况,及时抛出异常反馈,保障业务数据的合规性流入。
  2. 对象属性深度处理 :在处理实体类对象时,利用反射机制遍历对象字段,精准定位带有 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;
}

注解:

  1. 接口多样化设计 :提供两种新闻保存接口,分别演示基于方法参数注解与实体类注解的敏感词过滤方式,满足不同业务场景下对参数处理的灵活需求。
    例如,在简单的文本提交场景使用方法参数注解快速过滤,对于复杂的业务对象则借助实体类注解进行全面字段筛查。
  2. 注解配置实用性 :在 News 实体类的 content 字段上添加 @FilterSensitiveWords 注解,指定 isReplace = true 表示对敏感词进行替换,replacement = “***” 定义替换后的字符为三个星号。

这样,在新闻内容保存前,系统会自动依据敏感词库将内容中的敏感词替换为 “***”,实现内容的自动净化,无需人工干预,高效且可靠。


网站公告

今日签到

点亮在社区的每一天
去签到