MyBatisPlus拦截器记录VibeVoice请求日志
在当前AI语音生成系统快速迭代的背景下,可观测性已成为衡量一个智能服务是否“可运维、可调试、可持续”的关键标准。以VibeVoice-WEB-UI为例,这套支持长文本、多角色对话合成的语音生成平台,虽然前端交互流畅、模型能力强大,但在实际部署中一旦出现生成异常或性能瓶颈,若缺乏完整的请求上下文追踪机制,排查问题往往如盲人摸象。
传统做法是在业务代码中手动插入日志记录语句——比如在generate()方法开头打一条 info 日志。但这种方式不仅侵入性强,容易遗漏边缘路径,还会随着功能演进变得越来越臃肿。更糟糕的是,当面对一次失败的90分钟音频生成任务时,如果日志里只写着“开始生成”,而没有保存当时的完整参数快照,那后续复现和修复几乎无从谈起。
有没有一种方式,能在不改动现有业务逻辑的前提下,自动捕获每一次关键请求的核心数据?答案是:利用MyBatisPlus 拦截器实现非侵入式请求日志埋点。
为什么选择 MyBatisPlus 拦截器?
MyBatisPlus 作为 Java 生态中最主流的 ORM 增强框架之一,其插件体系基于 MyBatis 的原生 Plugin 机制构建,允许开发者在 SQL 执行链路上“挂载”自定义逻辑。这种机制本质上是一种轻量级 AOP(面向切面编程),非常适合用于实现监控、审计、权限控制等横切关注点。
我们并不需要去监听所有的数据库操作,而是聚焦于一个特定行为:当系统准备将一条语音生成请求写入request_log表时,顺手把当前 HTTP 请求的上下文信息也一并固化下来。
这听起来像是一个简单的数据持久化增强,但它带来的价值远超预期:
- 不用在每个 Controller 或 Service 里重复写
log.info(); - 避免因忘记记录而导致关键请求“失联”;
- 可统一收集用户身份、IP地址、请求参数、执行时间等元信息;
- 支持异步落库,不影响主流程性能。
换句话说,它让日志采集这件事从“被动记录”变成了“主动沉淀”。
拦截器是如何工作的?
整个流程可以拆解为几个关键步骤:
- 用户通过 Web 界面提交一段包含多个角色的对话脚本;
- 前端调用
/api/generate接口,携带 JSON 格式的参数对象; - 后端 Controller 解析请求,构造
VoiceGenerationTask实体; - 调用
requestLogService.save(task)将请求信息存入数据库; - 此时触发 MyBatisPlus 的
INSERT INTO request_log操作; - 拦截器被激活,提取 SQL 绑定参数、MappedStatement 元数据;
- 结合 Spring 的
RequestContextHolder获取当前 HTTP 请求上下文; - 构造完整日志记录并异步写入日志表或推送至消息队列。
这个过程的关键在于:拦截发生在 ORM 层,而非业务层。这意味着只要最终会落到数据库的操作,都能被捕获,无论它是来自 REST API、定时任务还是内部事件驱动。
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Component public class RequestLogInterceptor implements Interceptor { private static final String REQUEST_LOG_TABLE = "request_log"; private static final Logger logger = LoggerFactory.getLogger(RequestLogInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; // 只处理 request_log 表的插入操作 if (!ms.getId().contains(REQUEST_LOG_TABLE) || ms.getSqlCommandType() != SqlCommandType.INSERT) { return invocation.proceed(); } BoundSql boundSql = ms.getBoundSql(parameter); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); RequestLogRecord logRecord = new RequestLogRecord(); logRecord.setUserId(getCurrentUserId()); logRecord.setClientIp(getClientIp(request)); logRecord.setRequestTime(new Date()); logRecord.setParamJson(JSON.toJSONString(boundSql.getParameterObject())); logRecord.setFullSql(boundSql.getSql()); // 异步提交,避免阻塞主事务 AsyncTaskManager.submit(() -> saveLogToDatabase(logRecord)); logger.debug("Captured VibeVoice request: user={}, ip={}, params_size={}", logRecord.getUserId(), logRecord.getClientIp(), logRecord.getParamJson().length()); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) {} private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } private String getCurrentUserId() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return auth != null ? auth.getName() : "anonymous"; } private void saveLogToDatabase(RequestLogRecord record) { try { requestLogMapper.insert(record); } catch (Exception e) { logger.error("Failed to persist request log", e); } } }这段代码看似简单,实则暗藏工程智慧:
- 使用
@Intercepts注解精准定位到Executor.update方法,覆盖所有增删改操作; - 通过
MappedStatement.getId()判断是否为目标表操作,避免全量拦截造成性能损耗; - 利用
RequestContextHolder和SecurityContextHolder获取运行时上下文,无需显式传递参数; - 异步提交日志写入任务,防止 I/O 阻塞影响主流程响应速度;
- 内部异常完全捕获,确保即使日志组件故障也不会拖垮主业务。
更重要的是,这套机制对业务代码零侵入。你不需要在任何地方添加saveLog()调用,只要调用了insert方法,日志就会自动沉淀下来。
在 VibeVoice 中的实际应用场景
VibeVoice-WEB-UI 的核心优势之一是支持超长文本合成,单次请求可生成长达 90 分钟的音频内容。这类请求通常包含复杂的结构化输入,例如:
{ "scenes": [ { "title": "会议开场", "characters": [ { "name": "张经理", "text": "各位同事早上好..." }, { "name": "李工", "text": "我来汇报一下进度..." } ] } ], "voiceStyle": "professional", "outputFormat": "mp3" }如果这样的请求在模型推理阶段失败了,仅靠错误码和堆栈信息很难定位问题根源。但如果我们在request_log表中已经保存了完整的paramJson字段,就可以直接还原当时的输入内容,甚至可以通过脚本批量重放可疑请求进行回归测试。
此外,在多用户共享环境中(如高校实验室或企业内部工具平台),责任追溯尤为重要。曾有一次,系统突然出现大量高负载请求,导致 GPU 显存耗尽。通过查询日志表中的userId和clientIp字段,我们迅速锁定是一位实习生在本地脚本中误用了无限循环发起合成请求。如果没有这些日志,排查可能要耗费数小时。
性能与稳定性设计考量
尽管拦截器本身开销极小,但在高频场景下仍需谨慎设计,以下是我们在实践中总结的关键经验:
✅按需拦截,避免全表扫描
不要盲目拦截所有表的写入操作。应明确目标表名(如request_log),并通过 Mapper ID 或 SQL 模式匹配进行过滤。否则每条UPDATE user SET last_login=...都会被处理,反而成为性能瓶颈。
✅敏感字段脱敏处理
语音请求中可能包含用户隐私文本(如个人对话、内部文档)。建议在记录paramJson前做选择性脱敏,例如替换姓名、手机号等字段为占位符,符合 GDPR 和《网络安全法》要求。
✅异步化 + 失败容忍
日志写入必须异步执行,并使用独立线程池或任务队列。同时,整个拦截逻辑要包裹 try-catch,哪怕日志保存失败也不能影响主事务提交。毕竟,“不能生成语音”是致命问题,“没记日志”只是次要缺陷。
✅可配置开关,支持灰度发布
通过配置项控制拦截器启用状态:
interceptor: request-log-enabled: true便于在生产环境临时关闭、调试或做性能对比。
✅存储策略优化
日志数据增长迅速,建议采用以下策略:
- 日志表按月分区;
- 保留周期设为 90 天,过期自动归档;
- 高频场景接入 Kafka,由消费端写入 ES 或 ClickHouse,支撑实时分析。
系统架构中的位置与协同
在整个 VibeVoice-WEB-UI 架构中,该拦截器位于持久层与业务层之间,扮演着“透明审计探针”的角色:
[前端 UI] ↓ HTTPS [Spring Boot Controller] ↓ [Service Layer - 参数校验/任务封装] ↓ [MyBatisPlus insert(requestLog)] ↘ ↘ [主流程继续 → 调用模型生成] [RequestLogInterceptor → 提取上下文 → 异步落库] ↓ [ELK / Grafana 展示]它不像 APM 工具那样监控 JVM 指标,也不像链路追踪那样依赖 traceId 传递,而是专注于业务动作的语义记录——即“谁在什么时候发起了什么样的语音生成请求”。
这种细粒度的操作留痕,为后续的数据分析提供了坚实基础。例如:
- 统计不同角色组合的使用频率,辅助音色优化优先级排序;
- 分析平均请求长度分布,指导缓存策略调整;
- 检测异常调用模式(如短时间高频请求),触发风控告警;
- 审计合规审查,满足企业级应用对操作可追溯性的要求。
更进一步:不只是记录,更是治理桥梁
很多人认为日志只是一个“出事后再看的东西”,但实际上,高质量的日志本身就是一种主动治理能力。
在 VibeVoice 的迭代过程中,我们曾发现某些特定格式的输入会导致模型推理超时。通过分析request_log中的历史请求,我们提取出共性特征(如嵌套层级过深、特殊符号过多),并据此在前端增加了输入合法性校验规则,从根本上减少了无效请求的产生。
这说明,日志不仅是“事故回放带”,还可以成为“产品优化燃料”。
而 MyBatisPlus 拦截器正是点燃这一闭环的火种。它用最少的代码改动,撬动了最大的可观测性收益。
结语
在这个 AI 应用日益复杂的时代,后端系统的“看得见”比“跑得快”更重要。VibeVoice-WEB-UI 虽然核心能力在于语音生成,但真正让它稳定服务于多用户、长时间运行的,反而是这些不起眼的基础设施——比如一个小小的拦截器。
它不参与模型计算,不决定音频质量,却默默守护着每一次请求的完整性与可追溯性。正如一栋高楼的地基,虽不可见,却承载万钧。
未来,我们计划将此类拦截机制扩展至更多场景:
- 拦截task_status更新,构建任务生命周期追踪;
- 结合 Redis 缓存命中情况,记录热点请求模式;
- 推送日志至 Prometheus,实现请求量级的可视化监控。
技术不分大小,唯有恰到好处的运用,才能在关键时刻发挥巨大价值。MyBatisPlus 拦截器或许只是个“配角”,但在 VibeVoice 的舞台上,它早已成为不可或缺的一员。