在非 Spring 管理的類中(工具類、第三方庫回調、舊代碼遷移)獲取 Spring Bean 是常見需求。本文系統梳理 5 種方式,對比優缺點,推薦 實現 ApplicationContextAware + 靜態工具類 作為通用最佳實踐,並解析 WebApplicationContextUtils、ContextLoader 陷阱。
| 方式 | 核心原理 | 適用場景 | 優點 | 缺點 | 推薦指數 |
|---|---|---|---|---|---|
| 1. 實現 ApplicationContextAware (推薦) | Spring 初始化時回調 setApplicationContext 注入上下文 |
通用工具類、靜態方法獲取 Bean | 非侵入、靜態訪問、生命周期受 Spring 管理、支持多上下文隔離 | 需在 XML/配置類註冊 Bean | ⭐⭐⭐⭐⭐ |
| 2. WebApplicationContextUtils | 從 ServletContext 屬性獲取 WebApplicationContext | Web 環境、有 ServletContext 時 (Filter、Servlet、Listener) | 無需額外 Bean 註冊、標準 API | 依賴 Web 環境、需傳 ServletContext、非靜態友好 | ⭐⭐⭐⭐ |
| 3. 繼承 ApplicationObjectSupport | 父類提供 getApplicationContext() |
自定義 Spring 擴展類 (如自定義 Repository 基類) | 代碼復用、IDE 友好 | 強制繼承、單繼承限制、仍需註冊 Bean | ⭐⭐⭐ |
| 4. 繼承 WebApplicationObjectSupport | 父類提供 getWebApplicationContext() |
Web 環境下的 Spring 擴展類 | 同 3,專為 Web 設計 | 同 3,更窄 | ⭐⭐ |
| 5. 手動 new ClassPathXmlApplicationContext | 獨立構建容器 | 獨立應用、測試、批處理、無 Web 容器 | 完全獨立、無依賴 | 重複創建容器、資源浪費、Bean 不共享、配置文件耦合 | ⭐ (慎用) |
package com.example.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@Component // 關鍵:註冊為 Spring Bean,觸發 setApplicationContext
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NonNull ApplicationContext ctx) throws BeansException {
applicationContext = ctx;
}
// 靜態獲取 Bean
public static T getBean(String name) {
checkContext();
return (T) applicationContext.getBean(name);
}
public static T getBean(Class clazz) {
checkContext();
return applicationContext.getBean(clazz);
}
public static T getBean(String name, Class clazz) {
checkContext();
return applicationContext.getBean(name, clazz);
}
// 檢查上下文是否就緒
private static void checkContext() {
if (applicationContext == null) {
throw new IllegalStateException("SpringContextHolder 未初始化,請確認:\n" +
"1. 已掃描到 @Component 包路徑\n" +
"2. 未在靜態塊/構造函數中調用 getBean()\n" +
"3. 非 Web 環境需手動調用 refresh()");
}
}
// 可選:獲取環境屬性
public static String getProperty(String key) {
return getBean(org.springframework.core.env.Environment.class).getProperty(key);
}
}
// 任意非 Spring 管理類中
UserService userService = SpringContextHolder.getBean(UserService.class);
String url = SpringContextHolder.getProperty("app.url");
<context:component-scan> 或 @ComponentScan 覆蓋。ThreadLocal 或 key 前綴區分。getBean(String, Class) 比單純 getBean(String) 更安全,避免強轉異常。
// 在 Filter、Servlet、Listener 中有 ServletContext 時
WebApplicationContext ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(servletContext); // 找不到拋異常
// 或
WebApplicationContext ctx = WebApplicationContextUtils
.getWebApplicationContext(servletContext); // 找不到返回 null
UserService service = ctx.getBean(UserService.class);
優勢:無需額外 Bean 註冊,標準 SPI。
劣勢:耦合 Servlet API,非 Web 環境 (測試、批處理) 不可用。
// 嚴禁在容器啟動階段調用!
WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
wac.getBean("xxx"); // 啟動時 wac 為 null,拋 NPE
原因:ContextLoaderListener.contextInitialized() 完成前,ContextLoader.currentContext 為 null。若在 Bean 的構造函數、@PostConstruct、靜態塊中調用,必現 NPE。
需要在非 Spring 類獲取 Bean?
├─ 是 Web 環境且有 ServletContext (Filter/Servlet)?
│ ├─ 是 → WebApplicationContextUtils (零配置)
│ └─ 否 → 通用場景
│ └─ 實現 ApplicationContextAware + 靜態工具類 SpringContextHolder (推薦標準化)
├─ 獨立應用/測試/批處理?
│ └─ new ClassPathXmlApplicationContext (僅此場景)
└─ 正在開發 Spring 擴展基類?
└─ 繼承 ApplicationObjectSupport (框架開發)
獲取 Spring Bean 的本質是 「如何拿到 ApplicationContext 引用」。SpringContextHolder 以 ApplicationContextAware 為橋樑,將上下文「推」給靜態持有者,實現了「非 Spring 類零成本訪問 Spring Bean」的工程化平衡。記住三條鐵律:註冊 Bean、靜態持有、方法內調用,拒絕手動構建容器、拒絕啟動期調用 ContextLoader,你的代碼將既優雅又健壯。