愛上 Spring 2.0 的另一個理由:攔截器組合 (Interceptor Combining)

工程 (Engineering) | Ben Hale | 2006年4月9日 | ...

最近我正在開發一個專案,該專案有一個 Swing 用戶端透過 RMI 與服務層進行通訊。 服務層標記了交易,一切似乎都運作良好。 然而,每次在 Hibernate DAO 層發生異常時,Spring 都會將異常轉換為 runtime exception,並且會將其以 RemoteException 的形式一路傳播到堆疊頂端並透過 RMI 連線。 每當 exception 反序列化時,用戶端都會出現異常(與 RemoteException 分開)。 我們決定簡單地引入一個 aspect。 任何繼承 ServiceAccessException 的 exception 都會傳遞給用戶端,而其他任何 exception 都會轉換為 FilteredServiceAccessExceptionServiceAccessException 的子類別)然後拋出。 這導致內容遺失,因此我們確保在伺服器上記錄原始 exception,以便其有用,並讓用戶端顯示一個通用對話框,以便使用者大致了解發生了什麼事。

現在這是一個相當不錯的計畫,並且似乎可以順利運作,直到我們嘗試實作它。 我們使用神奇的方式自動代理 (autoproxying) 任何具有 @Transactional 的 bean,以取得我們的交易代理。 我們可以更新該自動代理的定義,以確保新增此 exception 篩選的 advice (例如 setPreInterceptorsTransactionProxyFactoryBean 中),但自動代理捕獲的不僅僅是服務層。

那麼我們該怎麼辦? 我們可以 A) 明確宣告每次使用 TransactionProxyFactoryBean、B) 建立兩組不同的自動代理,並使其彼此互斥,或 C) 現在忽略需求並希望發生一些神奇的事情。 由於該產品距離消費者還有六個月的時間,並且我嘗試遵循 Jeremy Miller 介紹給我的「最後負責的時刻 (last responsible moment)」原則,所以我決定暫緩此問題,並將選擇 A 作為我的備用計畫(最好沒有魔法,而不是兩倍的魔法)。

瞧,Spring 2.0 解決了我的問題。 我實在找不到我在哪裡讀到的,但從 2.0 的一個里程碑開始,當一個 bean 被代理時,代理工廠現在可以檢測到該 bean 已經有一個代理,並且只需將預期的攔截器新增為另一個攔截器(如果您知道在哪裡,請將連結留在評論中)。 這意味著我可以只使用新的魔法 (tx:annotation-driven) 並簡單地新增一個具有我想要的正確切入點的 aspect,而不必擔心交易代理和 AOP 代理混淆。 不太確定這是怎麼回事嗎? 舉個例子吧。 首先是一個介面和實作。


package interceptorcombiningexample;

import org.springframework.transaction.annotation.Transactional;

@Transactional
public interface ExampleTarget {

	void exampleMethod();

}

package interceptorcombiningexample;

public class DefaultExampleTarget implements ExampleTarget {

	public void exampleMethod() {
	}
}

請注意,該介面標記為 @Transactional。 我們將使用它來稍後取得一些神奇的自動代理。


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

	<tx:annotation-driven />

	<aop:config>
		<aop:aspect id="exampleAspect" ref="exampleAdvice">
			<aop:before method="exampleAdvice"
				pointcut="execution(* interceptorcombiningexample.ExampleTarget.exampleMethod())" />
		</aop:aspect>
	</aop:config>

	<bean id="exampleAdvice"
		class="interceptorcombiningexample.ExampleAdvice" />

	<bean id="exampleTarget"
		class="interceptorcombiningexample.DefaultExampleTarget" />

	<bean id="transactionManager"
		class="interceptorcombiningexample.DummyTransactionManager" />

</beans>

接下來我們將看看 bean 定義。


package interceptorcombiningexample;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class InterceptorCombiningExample {

	public static void main(String[] args) {
		ApplicationContext ctx = new ClassPathXmlApplicationContext(
				"classpath:interceptorcombiningexample/applicationContext.xml");

		ExampleTarget target = (ExampleTarget) ctx.getBean("exampleTarget");
		if (target instanceof Advised) {
			Advised advised = (Advised) target;
			System.out
					.println("Advisor count: " + advised.getAdvisors().length);
			for (Advisor advisor : advised.getAdvisors()) {
				System.out.println("Advisor type: "
						+ advisor.getAdvice().getClass().getName());
			}
		}
	}

}

您會注意到我們設定了註解驅動的交易,它會自動在我們的 DefaultExampleTarget 周圍建立一個代理。 此外,我們定義了另一個 aspect,它應該需要代理相同的 DefaultExampleTarget bean。 最後,讓我們看一下我們的可執行類別。

Advisor count: 3
Advisor type: org.springframework.aop.interceptor.ExposeInvocationInterceptor
Advisor type: org.springframework.transaction.interceptor.TransactionInterceptor
Advisor type: org.springframework.aop.aspectj.AspectJMethodBeforeAdvice

此類別利用了 Spring 代理機制的一個很好的小功能。 任何 Spring 建立的代理都可以轉換為 Advised 介面。 此介面將讓您存取代理中的所有攔截器。 當我們繼續執行此類別時,輸出顯示

由此我們可以知道,代理中不僅包含 TransactionInterceptor,還包含 AspectJMethodBeforeAdvice

重要的是要知道這不應影響任何已存在的嘗試執行相同操作的實作。 這應該只是讓所有一直在等待「最後負責的時刻」來解決此問題的人們的生活更輕鬆。 :)

附註:與上一篇文章一樣,我包含了一個來自此範例的 專案封存檔,以便您可以查看其餘程式碼(如果您需要)。

取得 Spring 電子報

隨時關注 Spring 電子報