依賴注入(Dependency Injection,DI)與面向切面程式設計(Aspect-Oriented Programming,AOP)是企業級應用開發中兩大關鍵技術。它們相輔相成,共同致力於簡化領域模型與應用分層。DI 封裝了資源發現與協調器的細節,而切面則封裝了中介軟體服務調用(如事務與安全管理)的複雜性。兩者結合不僅能產出更簡單、更易測試的物件導向應用,更能將 DI 的能力延伸至更廣泛的物件與服務,同時利用 DI 來配置切面本身。
在基於 DI 的解決方案中,代理(通常由 Spring 等輕量級容器扮演)肩負四大核心職責:
Spring 容器同時負責建立與配置核心應用物件(Bean),透過查詢 XML 定義的元模型確定依賴,並從容器管理的 Bean 倉庫中取得相依物件,最後透過 Setter 注入、建構子注入或查詢方法注入完成配置。
切面同樣受益於 DI 配置。最佳實踐是將切面實作為輕量級控制器:切面判斷何時觸發行為,實際工作委託給協作者。以遠端異常處理切面為例:
public aspect RemoteExceptionHandling {
private RemoteExceptionHandler exceptionHandler;
public void setExceptionHandler(RemoteExceptionHandler aHandler) {
this.exceptionHandler = aHandler;
}
pointcut remoteCall() : call(* *(..) throws RemoteException+);
after() throwing(RemoteException ex) : remoteCall() {
if (exceptionHandler != null)
exceptionHandler.onRemoteException(ex);
}
}
由於 AspectJ 切面由 AspectJ 執行期建立,Spring 無法直接實例化。對於單例切面,AspectJ 提供 aspectOf() 工廠方法。Spring 配置如下:
<beans>
<bean name="RemoteExceptionHandlingAspect"
class="org.aspectprogrammer.dw.RemoteExceptionHandling"
factory-method="aspectOf">
<property name="exceptionHandler">
<ref bean="RemoteExceptionHandler"/>
</property>
</bean>
<bean name="RemoteExceptionHandler"
class="org.aspectprogrammer.dw.DefaultRemoteExceptionHandler"/>
</beans>
透過 ApplicationContext 的預先實例化機制,確保切面在遠端異常拋出前完成配置。
對於 perthis、pertarget、percflow 等非單例切面,以及容器管控外建立的領域實體,Spring 無法預先實例化。需要新策略識別「物件已建立需配置」。
引入 SpringConfiguredObjectBroker 切面,實作 ApplicationContextAware,攔截標註 @SpringConfigured 之物件的初始化連接點,請求 Spring 套用屬性值:
public aspect SpringConfiguredObjectBroker implements ApplicationContextAware {
private ConfigurableListableBeanFactory beanFactory;
public void setApplicationContext(ApplicationContext aContext) { ... }
pointcut springConfiguredObjectCreation(Object newInstance, SpringConfigured scAnnotation) :
initialization((@SpringConfigured *).new(..)) && this(newInstance) && @this(scAnnotation);
after(Object newInstance, SpringConfigured scAnn) returning : springConfiguredObjectCreation(newInstance, scAnn) {
String beanName = getBeanName(newInstance, scAnn);
beanFactory.applyBeanPropertyValues(newInstance, beanName);
}
private String getBeanName(Object obj, SpringConfigured ann) { ... }
}
使用時僅需標註類別:@SpringConfigured("AccountBean") public class Account { ... }。此技術適用於任何容器管控外建立的物件,包括各類生命週期的切面。
透過合約介面宣告需求,實作 AccountOperationValidationClient 介面的物件自動獲得驗證服務。切面 AccountOperationValidationServiceInjector 持有服務參考,於客戶端建立時注入:
public aspect AccountOperationValidationServiceInjector {
private AccountOperationValidationService service;
public void setService(AccountOperationValidationService aService) { this.service = aService; }
pointcut clientCreation(AccountOperationValidationClient aClient) :
initialization(AccountOperationValidationClient+.new(..)) && this(aClient);
after(AccountOperationValidationClient aClient) returning : clientCreation(aClient) {
aClient.setAccountOperationValidationService(this.service);
}
}
雙層控制:服務定義於 Spring XML,客戶端僅需實作介面即可自動配置。
當客戶端需在執行期動態切換策略(如報價策略),可採用欄位層級注入。切面攔截欄位 get 連接點,每次存取皆回傳最新策略實例:
public aspect PricingStrategyInjector {
private PricingStrategy currentPricingStrategy;
public void setCurrentPricingStrategy(PricingStrategy aStrategy) { this.currentPricingStrategy = aStrategy; }
pointcut pricingStrategyAccess() : get(PricingStrategy *) && !within(PricingStrategyInjector);
PricingStrategy around() : pricingStrategyAccess() { return this.currentPricingStrategy; }
}
替代方案為服務定位器介面,犧牲些許程式碼簡潔度換取清晰度。
本文將 DI 視為物件與執行環境間的契約,詳述四大代理職責與多種滿足技術。優先讓 Spring 容器管理物件生命週期;對於容器外物件,@SpringConfigured 註解提供優雅解決方案,將配置資訊完全外部化。完整範例代碼可至 IBM developerWorks 下載。