傳統開發中,Service 層依賴 Dao 層具體實現:
// 硬編碼強耦合
public class ItemServiceImpl implements ItemService {
private ItemDao itemDao = new ItemDao4OracleImpl(); // 死寫死
}
問題:換數據庫、換實現、單元測試 Mock 都要改代碼、重編譯。
解決:接口隔離 + 配置文件聲明映射 + 工廠反射創建 + 緩存單例 —— 這就是 IoC 容器的核心雛形。
┌─────────────┐ 讀取/解析 ┌──────────────┐
│ beans.xml │ ─────────────────> │ BeanFactory │
│ (配置映射) │ │ (單例、緩存) │
└─────────────┘ └──────┬───────┘
│ getBean(Interface.class)
▼
┌──────────────┐
│ 實現類實例 │ (反射創建、存入 Map)
└──────────────┘
設計點:
- `id` 使用接口全限定名,獲取時直接傳 `Interface.class`,類型安全。
- 分組 (`service-group`/`dao-group`) 對應抽象工廠的「產品族」,語義清晰。
- 可擴展屬性:`scope="singleton|prototype"`、`lazy-init="true"`、`init-method`、`destroy-method`。
public class BeanFactory {
private static final Logger log = LoggerFactory.getLogger(BeanFactory.class);
// 單例緩存:接口 Class -> 實例
private final Map, Object> singletonCache = new ConcurrentHashMap<>();
// 配置解析結果緩存:接口 Class -> 實現類全限定名
private final Map, String> classNameCache = new ConcurrentHashMap<>();
private final Document document;
private static final String CONFIG_FILE = "beans.xml";
private BeanFactory() {
try {
SAXReader reader = new SAXReader();
// 關鍵:開啟命名空間支持、合併相鄰文本節點
reader.setFeature("https://apache.org/xml/features/disallow-doctype-decl", true); // 防 XXE
InputStream is = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE);
if (is == null) throw new IllegalStateException("Config not found: " + CONFIG_FILE);
this.document = reader.read(is);
// 預解析所有 bean 定義,構建 classNameCache,啟動即發現配置錯誤
parseAllBeanDefinitions();
log.info("BeanFactory 初始化完成,共加載 {} 個 Bean 定義", classNameCache.size());
} catch (Exception e) {
throw new ExceptionInInitializerError("BeanFactory init failed", e);
}
}
// 單例模式 (雙重檢查鎖)
private static volatile BeanFactory instance;
public static BeanFactory getInstance() {
if (instance == null) {
synchronized (BeanFactory.class) {
if (instance == null) instance = new BeanFactory();
}
}
return instance;
}
/** 統一入口:根據接口類獲取實現實例 */
@SuppressWarnings("unchecked")
public T getBean(Class interfaceClass) {
// 1. 緩存命中
Object cached = singletonCache.get(interfaceClass);
if (cached != null) return (T) cached;
// 2. 獲取實現類名
String implClassName = classNameCache.get(interfaceClass);
if (implClassName == null) {
throw new IllegalStateException("No bean definition for: " + interfaceClass.getName());
}
// 3. 反射創建實例
Object instance;
try {
Class> implClass = Class.forName(implClassName, true, getClass().getClassLoader());
instance = implClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new BeanCreationException("Create bean failed: " + implClassName, e);
}
// 4. 存入單例緩存 (雙重檢查)
Object existing = singletonCache.putIfAbsent(interfaceClass, instance);
return (T) (existing != null ? existing : instance);
}
/** 啟動時預解析所有 bean 定義 */
private void parseAllBeanDefinitions() {
// XPath 查找所有 bean 節點
List beanNodes = document.selectNodes("//bean");
for (Node node : beanNodes) {
Element bean = (Element) node;
String id = bean.attributeValue("id");
String className = bean.attributeValue("class");
if (id == null || className == null) {
throw new IllegalStateException("Bean missing id or class: " + bean.asXML());
}
try {
Class> iface = Class.forName(id, false, getClass().getClassLoader()); // 僅驗證類存在
classNameCache.put(iface, className);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Interface class not found: " + id, e);
}
}
}
}
// 獲取所有 bean
List beans = doc.selectNodes("//bean");
// 根據 id 精確查找 (獲取 Bean 時用)
Element bean = (Element) doc.selectSingleNode("//bean[@id='" + interfaceName + "']");
String className = bean.attributeValue("class");
// 獲取無參構造器 (拋異常如果不存在)
Constructor> ctor = implClass.getDeclaredConstructor();
// 破壞私有構造器訪問 (可選,建議構造器 public 或 package-private)
ctor.setAccessible(true);
Object instance = ctor.newInstance();
使用 `ConcurrentHashMap` + `putIfAbsent` 保證單例創建的線程安全,避免雙重創建。
統一使用 `getClass().getClassLoader()` (上下文類加載器) 加載實現類,兼容 Web 容器、模塊化環境。
public class Client {
public static void main(String[] args) {
// 獲取 Service
ItemService itemService = BeanFactory.getInstance().getBean(ItemService.class);
itemService.save(new Item("Book", 99.0));
// 獲取 Dao (Service 內部也應通過工廠獲取 Dao,而非 new)
ItemDao itemDao = BeanFactory.getInstance().getBean(ItemDao.class);
List- list = itemDao.findAll();
// 同一接口多次獲取,返回同一實例 (單例)
System.out.println(itemService == BeanFactory.getInstance().getBean(ItemService.class)); // true
}
}
工廠需遞歸解析 `constructor-arg` 和 `property`,按類型/名稱匹配注入依賴 (遞歸調用 `getBean`)、基礎類型轉換、引用注入。這已接近 Spring 早期核心邏輯。
| 問題 | 風險 | 對策 |
|---|---|---|
| XXE 攻擊 | 解析惡意 XML 讀取服務器文件 | SAXReader 設置 `disallow-doctype-decl=true`、`external-general-entities=false` |
| 循環依賴 | A 注入 B,B 注入 A → 無限遞歸/死鎖 | 三級緩存 (Spring 方案) / 構造器注入強制打破 / 檢測拋異常 |
| 類加載器洩漏 | 熱部署時舊類無法卸載 | 緩存 Key 用 WeakReference / 容器管理工廠生命周期 |
| 配置變更熱加載 | 修改 XML 需重啟 | WatchService 監聽文件變更,重建 Document 和緩存 |
| 性能:頻繁反射 | getBean 熱點路徑 | 緩存 Constructor/Method 對象、使用 MethodHandle/VarHandle (JDK 9+) |
「XML 配置 + Dom4j 解析 + 反射創建 + 單例緩存」構成了輕量級 IoC 容器的核心閉環。理解這一流程,能深刻掌握 Spring Bean 生命周期、依賴注入、循環依賴解決、AOP 代理創建等高級特性的底層邏輯。實際項目中,請直接使用 Spring/CDI 成熟容器;但自寫一個 Mini-IoC 是進階 Java 開發者的必修「手藝活」,建議在 Side Project 中完整實現:支持注解掃描、生命周期回調、AOP 切面、條件裝配、啟動性能優化。