@Autowired 的Bug让我们白忙三天

微信小程序 - 获取当前日期时间(函数封装) 效果图 前言 摘自微信小程序demo程序中utils.js方法。 普通newDate()出来的日期时间,并不是我们想要的格式,所以要进行格式整理。 解决方案 打开utils.js文件(随意),复制以下代码: /** *获取当前时间 *@description自定义日期分钟秒数 *@param{Object}date-newData()对象 *@returnString */ functionformatTime(date){ //年月日 阅读详情

两个“看似无害”的 PR 如何在凌晨 2 点联手击碎生产环境的依赖注入。

我们的首席架构师在一个构造器上加了 @Autowired,应用能编译。测试通过。代码评审也过了。

然后凌晨两点,生产炸了,NullPointerException 到处都是。

故事从一次“简单”的重构开始

如果你曾在周五合并过一个“很安全”的重构,你大概知道故事怎么发展。

我们在支付服务里清理技术债。没啥花哨的——把一个巨型类拆成更小、更可测的组件而已。

@Service
public class PaymentProcessor {
    
    @Autowired
    private PaymentGateway gateway;
    
    @Autowired
    private FraudDetector fraudDetector;
    
    @Autowired
    private NotificationService notificationService;
    
    // 847 行业务逻辑...
}

我们的架构师,就叫他 Dave,决定改用构造器注入。“最佳实践”,他说。干净、不可变、易测试。

听起来很合理,对吧?

@Service
public class PaymentProcessor {
    
    private final PaymentGateway gateway;
    private final FraudDetector fraudDetector;
    private final NotificationService notificationService;
    
    @Autowired
    public PaymentProcessor(
        PaymentGateway gateway,
        FraudDetector fraudDetector,
        NotificationService notificationService
    ) {
        this.gateway = gateway;
        this.fraudDetector = fraudDetector;
        this.notificationService = notificationService;
    }
    
    // 业务逻辑...
}

完美。final 字段、构造器注入,跟每篇 Spring Boot 教程教的一样。

周四发版。周五风平浪静。周末也很安稳。

周一清晨,地狱之门打开。

凌晨两点:一切开始崩坏

我们 Slack 的 #incidents 频道像圣诞树一样亮了起来。

PagerDuty: 🚨 CRITICAL: Payment Service - Error rate 34%
DataDog: Payment processing failures spiking
AWS CloudWatch: 500 errors on /api/payments/process

我是当周值班工程师。真“幸运”。

日志像噩梦:

java.lang.NullPointerException: Cannot invoke "FraudDetector.check()" because "this.fraudDetector" is null
    at PaymentProcessor.processPayment(PaymentProcessor.java:67)
    at PaymentController.createPayment(PaymentController.java:45)

等等,啥?

fraudDetector 是 null?可它是 @Autowired 的依赖啊。Spring 不应该给它注入吗?这不就是 Spring 的工作嘛。

我检查了 Bean 配置。FraudDetector 的 Bean 存在、已注册,其他服务用它也都没问题。

我重启了服务。还是一样的错误。

我看了下构造器。@Autowired 注解也在。

这到底怎么回事?

事情并不随机

然后事情开始变得诡异。

有些支付成功。大多数失败。

但并非随机。它有规律:

  • 小额支付(<$100):成功
  • 大额支付(>$100):抛 NullPointerException

这完全说不通。支付金额不应该影响依赖注入。这不是 Spring 的工作方式。

我到处加了调试日志:

@Autowired
public PaymentProcessor(
    PaymentGateway gateway,
    FraudDetector fraudDetector,
    NotificationService notificationService
) {
    System.out.println("Constructor called!");
    System.out.println("Gateway: " + gateway);
    System.out.println("FraudDetector: " + fraudDetector);
    System.out.println("NotificationService: " + notificationService);
    
    this.gateway = gateway;
    this.fraudDetector = fraudDetector;
    this.notificationService = notificationService;
}

日志显示构造器在启动时只被调用了一次。所有依赖都注入正确。

那运行期为什么 fraudDetector 会是 null?

周二早晨:Git Blame 揭晓谜底

周二早上,Dave 来了。我给他看日志。他很困惑。

“不可能,”他说,“构造器注入可以保证不可变。”

我打开 git diff。他的重构 PR——改了 47 个文件。

然后我看到了——埋在 diff 中间的那一行。

@Service
@Scope("prototype")  // 就是这一行
public class PaymentProcessor {
    // ...
}

有人给这个类加了 @Scope("prototype")。

不是 Dave。是上周另一个 PR。一个“性能优化”——一位初级同事以为每次创建新实例可以防止内存泄漏。

两个 PR 分别合并。没有冲突。评审都通过。各自看也都合理。

但放在一起?灾难。

技术拆解:为什么会炸

来解释一下 @Scope("prototype") 到底做了什么。

范围(Scope)

实例生命周期

常见陷阱

Singleton

启动时创建一次

与构造器依赖注入组合安全

Prototype

每次请求创建一个

与字段访问组合会出现问题

普通 Spring Bean(单例):

  1. Spring 在启动时创建一个实例
  2. 构造器只执行一次
  3. 依赖只注入一次
  4. 该实例服务所有请求

原型范围(prototype)的 Bean:

  1. 每次获取都会创建一个新实例
  2. 构造器每次都会执行
  3. 依赖也应该每次都被注入

但关键点在这里:

当其他服务(比如我们的 PaymentController)通过 @Autowired 注入 PaymentProcessor 时,Spring 注入的不是每次都创建的新实例,而是一个代理(proxy)。

这个代理会在“方法调用”时创建新实例,而不是在“字段访问”时创建。

所以这段代码:

@RestController
public class PaymentController {
    
    @Autowired
    private PaymentProcessor processor;
    
    public void handlePayment(Payment payment) {
        processor.processPayment(payment);  // 在这里触发创建新实例
    }
}

工作正常。方法调用会触发实例创建。

但我们还有第二条代码路径:

@Component
public class ScheduledPaymentJob {
    
    @Autowired
    private PaymentProcessor processor;
    
    @Scheduled(fixedRate = 60000)
    public void processScheduledPayments() {
        List<Payment> pending = getPendingPayments();
        
        for (Payment p : pending) {
            // 直接字段访问!
            if (processor.fraudDetector.isHighRisk(p)) {
                processor.processManually(p);
            } else {
                processor.processPayment(p);
            }
        }
    }
}

看出问题了吗?

processor.fraudDetector 是直接字段访问,不是方法调用。代理不会拦截它。不会创建新实例。字段自然就是 null。

小额支付走控制器路径(方法调用 = 正常)。

大额支付触发了定时任务里的人工复核路径(字段访问 = NullPointerException)。

这就是为什么它不是随机的。它非常“合情合理”,只不过很隐蔽。

周三:并不简单的“修复”

第一反应:移除 @Scope("prototype"),改回单例。

问题:初级同事加它是有理由的。我们之前看到过“内存泄漏”。移除它可能把旧问题带回来。

第二个想法:保留 prototype,但修代理行为。

问题:你无法用 prototype Bean 修复代理的拦截行为。这是 Spring 的基本限制。

第三个想法:原型 Bean 不用 @Autowired,改成手动从 ApplicationContext.getBean() 获取。

@Component
public class ScheduledPaymentJob {
    
    @Autowired
    private ApplicationContext context;
    
    @Scheduled(fixedRate = 60000)
    public void processScheduledPayments() {
        List<Payment> pending = getPendingPayments();
        
        for (Payment p : pending) {
            // 每次获取一个全新实例
            PaymentProcessor processor = context.getBean(PaymentProcessor.class);
            
            if (processor.getFraudDetector().isHighRisk(p)) {
                processor.processManually(p);
            } else {
                processor.processPayment(p);
            }
        }
    }
}

问题:这太丑了。手动查找 Bean 违背了依赖注入的初衷。

第四个想法(最终有效的那个):

停止使用 prototype 范围。去修真正的内存问题。

结果发现,“内存泄漏”只是误解。初级同事看到堆内存使用上升,就以为是泄漏。其实不是,是 G1GC 下 JVM 的正常行为。

我们移除了 @Scope("prototype")。内存使用保持稳定。问题解决。

真正的教训

出了什么问题:

  1. 我们在不理解的情况下信任了注解。@Scope("prototype") 看起来无害,其实会改变 Spring 的整个对象生命周期。
  2. 我们把 PR 当作孤立改动来评审。Dave 的重构看起来没问题;Scope 的改动也看起来没问题;放到一起就是灾难。
  3. 我们的测试没覆盖到所有路径。集成测试只跑了控制器路径(它是正常的),没人测定时任务路径。
  4. 我们以为字段注入和构造器注入是等价的。不是。构造器注入 + prototype 范围 + 字段访问 = 空引用。

我们之后的改进

新的团队规则:

  • ✅ 未经明确审批,不得使用 prototype 范围。需要时必须在 wiki 里写明理由和使用方式。
  • ✅ 只用构造器注入(一个例外)。字段注入被禁用。唯一的例外是环状依赖(然后我们会尽快重构掉它)。
  • ✅ 集成测试必须覆盖所有代码路径。不只“快乐路径”。要测定时任务、边界场景。
  • ✅ 代码评审要检查交互效应。合并前要看近期有哪些 PR 动过同一服务。
  • ✅ 内存“泄漏”必须用性能分析工具证明。没有 YourKit 或 VisualVM 的证据,就不能叫“泄漏”。

不那么舒适的真相

Spring 让依赖注入看起来很有“魔法”。90% 的时候,它确实很好用。

但“魔法”也有边角。原型 Bean 与代理、构造器注入中的环状依赖、@Transactional 的内部方法调用等等。

文档都写了。这些坑也都被记录了。但没人会在写一个 @Service 类之前,把 500 页的 Spring 参考手册通读一遍。

我们都是在翻车中学习。这次我们在凌晨两点炸了生产,花了三天时间才搞清楚一个注解背后的交互效应。

Dave 的重构并没有错。prototype 范围也不是绝对错误。错的是它们的组合——只在生产、只在某些代码路径、只在两个 PR 都合并之后才显现的问题。

这就是“魔法”框架的真实代价:顺的时候很妙;不顺的时候,你需要一个 Spring 内部机制博士学位。

【YoloV12改进策略】基于流形约束超连接(mHC)增强YoloV12全局特征提取能力_独家复现_梁文峰署名论文 论文标题:流形约束超连接 (Manifold-Constrained Hyper-Connections, mHC)核心问题:针对超连接 (Hyper-Connections, HC) 扩展残差流宽度时破坏恒等映射、引发训练不稳定性和高内存开销的问题,提出一种约束性框架。目标:恢复残差连接的稳定性与可扩展性,同时保持HC的性能增益。基准模型:基于DeepSeek-V3的MoE架构,进行了3B、9B、27B模型的实验验证。论文图示mHC 是一个通用的HC扩展框架。 阅读详情

相关推荐

从贝叶斯网络到结构因果模型:因果推理在可解释AI中的实践

在机器学习和数据分析领域,理解变量间的因果关系而不仅仅是统计关联,是构建可信赖、可解释系统的关键。概率图模型,特别是贝叶斯网络,通过有向无环图和条件概率表,能够高效地编码和推理变量间的条件依赖关系,广泛应用于医疗诊断、风险评估等场景。然而,贝叶斯网络主要刻画的是“是什么”的关联,难以回答“如果…会怎样”的因果问题。为了进行因果推断,需要引入结构因果模型和do-演算,它们通过定义结构方程和模拟干预操作,能够区分混杂效应,估计处理变量对结果变量的净因果影响。这一套方法论对于可解释人工智能至关重要,它能生成反事实

weixin_42168902的博客 381

让 AI 修一个 Bug,怎样避免它顺手改坏其他功能?

这个按钮偶尔会触发两次,帮我修一下。需求只有一句。等改动出来,除了按钮处理,还可能包含公共函数调整、默认值变化,以及一批看起来更整齐的代码。如果这些改动混在一起,审查时就得重新确认:哪些是修复必需的,哪些会影响其他入口,已有数据还能不能正常读取?我现在让 AI 修问题时,会把这几件事提前说清楚。上一篇写了功能改完以后怎么验收,这次聊聊怎样控制修复范围,以及拿到代码后先看什么。

qq_26483133的博客 109

vislib_vex5:面向VEX V5的嵌入式视觉处理库

嵌入式视觉处理是机器人实时感知的核心能力,其本质是在资源受限环境下实现图像采集、色彩转换、阈值分割与目标定位的确定性流水线。原理上依赖硬件加速(如DMA2D)、定点运算优化与中断协同机制,以规避浮点开销和动态内存分配。技术价值在于提供亚毫秒级、无RTOS依赖、低延迟可预测的视觉基础能力,显著提升教育及竞赛机器人在动态场景下的响应鲁棒性。典型应用场景包括色块跟踪、目标居中控制、自动导航中的简易视觉伺服等。本文聚焦于专为VEX V5主控(STM32H743+图像协处理器)深度优化的轻量库vislib_vex5,

weixin_31185473的博客 607

fix bug

当你通过 cpolar 域名外网访问时,请求到达你本机的 Apache 时,如果 Apache 的虚拟主机配置(如。)默认匹配了本地环境,或者反向代理透传了本地 Host,它依然可能表现为本地地址。$_SERVER['REMOTE_ADDR']也永远指向127.0.0.1。

huxyc的博客 76

Hive SQL零基础到精通:100道练习题+答案,SQL能力快速提升

我将以我15年架构与开发的经验,带领大家从零基础到精通,通过精心设计的100道练习题及其详解,系统地、深度地掌握Hive SQL的核心精髓。它可以将结构化的数据文件映射为一张数据库表,并提供了一套类SQL的查询语言——HiveQL(简称HQL),允许熟悉SQL的用户直接查询存储在HDFS上的海量数据。它将Job描绘为DAG(有向无环图),允许更灵活的任务编排,避免不必要的中间落盘,可以复用容器,从而大幅降低延迟。希望你以此文为起点,在广阔的大数据世界里继续遨游,成为一名真正的数据高手!

架构师的AI之路,分享AI应用开发架构的学习与实践。 801

统信UOS/麒麟Kylin OS下利用pyenv实现Python多版本开发环境搭建指南

本文详细介绍了在统信UOS和麒麟Kylin OS国产操作系统下使用pyenv搭建Python多版本开发环境的完整指南。从系统环境准备、pyenv定制安装到Python版本编译实战,提供了针对国产CPU架构的特殊优化方案和常见问题解决方法,帮助开发者高效管理不同Python版本需求。

sea99的博客 1054

2026年Bug跟踪管理系统推荐:研发与测试协作的关键枢纽

回到最初的问题,2026年的Bug跟踪管理系统推荐,核心不是列出一串名字,而是找到一条适合自己组织的协作链路。中大型企业优先看闭环、测试深度、权限治理与部署边界;工程团队优先看代码关联和工作流效率;流程稳定的团队可以把自托管工具作为起点。选型时先定流程,再用真实Bug和真实用例做试点,并以数据复盘。能把研发与测试协作串起来的系统,才配得上“关键枢纽”这个位置。本文基于公开资料与产品文档整理,具体功能、部署方式与服务范围以官方说明和实际试用为准。更新时间:2026年11月。

zentao_pms的博客 182

Virtualbox “Kernel driver not installed (rc=-1908)”问题全面解析与修复指南

本文全面解析了Virtualbox出现'Kernel driver not installed (rc=-1908)'错误的原因及修复方法。详细介绍了如何通过重新安装virtualbox-dkms、处理Secure Boot特殊情况等步骤解决该问题,并提供了预防措施与最佳实践,帮助用户避免类似错误再次发生。

weixin_30352645的博客 531

不收 PR、让 Agent 自己修 Bug:3,900 Star 的 Rust UI 框架 Rune,为“AI 写界面“重新设计

Rune:GPU 渲染的 Rust 原生 UI 框架,headless 模式让 Agent 渲染 PNG、导出元素树、脚本驱动输入并自检——UI 框架第一次把可被 AI 观测作为架构需求。更激进的是:它不收 PR,贡献和维护两端都是 Agent。

话梅糖の优快云 Blog 603

22、光无线通信调制技术详解

本文详细介绍了光无线通信中的多种调制技术,包括开关键控(OOK)、脉冲位置调制(PPM)、数字脉冲间隔调制(DPIM)、副载波强度调制(SCM)、相移键控(PSK)、正交幅度调制(QAM)以及多载波调制(MCM)如OFDM和MSIM。文章分析了各类调制技术在带宽需求、功率效率、传输容量、传输速率和PAPR等方面的性能差异,并通过Matlab仿真展示了其在AWGN信道下的误码率表现。同时,对比了不同技术的适用场景,提出了调制选择流程,并结合室内光通信与自由空间光通信的应用案例,展望了未来调制技术向高频谱效率、

net55的博客 117

LVGL core group焦点分组管理(lv_group.c)

lv_group_t是用于焦点管理的,将需要获取焦点的对像加入到同一个分组内,再将其通过挂到指定的事件驱动中,就可以控制焦点了。而被加入的对象又可以通过焦点状态事件和事件来控制对象的焦点逻辑了。创建好的分组通过接口挂事件驱动中,这样就可以指定一个自定义的焦点分组了。;) {if(!} }友情提示:无论是接口还是都是单例的,即被设置后前一个被设置的值就失效了;此方案可以保证焦点的唯一 性,但也一定要记住要在适合的位置设置回来,以防丢失焦点。

Bigan(安)的博客 3910

React 前端工程化在秒杀活动中的性能优化与 Bug 修

措施名称 | 实施成本 | 预期收益 | 风险等级 | |----------|----------|-----------|------------| | 使用 Redux Toolkit 管理状态 | 中等 | 提高可维护性和性能 | 中等 | | 引入 Suspense / Lazy Loading | 高 | 显著提升用户体验 | 高 | | 使用 Error Boundaries 捕获异常 | 中等 | 提高系统健壮性 | 中等 |在大型互联网企业中,秒杀与抢购活动是高频的业务场景之一。

wuchangjian的专栏 210

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Bug跟踪管理系统怎么搭?从提交到关闭全流程最佳实践

搭一套Bug跟踪管理系统,真正要解决的不是把问题记下来,而是让每个问题从提交到关闭都有明确的责任人、进入条件和退出证据。,这三样决定Bug管理能不能长期跑下去。软件Bug的代价并不抽象。美国国家标准与技术研究院(NIST)2002年发布的一项委托研究测算,软件Bug每年给美国经济造成约595亿美元损失。对研发组织来说,越晚发现、越晚闭环,返工和沟通损耗越高。

zentao_pms的博客 200

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

IINA BUG 正在播放的视频不显示原始文件名

视频播放时标题异常显示为录屏软件名,源于内部元数据干扰。通过在IINA中配置Lua脚本,强制使用文件原始名称作为媒体标题,实现播放列表正确显示。脚本路径为~/.config/iina/scripts/filename-title.lua,需在高级设置中添加scripts选项指向该文件。重启IINA后验证生效,不影响原文件与元数据。适用于macOS版IINA,可随时移除脚本恢复原行为。

烨之魅儿的专栏 277

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

【Unity Bug挑战】第12期 Awaitable 二次 await 死锁之谜 —— 为什么同一个异步操作等两次就卡死?

你正在把项目从 UniTask 迁移到 Unity 6 原生的 Awaitable。你觉得这很合理——就像 Task 一样,同一个 Awaitable 等多次又能怎么样呢?栏目:Unity Bug挑战 | 期号:NO.012 | 日期:2026-10-31 | 4星 · 中高级 | Bug挑战 | Unity 6+ · 异步编程 / Awaitable / 对象池。标签:Unity、Bug挑战、Awaitable、异步编程、死锁、Unity6、对象池、游戏开发、C#

crystal_WJT的博客 205
上一篇: 聊聊Java里的那把锁:ReentrantLock到底有多强大?
程序员职业指南
博客等级 码龄4年 2139粉丝 960原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值