1. 程式人生 > >JavaWeb 筆記之 Spring AOP

JavaWeb 筆記之 Spring AOP

Spring AOP

  • AspectJ:Java 社群裡最完整最流行的 AOP 框架
  • 在 Spring2.0 以上版本中, 可以使用基於 AspectJ 註解或基於 XML 配置的 AOP

基於 AspectJ 註解的 AOP

啟用 AspectJ 註解支援

  1. 引入 Jar 包(aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar、spring-aop.jar)
  2. 將 aop Schema 新增到 <beans> 根元素中
  3. 要在 Spring IOC 容器中啟用 AspectJ 註解支援, 只要在 Bean 配置檔案中定義一個空的 XML 元素 <aop:aspectj-autoproxy>
  4. 當 Spring IOC 容器偵測到 Bean 配置檔案中的 <aop:aspectj-autoproxy> 元素時, 會自動為與 AspectJ 切面匹配的 Bean 建立代理.

用 AspectJ 註解宣告切面

  1. 要在 Spring 中宣告 AspectJ 切面, 只需要在 IOC 容器中將切面宣告為 Bean 例項. 當在 Spring IOC 容器中初始化 AspectJ 切面之後, Spring IOC 容器就會為那些與 AspectJ 切面相匹配的 Bean 建立代理.
  2. 在 AspectJ 註解中, 切面只是一個帶有 @Aspect 註解的 Java 類.
  3. 通知是標註有某種註解的簡單的 Java 方法.
  4. AspectJ 支援 5 種類型的通知註解:
  • @Before: 前置通知, 在方法執行之前執行
  • @After: 後置通知, 在方法執行之後執行
  • @AfterRunning: 返回通知, 在方法返回結果之後執行
  • @AfterThrowing: 異常通知, 在方法丟擲異常之後
  • @Around: 環繞通知, 圍繞著方法執行

1. 前置通知

  • 前置通知:在方法執行之前執行的通知
  • 前置通知使用 @Before 註解, 並將切入點表示式的值作為註解值.
 @Aspect
 @Component
 public class LoggingAspect {
	
	 @Before("execution(public int com.axon.spring5.aop.impl.ArithmeticCalculator.add(int, int))")
	 public void beforeMethod(JoinPoint joinPoint) {
	 String methodName = joinPoint.getSignature().getName();
	 List<Object> args = (List<Object>) Arrays.asList(joinPoint.getArgs());
	 System.out.println("The method " + methodName + "begins with "+args);
 }
}

  • 編寫 AspectJ 切入點表示式
  • execution * com.axon.spring.ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中宣告的所有方法,第一個 * 代表任意修飾符及任意返回值. 第二個 * 代表任意方法. .. 匹配任意數量的引數. 若目標類與介面與該切面在同一個包中, 可以省略包名.
  • execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 介面的 所有公有方法.
  • execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中 返回 double 型別數值的方法
  • execution public double ArithmeticCalculator.*(double, ..): 匹配第一個引數為 double 型別的方法, .. 匹配任意數量任意型別的引數
  • execution public double ArithmeticCalculator.*(doubledouble): 匹配引數型別為 double, double 型別的方法.
  • 在 AspectJ 中, 切入點表示式可以通過操作符 &&, ||, ! 結合起來
@Aspect
@Component
public class LoggingAspect {
	
	@Pointcut("execution(* *.add(int,..)) || execution(* *.sub(int,..))")
	private void loggingOperation(){}
	
	@Before("loggingOperation()")
	public void beforeMethod(JoinPoint joinPoint) {
	String methodName = joinPoint.getSignature().getName();
 	List<Object> args = (List<Object>) Arrays.asList(joinPoint.getArgs());
		System.out.println("The method " + methodName + "begins with "+args);
	}
}

2. 後置通知

  • 後置通知是在連線點完成之後執行的, 即連線點返回結果或者丟擲異常的時候, 下面的後置通知記錄了方法的終止.
  • 一個切面可以包括一個或者多個通知.
  • 後置通知還不能訪問目標方法執行的結果
	 @After("execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))")
	 public void afterMethod(JoinPoint joinPoint) {
		 String methodName = joinPoint.getSignature().getName();
		 System.out.println("The method " + methodName + " ends");
	 }

3. 返回通知

  • 無論連線點是正常返回還是丟擲異常, 後置通知都會執行. 如果只想在連線點返回的時候記錄日誌, 應使用返回通知代替後置通知.
  • 可以訪問方法的返回值
	@AfterReturning(pointcut="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))",returning="result")
	public void afterReturning(JoinPoint joinPoint,Object result) {
		String methodName = joinPoint.getSignature().getName();
		System.out.println("The method " + methodName + " ends with " + result);
	}

4. 異常通知

  • 只在連線點丟擲異常時才執行異常通知
  • 將 throwing 屬性新增到 @AfterThrowing 註解中, 也可以訪問連線點丟擲的異常. Throwable 是所有錯誤和異常類的超類. 所以在異常通知方法可以捕獲到任何錯誤和異常.
  • 如果只對某種特殊的異常型別感興趣, 可以將引數宣告為其他異常的引數型別. 然後通知就只在丟擲這個型別及其子類的異常時才被執行.
    @AfterThrowing(value="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))",throwing="ex")
	public void afterThrowing(JoinPoint joinPoint,Exception ex) {
		String methodName = joinPoint.getSignature().getName();
		System.out.println("The method " + methodName + " occurs exception: " + ex);
	}

5. 環繞通知

  • 環繞通知是所有通知型別中功能最為強大的,環繞通知類似於動態代理的全過程
  • ProceedingJoinPoint . 它是 JoinPoint 的子介面, 允許控制何時執行, 是否執行連線點.
  • 注意: 環繞通知的方法需要返回目標方法執行之後的結果, 即呼叫 joinPoint.proceed(); 的返回值, 否則會出現空指標異常
    @Around("execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))")
	public Object aroundMethod(ProceedingJoinPoint pjd) throws Throwable {
		Object result = null;
		String methodName = pjd.getSignature().getName();
		//執行目標方法
		try{
			//前置通知
			System.out.println("The method " + methodName + " begins with "+ Arrays.asList(pjd.getArgs()));
			result = pjd.proceed();
			//返回通知
			System.out.println("The method " + methodName + " ends with " + result);
		} catch (Throwable e){
			//異常通知
			System.out.println("The method " + methodName + " occurs exception: " + e);
			throw new RuntimeException(e);
		}
		//後置通知
		System.out.println("The method " + methodName + " ends ");
		return result;
	}

指定切面的優先順序

  • 同一個連線點上應用不止一個切面時, 除非明確指定, 否則它們的優先順序是不確定的
  • 切面的優先順序可以通過實現 Ordered 介面或利用 @Order 註解指定.
  • 實現 Ordered 介面, getOrder() 方法的返回值越小, 優先順序越高.
  • 若使用 @Order 註解, 序號出現在註解中
@Aspect
@Order(1)
@Component
public class LoggingAspect {
}

@Aspect
@Order(2)
@Component
public class ValidationAspect 

基於 XML 的配置的 AOP

  • 除了使用 AspectJ 註解宣告切面, Spring 也支援在 Bean 配置檔案中宣告切面. 這種宣告是通過 aop schema 中的 XML 元素完成的.
  • 正常情況下, 基於註解的宣告要優先於基於 XML 的宣告. 通過 AspectJ 註解, 切面可以與 AspectJ 相容, 而基於 XML 的配置則是 Spring 專有的. 由於 AspectJ 得到越來越多的 AOP 框架支援, 所以以註解風格編寫的切面將會有更多重用的機會.

宣告切面步驟

  • 首先 需要在 <beans> 根元素中匯入 aop Schema
  • 在 Bean 配置檔案中, 所有的 Spring AOP 配置都必須定義在 <aop:config> 元素內部. 對於每個切面而言, 都要建立一個 <aop:aspect> 元素來為具體的切面實現引用後端 Bean 例項.
  • 切面 Bean 必須有一個標示符, 供 <aop:aspect> 元素引用
  • 切入點使用 <aop:pointcut> 元素宣告
  • 切入點必須定義在 <aop:aspect> 元素下, 或者直接定義在 <aop:config> 元素下.
  • 定義在 <aop:aspect> 元素下: 只對當前切面有效
<!-- 配置 aop  -->
	<aop:config>
		<!-- 配置切面以及通知 -->
		<aop:aspect  ref="xxxx" order="1">
			<aop:around method="aroundMethod"  pointcut="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" />
		</aop:aspect>
	</aop:config>
  • 定義在 <aop:config> 元素下: 對所有切面都有效
<aop:config>
		<!-- 配置切入點表示式 -->
		<aop:pointcut expression="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" id="pointcut" />
        <aop:aspect  ref="xxxx" order="1">
			<aop:before method="beforeMethod"  pointcut-ref="pointcut" />
			<aop:after method="afterMethod"  pointcut-ref="pointcut" />
			<aop:after-returning method="afterReturning"  pointcut-ref="pointcut" returning="result"/>
			<aop:after-throwing method="afterThrowing"  pointcut-ref="pointcut" throwing="ex"/>
		</aop:aspect>
		<aop:aspect  ref="xxxx" order="0">
			<aop:before method="validateArgs" pointcut-ref="pointcut"/>
		</aop:aspect>
</aop:config>
  • 基於 XML 的 AOP 配置不允許在切入點表示式中用名稱引用其他切入點.
  • 詳細示例:
    <!-- 配置切面的 bean -->
	<bean id="loggingAspect" class="com.axon.spring5.aop.impl.LoggingAspect"></bean>
	<bean id="validationAspect" class="com.axon.spring5.aop.impl.ValidationAspect"></bean>
	<!-- 配置 aop  -->
	<aop:config>
		<!-- 配置切入點表示式 -->
		<aop:pointcut expression="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" id="pointcut" />
		<!-- 配置切面以及通知 -->
		<aop:aspect  ref="loggingAspect" order="1">
			<aop:before method="beforeMethod"  pointcut-ref="pointcut" />
			<aop:after method="afterMethod"  pointcut-ref="pointcut" />
			<aop:after-returning method="afterReturning"  pointcut-ref="pointcut" returning="result"/>
			<aop:after-throwing method="afterThrowing"  pointcut-ref="pointcut" throwing="ex"/>
			<!-- <aop:around method="aroundMethod"  pointcut-ref="pointcut" /> -->
			<!-- <aop:around method="aroundMethod"  pointcut="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" /> -->
		</aop:aspect>
		<aop:aspect  ref="validationAspect" order="0">
			<aop:before method="validateArgs" pointcut-ref="pointcut"/>
		</aop:aspect>
	</aop:config>