<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      @Autowired 的Bug讓我們白忙三天

      凌晨兩點,支付服務的告警像雪崩一樣砸來,你在控制臺和棧跟蹤間瘋狂穿梭,卻始終想不明白:Spring 的依賴注入,怎么會在生產里突然“失手”?我最近讀到一篇事故復盤,講的是兩個看似無害的改動如何在生產環境聯手把系統擊穿,分析深入、啟發很大。于是我把它完整翻譯出來,分享給大家,希望能幫你少走彎路。

      以下內容翻譯自:https://medium.com/javarevisited/the-autowired-bug-that-cost-us-3-days-7d24a1e31435

      兩個“看似無害”的 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 內部機制博士學位。

      posted @ 2025-10-23 10:36  程序猿DD  閱讀(249)  評論(3)    收藏  舉報
      主站蜘蛛池模板: 天堂亚洲免费视频| 国产一级av在线播放| 综合色在线| 亚洲激情国产一区二区三区| 日韩在线视频线观看一区| 久女女热精品视频在线观看| 国产精品办公室沙发| 一区二区三区成人| 四虎国产精品成人免费久久| 男人和女人做爽爽视频| 亚洲精中文字幕二区三区| 国产一区二区不卡在线| 久久天天躁夜夜躁狠狠 ds005.com| 97久久精品无码一区二区| 中文文字幕文字幕亚洲色| 国产日韩乱码精品一区二区| 国产精品任我爽爆在线播放6080| 无码中文字幕av免费放| 色偷偷亚洲精品一区二区| 中文字幕在线亚洲精品| 久操热在线视频免费观看| 高清自拍亚洲精品二区| 国内精品伊人久久久久777| AV秘 无码一区二| 亚洲女人天堂| 荔浦县| 亚洲国产午夜精品理论片| 99视频30精品视频在线观看| 樟树市| 免费观看全黄做爰大片| 午夜视频免费试看| 亚洲av一本二本三本| 国产一区二区三区乱码| 无码国产偷倩在线播放| 欧洲无码一区二区三区在线观看| 亚洲高潮喷水无码AV电影| 精品国产成人亚洲午夜福利 | 久久国产免费观看精品3| 亚洲一区二区三区四区三级视频| 久久夜色撩人国产综合av| 成人麻豆日韩在无码视频 |