本系列教程共六部分,此為第四篇,重點介紹 Spring AOP(面向切面編程)模組如何簡化切面導向開發,以及 Spring Web Flow (SWF) 如何以 XML 宣告式方式定義 Web 應用流程。您將看到如何利用 Spring AOP 實現聲明式事務管理,並體驗 Apache Geronimo 管理主控台讓資料庫建立與管理更輕鬆——全程無需重啟伺服器。
本教程適合想深入了解 Spring 框架、並掌握在 Apache Geronimo 應用伺服器上發揮其威力的 Java EE 開發人員。我們將繼續打造個人電話簿應用程式,功能涵蓋:顯示電話簿、查看條目詳情、新增/編輯/刪除條目、擴充欄位(如主 Email)等。
AOP 將橫切關注點(如日誌、事務、安全)從業務邏輯中分離。核心術語包括:
以電話簿應用為例,我們定義「方法標記」切面來追蹤方法呼叫流程:
實作 MethodBeforeAdvice 介面,在方法執行前輸出追蹤訊息:
public class MarkingBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object target) throws Throwable {
Calendar cal = Calendar.getInstance();
System.out.println(cal.getTime() + " :::: PREPARING TO CALL " + method.getName()
+ " of " + target.getClass().getName());
}
}
實作 AfterReturningAdvice,方法正常返回後執行:
public class MarkingAfterAdvice implements AfterReturningAdvice {
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
Calendar cal = Calendar.getInstance();
System.out.println(cal.getTime() + " :::: FINISHED CALLING " + method.getName());
}
}
最強大也最常用,實作 MethodInterceptor,可完全控制執行流程:
public class MarkingInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Calendar cal = Calendar.getInstance();
System.out.println(cal.getTime() + " :::: NOW INVOKING " + invocation.getMethod().getName());
Object retVal = invocation.proceed(); // 必須顯式呼叫才會執行目標方法
cal = Calendar.getInstance();
System.out.println(cal.getTime() + " :::: FINISHED INVOKING " + invocation.getMethod().getName());
return retVal;
}
}
在 ApplicationContext.xml 中定義 Bean 與代理:
<!-- 定義通知 Bean -->
<bean id="beforeTracer" class="phonebook.aop.MarkingBeforeAdvice"/>
<!-- 定義切入點與通知的關聯 -->
<bean id="beforeMarkingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice"><ref local="beforeTracer"/></property>
<property name="pattern"><value>.*</value></property> <!-- 套用至所有方法 -->
</bean>
<!-- ProxyFactoryBean 整合 -->
<bean id="phonebook" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces"><value>phonebook.dao.IPhonebookDataProvider</value></property>
<property name="target"><ref local="phonebookJdbcDataProvider"/></property>
<property name="interceptorNames">
<list><value>beforeMarkingAdvisor</value></list>
</property>
</bean>
Spring 利用 AOP 內部實現事務,開發者只需在 XML 宣告:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="pbJdbcDataSource"/>
</bean>
<bean id="phonebookTxn" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="txManager"/>
<property name="proxyInterfaces"><value>phonebook.dao.IPhonebookDataProvider</value></property>
<property name="target" ref="phonebookJdbcDataProvider"/>
<property name="transactionAttributes">
<props>
<prop key="add*">PROPAGATION_REQUIRED,-Exception</prop>
</props>
</property>
</bean>
PROPAGATION_REQUIRED 表示支援現有事務或建立新事務;-Exception 表示遇到例外回滾。測試時嘗試新增條目時留空必填欄位,即可觀察事務自動回滾。
隨著應用複雜度增加,頁面導航邏輯分散在各處難以維護。SWF 以有限狀態機(FSM)建模流程,在 XML 中定義狀態與轉換,支援跨環境重用(Struts、JSF、Portlet 等)。
<flow start-state="showPhonebook">
<view-state id="showPhonebook" view="home-dao">
<transition on="goToAddEntry" to="addEntry"/>
</view-state>
<view-state id="addEntry" view="addentry">
<transition on="goToHome" to="showPhonebook"/>
<transition on="saveEntry" to="addEntry"/>
</view-state>
</flow>
<bean id="flowRegistry" class="org.springframework.webflow.registry.XmlFlowRegistryFactoryBean">
<property name="flowLocations" value="/WEB-INF/phonebookFlow.xml"/>
</bean>
<bean id="repositoryFactory" class="org.springframework.webflow.execution.repository.continuation.ContinuationFlowExecutionRepositoryFactory">
<constructor-arg ref="flowRegistry"/>
</bean>
<bean id="flowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl">
<constructor-arg ref="repositoryFactory"/>
</bean>
<bean name="phonebookFlowController" class="org.springframework.webflow.executor.mvc.FlowController">
<property name="flowExecutor" ref="flowExecutor"/>
<property name="cacheSeconds" value="5"/>
</bean>
關鍵在於隱藏欄位 _flowExecutionKey 與 _eventId:
<input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"/>
<input type="hidden" name="_eventId" value=""/>
<script>
function doSave() {
document.myForm._eventId.value = "saveEntry";
document.myForm.submit();
}
function goHome() {
document.myForm._eventId.value = "goToHome";
document.myForm.submit();
}
</script>
<WORKSPACE>/phonebook/lib 包含所有必要 JAR(參考 readme.txt)https://localhost:8080/phonebook/home-aop.jsp 測試 AOP 追蹤https://localhost:8080/phonebook/phonebook.htm?_flowId=phonebookFlow 體驗 SWF 流程
| 技術 | 核心優勢 |
|---|---|
| 聲明式事務 | 不依賴 EJB 容器、可套用至 POJO、簡單的回滾規則、可疊加自訂 AOP |
| Spring Web Flow | 流程定義外部化、可重用、生命週期清晰受管、解決 MVC 中 Controller 複雜度 |
第五部分將深入 Spring MVC,探討各類 Controller 與 Handler,並擴充電話簿的修改、刪除功能。敬請期待!