Spring 提供了很多的實現AOP的方式:Spring 介面方式,schema配置方式和註解.
本文重點介紹Spring使用介面方式實現AOP. 研究使用介面方式實現AOP, 以瞭解為目的. 更好地理解spring使用動態代理實現AOP. 通常我們使用的更多的是使用註解的方式實現AOP
下面來看看如何實現介面方式的AOP
一. 環境準備
要在專案中使用Spring AOP 則需要在專案中匯入除了spring jar包之外, 還需要引入aspectjrt.jar,aspectjweaver.jar,aopalliance.jar ,spring-aop-3.2.0.M2.jar和cglib.jar
二、Spring介面方式實現AOP步驟
使用Spring aop介面方式實現aop, 可以通過自定義通知來供Spring AOP識別. 常見的自己定義通知有:前置通知, 後置通知, 返回通知, 異常通知, 環繞通知. 對應實現的介面是:
- 前置通知: MethodBeforeAdvice
- 後置通知: AfterAdvice
- 返回通知:AfterReturningAdvice
- 異常通知:ThrowsAdvice
- 環繞通知:MethodInterceptor
實現步驟如下:
1. 業務介面實現
package com.lxl.www.aop.interfaceAop; /** * 使用介面方式實現AOP, 預設通過JDK的動態代理來實現. 非介面方式, 使用的是cglib實現動態代理 * * 業務介面類-- 計算器介面類 * * 定義三個業務邏輯方法 */ public interface IBaseCalculate { int add(int numA, int numB); int sub(int numA, int numB); int div(int numA, int numB); int multi(int numA, int numB); int mod(int numA, int numB); }
2. 業務類
package com.lxl.www.aop.interfaceAop;//業務類,也是目標物件 import com.lxl.www.aop.Calculate; import org.springframework.aop.framework.AopContext; import org.springframework.stereotype.Service; /** * 業務實現類 -- 基礎計算器 */ @Service public class BaseCalculate implements IBaseCalculate { @Override public int add(int numA, int numB) { System.out.println("執行目標方法: add"); return numA + numB; } @Override public int sub(int numA, int numB) { System.out.println("執行目標方法: sub"); return numA - numB; } @Override public int multi(int numA, int numB) { System.out.println("執行目標方法: multi"); return numA * numB; } @Override public int div(int numA, int numB) { System.out.println("執行目標方法: div"); return numA / numB; } @Override public int mod(int numA, int numB) { System.out.println("執行目標方法: mod"); int retVal = ((Calculate) AopContext.currentProxy()).add(numA, numB); return retVal % numA; } }
3. 通知類
前置通知
package com.lxl.www.aop.interfaceAop; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 定義前置通知 */ @Component public class BaseBeforeAdvice implements MethodBeforeAdvice { /** * * @param method 切入的方法 * @param args 切入方法的引數 * @param target 目標物件 * @throws Throwable */ @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("===========進入beforeAdvice()============"); System.out.println("目標物件:" + target); System.out.println("方法名: "+method); System.out.println("即將進入切入點方法"); } }
後置通知
package com.lxl.www.aop.interfaceAop; import org.aspectj.lang.annotation.AfterReturning; import org.springframework.aop.AfterAdvice; import org.springframework.aop.AfterReturningAdvice; import org.springframework.stereotype.Component; import java.lang.reflect.Method; public class BaseAfterReturnAdvice implements AfterReturningAdvice { /** * * @param returnValue 切入點執行完方法的返回值,但不能修改 * @param method 切入點方法 * @param args 切入點方法的引數陣列 * @param target 目標物件 * @throws Throwable */ @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("==========進入afterReturning()=========== \n"); System.out.println("切入點方法執行完成"); System.out.println("後置通知--目標物件:" + target); System.out.println("後置通知--方法名: "+method); System.out.println("後置通知--方法入參: "+ args.toString()); System.out.println("後置通知--方法返回值: "+ returnValue); } }
異常通知
package com.lxl.www.aop.interfaceAop; import org.springframework.aop.ThrowsAdvice; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Component public class BaseAfterThrowsAdvice implements ThrowsAdvice { /** * @param method 可選:切入的方法 * @param args 可選:切入的方法的引數 * @param target 可選:目標物件 * @param throwable 必填 : 異常子類,出現這個異常類的子類,則會進入這個通知。 */ public void afterThrowing(Method method, Object[] args, Object target, Throwable throwable) { System.out.println("出錯啦"); } }
環繞通知
package com.lxl.www.aop.interfaceAop; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 環繞通知 */ @Component public class BaseAroundAdvice implements MethodInterceptor { /** * invocation :連線點 */ @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("===========around環繞通知方法 開始==========="); // 呼叫目標方法之前執行的動作 System.out.println("環繞通知--呼叫方法之前: 執行"); // 呼叫方法的引數 Object[] args = invocation.getArguments(); // 呼叫的方法 Method method = invocation.getMethod(); // 獲取目標物件 Object target = invocation.getThis(); System.out.println("輸入引數:" + args[0] + ";" + method + ";" + target); // 執行完方法的返回值:呼叫proceed()方法,就會觸發切入點方法執行 Object returnValue = invocation.proceed(); System.out.println("環繞通知--呼叫方法之後: 執行"); System.out.println("輸出引數:" + args[0] + ";" + method + ";" + target + ";" + returnValue); System.out.println("===========around環繞通知方法 結束==========="); return returnValue; } }
4. 自定義切點
package com.lxl.www.aop.interfaceAop; /** * 切點 * * 繼承NameMatchMethodPointcut類,來用方法名匹配 */ import org.springframework.aop.support.NameMatchMethodPointcut; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Component public class Pointcut extends NameMatchMethodPointcut { private static final long serialVersionUID = 3990456017285944475L; @SuppressWarnings("rawtypes") @Override public boolean matches(Method method, Class targetClass) { // 設定單個方法匹配 this.setMappedName("add"); // 設定多個方法匹配 String[] methods = { "add", "div" }; //也可以用“ * ” 來做匹配符號 // this.setMappedName("get*"); this.setMappedNames(methods); return super.matches(method, targetClass); } }
5. 配置xml檔案
<?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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" > <!-- ==============================aop配置================================ --> <!-- 宣告一個業務類 --> <bean id="baseCalculate" class="com.lxl.www.aop.interfaceAop.BaseCalculate"/> <!-- 宣告通知類 --> <bean id="baseBefore" class="com.lxl.www.aop.interfaceAop.BaseBeforeAdvice"/> <bean id="baseAfterReturn" class="com.lxl.www.aop.interfaceAop.BaseAfterReturnAdvice"/> <bean id="baseAfterThrows" class="com.lxl.www.aop.interfaceAop.BaseAfterThrowsAdvice"/> <bean id="baseAround" class="com.lxl.www.aop.interfaceAop.BaseAroundAdvice"/> <!-- 指定切點匹配類 --> <bean id="pointcut" class="com.lxl.www.aop.interfaceAop.Pointcut"/> <!-- 包裝通知,指定切點 --> <bean id="matchBeforeAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="pointcut"> <ref bean="pointcut"/> </property> <property name="advice"> <ref bean="baseBefore"/> </property> </bean> <!-- 使用ProxyFactoryBean 產生代理物件 --> <bean id="businessProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 代理物件所實現的介面 ,如果有介面可以這樣設定 --> <property name="proxyInterfaces"> <value>com.lxl.www.aop.interfaceAop.IBaseCalculate</value> </property> <!-- 設定目標物件 --> <property name="target"> <ref bean="baseCalculate"/> </property> <!-- 代理物件所使用的攔截器 --> <property name="interceptorNames"> <list> <value>matchBeforeAdvisor</value> <value>baseAfterReturn</value> <value>baseAround</value> </list> </property> </bean> </beans>
6. 方法入口
package com.lxl.www.aop.interfaceAop; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class InterfaceMainClass{ public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/aop.xml"); BaseCalculate calculate = (BaseCalculate) context.getBean("baseCalculate"); calculate.add(1, 3); } }
三. 分析
各種型別通知的執行順序: 前置方法會在切入點方法之前執行,後置會在切入點方法執行之後執行,環繞則會在切入點方法執行前執行同事方法結束也會執行對應的部分。主要是呼叫proceed()方法來執行切入點方法。來作為環繞通知前後方法的分水嶺
在xml 配置 businessProxy這個bean的時候,ProxyFactoryBean類中指定了,proxyInterfaces引數。這裡把他配置了IBaseCalculate介面。因為在專案開發過程中,往往業務類都會有對應的介面,以方便利用IOC解耦。但Spring AOP卻也能支援沒有介面的代理。這就是為什麼需要匯入cglib.jar包了。看過spring的原始碼,知道在目標切入物件如果有實現介面,spring會預設使用jdk動態代理來實現代理類。如果沒有介面,則會通過cglib來實現代理類。
這個業務類現在有 前置通知,後置通知,環繞三個通知同時作用,可能以及更多的通知進行作用。那麼這些通知的執行順序是怎麼樣的?就這個例子而言,同時實現了三個通知。在例 子xml中,則顯示執行before通知,然後執行around的前處理,執行切點方法,再執行return處理。最後執行around的後處理。經過測 試,知道spring 處理順序是按照xml配置順序依次處理通知,以佇列的方式存放前通知,以壓棧的方式存放後通知。所以是前通知依次執行,後通知到切入點執行完之後,從棧裡 在後進先出的形式把後通知執行。
在實現過程中發現通知執行對應目標物件的整個類中的方法,如何精確到某個方法,則需要定義一個切點匹配的方式:spring提供了方法名匹配或正則方式來匹配。然後通過DefaultPointcutAdvisor來包裝通知,指定切點。
使用介面方式配置起來,可見程式碼還是非常的厚重的,定義一個切面就要定義一個切面類,然而切面類中,就一個通知方法,著實沒有必要。所以Spring提供了,依賴aspectj的schema配置和基於aspectj 註解方式。這兩種方式非常簡單方便使用,也是專案中普遍的使用方式。