首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

CGLib 動態代理導致 final 方法空指針異常問題分析

     編輯:Java 相關知識

Spring AOP 預設使用 CGLib 代理類(非介面情況)。CGLib 透過繼承目標類並覆寫非 final 方法實現代理。若業務方法被標記 final,代理類無法覆寫,導致呼叫未被攔截、依賴注入的欄位(如 logger)為 null。

問題場景重現


public abstract class BaseServlet extends HttpServlet {
    protected Log logger;

    @Override
    public final void doGet(HttpServletRequest req, HttpServletResponse res)  // ❌ final
            throws IOException {
        response(req, res);
        logger.debug("@@@@@@doGet");  // NPE:logger 為 null
    }

    public void setLogger(Log logger) { this.logger = logger; }
    // ...
}

CGLib 代理原理模擬


// 目標類
class A {
    protected String name = null;
    public final void finalMethod() { System.out.println(name); }
    public void notFinalMethod() { System.out.println(name); }
    public void setName(String name) { this.name = name; }
}

// CGLib 生成的代理類(簡化)
class B extends A {
    private A target = new A();  // 內部持有目標實例

    @Override
    public void notFinalMethod() { target.notFinalMethod(); }  // 覆寫並委派

    @Override
    public void setName(String name) { target.setName(name); } // 覆寫並委派

    // finalMethod 無法覆寫!繼承自 A,操作 A 的 name(永遠為 null)
}

記憶體模型解析

  1. Spring 建立代理實例 B,注入 logger 到 target (A 實例)
  2. 外部呼叫 proxy.doGet()
  3. doGet 為 final,代理類 B 無法覆寫 → 直接執行 A.doGet()
  4. A.doGet() 存取 this.logger → A 實例的 logger 為 null(注入給了 target)
  5. 拋出 NullPointerException

解決方案

  1. 移除 final 修飾符(推薦):允許 CGLib 覆寫攔截
  2. 改用介面 + JDK 動態代理:proxy-target-class="false",代理介面而非類
  3. 構造器注入:確保目標實例建立時即完整初始化(但 final 方法仍不會被代理)
  4. @PostConstruct 初始化:避免 setter 依賴

Spring 配置建議




    




    

核心原則

  • ❌ 參與 AOP 代理的類/方法 不可標記 final
  • ✅ 盡量對介面編程,啟用 JDK 動態代理
  • ✅ 單元測試覆蓋代理行為驗證
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved