好家伙,Claude Code 竟然写了这么多 Bug!
大家好,我是 Guide!元旦假期我不是开源了 InterviewGuide 这个 AI 项目嘛,这个项目开发过程中,深度体验了 Claude Code、Cursor 等 AI 编程工具。
不得不说,AI 的产出效率确实惊人,但“效率越高,坑埋得越深”。AI 往往能写出逻辑通顺的代码,却可能在 Spring 框架底层原理、异步时序、资源管理等方面给你“下毒”。
今天,复盘一下在 InterviewGuide 开发中,被 Claude 坑过的 4 个典型 Bug。每一个都是面试高频考点,建议反复阅读,避免在你的项目里留下“定时炸弹”。
1. @Transactional 自调用导致事务增强失效
这是 Spring 事务失效最经典的场景,也是面试高频考点!
场景还原
在知识库上传逻辑中,Claude 帮我写了一个“上传后继续处理”的流程。它为了省事,直接在同一个 Service 类里写了两个带事务的方法,然后在方法内部用 this 调用另一个方法。
说明:这里的核心问题是“事务 AOP 代理被绕过”,与是否异步无关。下面示例为突出事务问题,先不展开异步实现。
问题代码
@Service
publicclassKnowledgeBaseUploadService{
@Transactional
publicvoiduploadAndProcess(File file){
saveFile(file);
// 自调用:绕过代理对象,导致 processInNewTx() 上的事务配置不生效
this.processInNewTx(file);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
publicvoidprocessInNewTx(File file){
// 期望在新事务运行(REQUIRES_NEW),但自调用会导致该注解不生效
// 结果:不会开启“新事务”,往往会在当前调用上下文(通常是外层事务)里执行
}
}
原理分析
Spring 基于的@Transactional是AOP 动态代理实现的。
正常流程:外部 Bean 调用 service.method()→ 进入Proxy 代理对象→ 开启/加入事务 → 执行目标方法自调用流程:内部通过 this调用 → 直接调用目标对象(Target) → 事务拦截器根本没机会执行 →processInNewTx()上的事务传播属性(如REQUIRES_NEW)不会生效
修复方案
核心原则:调用必须“跨过代理”,不要this.xxx()。
最稳定的做法是:把需要新事务的方法拆到另一个 Bean 上,通过注入后调用(确保经过代理)。
@Service
@RequiredArgsConstructor
publicclassKnowledgeBaseUploadService{
privatefinal KnowledgeBaseProcessService processService;
@Transactional
publicvoiduploadAndProcess(File file){
saveFile(file);
processService.processInNewTx(file); // 通过代理调用,事务增强生效
}
}
@Service
publicclassKnowledgeBaseProcessService{
@Transactional(propagation = Propagation.REQUIRES_NEW)
publicvoidprocessInNewTx(File file){
// 现在可以保证以新事务边界执行
}
}
2. AI 响应解析空指针
现象
面试评估功能在压力测试时,偶尔会报 NullPointerException,导致整个面试流程中断。
原因
LLM 的输出具有随机性。即使在提示里要求“必须返回 JSON”,也可能出现:
字段删除或为 null结构变化(字段名拼错) 类型不一致(数字变字符串) token 截断/中断导致 JSON 不完整
Claude 写的解析代码太“简单”,直接相信了 AI 的输出。
// 问题代码:dto.questionEvaluations() 可能为 null
for (int i = 0; i < dto.questionEvaluations().size(); i++) {
// NPE!
}
修复
在 LLM 应用里,永远不要信任外部输入(包括 AI)。建议做到两层防护:
解析层:保证结构可解析(必要时做 schema/字段校验) 业务层:保证流程不崩溃(兜底策略+记录异常)
List<QuestionEvaluationDTO> evaluations = dto.questionEvaluations();
if (evaluations == null || evaluations.isEmpty()) {
log.error("AI 响应异常:面试评估列表缺失或为空,sessionId={}", sessionId);
// 兜底策略示例:直接降级为“无评估”,保证流程继续
evaluations = Collections.emptyList();
// 也可以 return + 返回默认评估结果,看你的业务需要
}
// 只处理与问题数量对齐的部分,避免越界
int n = Math.min(evaluations.size(), questions.size());
for (int i = 0; i < n; i++) {
QuestionEvaluationDTO ev = evaluations.get(i);
// 对 ev 内部字段也做必要校验(例如 ev.getScore() 为空就给默认值)
}
3. 删除实体后异步任务报错
现象
后台日志频繁出现 简历不存在: ID=35 的 Error。检查发现,这是由于用户删除了简历,但分析任务还在跑。
原因
这是导致数据不一致的典型问题:
用户上传简历 → 发送分析任务到 Redis Stream 分析失败 → 消息进入 pending/等待重试 用户删除简历 → 数据库记录已删除 消费者重试处理 → 找不到简历 → 报错
修复
把“生命周期校验”放在异步任务处理的最前面,并区分:
不可恢复错误(实体不存在、参数非法)→ 记录后 ACK/丢弃 可恢复错误(临时网络故障、依赖服务超时)→ 不 ACK,让其重试或进入重试队列
示例(用一次查询代替existsById + findById两次查询):
privatevoidprocessMessage(StreamMessageId messageId, Map<String, String> data){
Long resumeId = Long.parseLong(data.get("resumeId"));
var resumeOpt = resumeRepository.findById(resumeId);
if (resumeOpt.isEmpty()) {
// 不可恢复:实体已被用户删除(或数据已不存在)
log.warn("检测到实体已被删除,跳过异步任务: resumeId={}", resumeId);
ackMessage(messageId); // 必须 ACK,否则会反复重试造成噪音与堆积
return;
}
try {
Resume resume = resumeOpt.get();
// 继续业务逻辑...
ackMessage(messageId);
} catch (TransientDependencyException e) {
// 可恢复错误:不 ACK,让其重试(或转入重试/死信机制)
log.warn("依赖异常,等待重试: resumeId={}, msgId={}", resumeId, messageId, e);
throw e;
} catch (Exception e) {
// 根据你的策略决定是否 ACK/重试/转死信
log.error("处理失败: resumeId={}, msgId={}", resumeId, messageId, e);
throw e;
}
}
4. Redis Stream 消息无限堆积
现象
Redis 中某个 Stream 积累了 100+ 条消息,且持续增长。
原因
一个常见的误区:XACK 只是确认消息已被消费,不会删除 Stream 里的消息条目。
XADD:写入消息到 StreamXREADGROUP:消费者组读取消息XACK:确认消费(把消息从消费者组的 PEL / Pending Entries List “待处理列表”里移除)XDEL:从 Stream 中删除指定消息条目XTRIM/MAXLEN:裁剪 Stream(限制 Stream 长度,删除较旧的条目)
如果你既没有 XDEL,也没有 XTRIM/MAXLEN,那么 Stream 里的历史消息会持续累积,占用内存/磁盘。生产环境中,最推荐的方式是在写入时直接指定 MAXLEN,实现类似于定长环形队列的效果。
另外还有一种“堆积”是 PEL 堆积:消费者没有
XACK,导致待确认(pending)的消息越来越多。两者要区分排查。
修复
发送消息时添加 MAXLEN 限制,自动裁剪旧消息:
// 修复前
stream.add(StreamAddArgs.entries(message));
// 修复后(自动裁剪超过 1000 条的旧消息)
stream.add(StreamAddArgs.entries(message)
.trimNonStrict().maxLen(1000));
trimNonStrict(): 使用近似裁剪(~),性能更好maxLen(1000): 保留最新 1000 条消息
总结
AI 编程时代的到来,确实降低了代码的编写门槛,但也拔高了代码审计的门槛,对使用者能力的要求还是有门槛的。
| Bug 场景 | 考察点 | 关键词 |
|---|---|---|
| 事务失效 | this | |
| AI 解析 NPE | ||
| 异步报错 | ||
| Stream 堆积 | XACKXDEL、MAXLEN |
AI 写的代码,一定要经过你的“穿透式 Review”。理解每一行代码背后的框架原理和系统边界,才是你作为核心开发者的护城河。
源码地址:
Github 地址:https://github.com/Snailclimb/interview-guide Gitee 地址:https://gitee.com/SnailClimb/interview-guide