IoC 容器負責創建對象、管理依賴關係、注入依賴。DI 實現方式決定了依賴何時、如何、以何種形式進入組件。
組件實現特定接口,容器通過接口方法注入依賴。
// 容器定義的接口
public interface ServiceLocator {
void injectService(Service service);
}
// 組件實現
public class Client implements ServiceLocator {
private Service service;
public void injectService(Service service) { this.service = service; }
}
Context.lookup()) 採用。通過構造函數參數注入依賴,構造函數即依賴聲明。
public class OrderService {
private final PaymentGateway gateway;
private final InventoryService inventory;
private final String currency;
public OrderService(PaymentGateway gateway, InventoryService inventory,
@Value("${app.currency}") String currency) {
this.gateway = gateway;
this.inventory = inventory;
this.currency = currency;
}
}
| 優勢 | 說明 |
|---|---|
| 不可變性 | final 字段,構造後狀態固化,線程安全 |
| 強依賴契約 | 缺少依賴啟動失敗,快速失敗,避免運行時 NPE |
| 依賴可見 | 構造函數簽名即文檔,IDE 可直接導航 |
| 注入順序可控 | 參數順序決定初始化順序,解決循環依賴、初始化順序敏感場景 |
| 黑盒封裝 | 調用者無需關心內部依賴,符合迪米特法則 |
| 無 Setter 汙染 | 防止運行時被惡意/誤修改,單例組件尤為關鍵 |
@Lazy 或重構)。通過 setter 方法注入依賴,符合 JavaBean 規範。
public class NotificationService {
private EmailSender emailSender;
private SmsSender smsSender;
private PushSender pushSender;
private String templatePath = "default.vm"; // 可選依賴有默認值
public void setEmailSender(EmailSender emailSender) { this.emailSender = emailSender; }
public void setSmsSender(SmsSender smsSender) { this.smsSender = smsSender; }
public void setPushSender(PushSender pushSender) { this.pushSender = pushSender; }
public void setTemplatePath(String templatePath) { this.templatePath = templatePath; }
}
「構造器注入用於強制依賴,Setter 注入用於可選依賴。」 — Spring Reference Documentation
@Component
public class ReportGenerator {
// 強制依賴:構造器注入
private final DataSource dataSource;
private final ReportTemplateEngine engine;
// 可選依賴:Setter 注入 + 默認值
private OutputFormat format = OutputFormat.PDF;
private String footerText = "Confidential";
public ReportGenerator(DataSource dataSource, ReportTemplateEngine engine) {
this.dataSource = dataSource;
this.engine = engine;
}
public void setFormat(OutputFormat format) { this.format = format; }
public void setFooterText(String footerText) { this.footerText = footerText; }
}
@Component
@RequiredArgsConstructor // 生成包含 final/@NonNull 字段的構造函數
@Setter(onMethod_ = @Autowired) // 為非 final 字段生成 setter 並標記 @Autowired
public class OrderService {
private final PaymentGateway gateway; // 構造器注入
private final InventoryService inventory; // 構造器注入
@Autowired(required = false) // 可選 Setter 注入
private DiscountService discountService;
private String currency = "USD"; // 默認值,可選 Setter
}
| 方案 | 適用注入模式 | 原理 | 缺點 |
|---|---|---|---|
| @Lazy 代理 | 構造器 | 注入代理,首次調用時實例化 | 掩蓋設計問題、代理開銷 |
| Setter 注入 | Setter | 先構造再注入,打破循環 | 引入可變狀態 |
| @PostConstruct 初始化 | 構造器 + Setter | 構造時注入 Provider/Factory,初始化時獲取實例 | 代碼複雜化 |
| 重構拆分 | 任意 | 提取公共接口、引入中介者、事件總線 | 需修改架構 |
| 場景 | 推薦模式 | 理由 |
|---|---|---|
| 核心業務服務、Repository、強依賴組件 | 構造器注入 | 不可變、快速失敗、線程安全 |
| 可選功能、配置屬性、第三方庫兼容 | Setter 注入 | 默認值、無參構造、循環依賴緩解 |
| 遺留代碼、Struts/JSF/JPA 實體 | Setter 注入 | 框架強制要求 |
| 新專案綠地開發 | 構造器為主 + Setter 輔 | 最佳實踐平衡點 |
理解三種注入模式的本質差異、適用邊界、組合策略,是掌握 Spring IoC 容器、編寫高質量可維護代碼的關鍵。現代 Spring 生態默認構造器注入,配合 Lombok @RequiredArgsConstructor 實現「零樣板、強契約、高可讀」。