Spring有兩大核心,IOC和AOP。IOC在java web項目中無時無刻不在使用。然而AOP用的比較少,的確也是一般的項目用的場所不多。事務控制基本都用,但卻是Spring封裝的不需要我們再去實現,但Spring的AOP遠不止這些,不能因爲項目中沒有使用,而不去學習及理解。我覺得這是作爲一個java web軟件開發人員必須具備的技能。業內很多將AOP應用在日誌記錄上,可惜我們項目沒這麼做,後面需要學習下。在這先把Spring AOP的基本用法,在腦子裏理一邊,做一次積累。
1、概念術語
在開始之前,需要理解Spring aop 的一些基本的概念術語(總結的個人理解,並非Spring官方定義):
切面(aspect):用來切插業務方法的類。
連接點(joinpoint):是切面類和業務類的連接點,其實就是封裝了業務方法的一些基本屬性,作爲通知的參數來解析。
通知(advice):在切面類中,聲明對業務方法做額外處理的方法。
切入點(pointcut):業務類中指定的方法,作爲切面切入的點。其實就是指定某個方法作爲切面切的地方。
目標對象(target object):被代理對象。
AOP代理(aop proxy):代理對象。
通知:
前置通知(before advice):在切入點之前執行。
後置通知(after returning advice):在切入點執行完成後,執行通知。
環繞通知(around advice):包圍切入點,調用方法前後完成自定義行爲。
異常通知(after throwing advice):在切入點拋出異常後,執行通知。
2、Spring AOP環境
要在項目中使用Spring AOP 則需要在項目中導入除了spring jar包之外,還有aspectjweaver.jar,aopalliance.jar ,asm.jar 和cglib.jar 。
好了,前提工作準備完成,Spring 提供了很多的實現AOP的方式,在學習過程中,循序漸進。進行Spring 接口方式,schema配置方式和註解的三種方式進行學習。好了廢話不多說了,開始spring aop學習之旅:
3、方式一:AOP接口
利用Spring AOP接口實現AOP,主要是爲了指定自定義通知來供spring AOP機制識別。主要接口:前置通知 MethodBeforeAdvice ,後置通知:AfterReturningAdvice,環繞通知:MethodInterceptor,異常通知:ThrowsAdvice 。見例子代碼:
a、業務接口:
/** * 代理類接口,也是業務類接口<br> * * 利用接口的方式,spring aop 將默認通過jdk 動態代理來實現代理類<br> * 不利用接口,則spring aop 將通過cglib 來實現代理類 * * @author yanbin * */ public interface IBaseBusiness { /** * 用作代理的切入點方法 * * @param obj * @return */ public String delete(String obj); /** * 這方法不被切面切 * * @param obj * @return */ public String add(String obj); /** * 這方法切不切呢?可以設置 * * @param obj * @return */ public String modify(String obj); }
b、業務類:
/** * 業務類,也是目標對象 * * @author yanbin * */ public class BaseBusiness implements IBaseBusiness { /** * 切入點 */ public String delete(String obj) { System.out.println("==========調用切入點:" + obj + "說:你敢刪除我!===========\n"); return obj + ":瞄~"; } public String add(String obj) { System.out.println("================這個方法不能被切。。。============== \n"); return obj + ":瞄~ 嘿嘿!"; } public String modify(String obj) { System.out.println("=================這個也設置加入切吧====================\n"); return obj + ":瞄改瞄啊!"; } }
c、通知類:
前置通知:
/** * 前置通知。 * * @author yanbin * */ public class BaseBeforeAdvice implements MethodBeforeAdvice { /** * method : 切入的方法 <br> * args :切入方法的參數 <br> * target :目標對象 */ @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("===========進入beforeAdvice()============ \n"); System.out.print("準備在" + target + "對象上用"); System.out.print(method + "方法進行對 '"); System.out.print(args[0] + "'進行刪除!\n\n"); System.out.println("要進入切入點方法了 \n"); } }
後置通知:
/** * 後置通知 * * @author yanbin * */ public class BaseAfterReturnAdvice implements AfterReturningAdvice { /** * returnValue :切入點執行完方法的返回值,但不能修改 <br> * method :切入點方法 <br> * args :切入點方法的參數數組 <br> * target :目標對象 */ @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("==========進入afterReturning()=========== \n"); System.out.println("切入點方法執行完了 \n"); System.out.print(args[0] + "在"); System.out.print(target + "對象上被"); System.out.print(method + "方法刪除了"); System.out.print("只留下:" + returnValue + "\n\n"); } }
環繞通知:
/** * 環繞通知 * * @author yanbin * */ public class BaseAroundAdvice implements MethodInterceptor { /** * invocation :連接點 */ @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("===========進入around環繞方法!=========== \n"); // 調用目標方法之前執行的動作 System.out.println("調用方法之前: 執行!\n"); // 調用方法的參數 Object[] args = invocation.getArguments(); // 調用的方法 Method method = invocation.getMethod(); // 獲取目標對象 Object target = invocation.getThis(); // 執行完方法的返回值:調用proceed()方法,就會觸發切入點方法執行 Object returnValue = invocation.proceed(); System.out.println("===========結束進入around環繞方法!=========== \n"); System.out.println("輸出:" + args[0] + ";" + method + ";" + target + ";" + returnValue + "\n"); System.out.println("調用方法結束:之後執行!\n"); return returnValue; } }
異常通知:
/** * 異常通知,接口沒有包含任何方法。通知方法自定義 * * @author yanbin * */ 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("刪除出錯啦"); } }
d、定義指定切點:
/** * 定義一個切點,指定對應方法匹配。來供切面來針對方法進行處理<br> * * 繼承NameMatchMethodPointcut類,來用方法名匹配 * * @author yanbin * */ public class Pointcut extends NameMatchMethodPointcut { private static final long serialVersionUID = 3990456017285944475L; @SuppressWarnings("rawtypes") @Override public boolean matches(Method method, Class targetClass) { // 設置單個方法匹配 this.setMappedName("delete"); // 設置多個方法匹配 String[] methods = { "delete", "modify" }; //也可以用“ * ” 來做匹配符號 // this.setMappedName("get*"); this.setMappedNames(methods); return super.matches(method, targetClass); } }
e、配置:
<?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" default-autowire="byName"> <!-- ==============================利用spring自己的aop配置================================ --> <!-- 聲明一個業務類 --> <bean id="baseBusiness" class="aop.base.BaseBusiness" /> <!-- 聲明通知類 --> <bean id="baseBefore" class="aop.base.advice.BaseBeforeAdvice" /> <bean id="baseAfterReturn" class="aop.base.advice.BaseAfterReturnAdvice" /> <bean id="baseAfterThrows" class="aop.base.advice.BaseAfterThrowsAdvice" /> <bean id="baseAround" class="aop.base.advice.BaseAroundAdvice" /> <!-- 指定切點匹配類 --> <bean id="pointcut" class="aop.base.pointcut.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>aop.base.IBaseBusiness</value> </property> <!-- 設置目標對象 --> <property name="target"> <ref local="baseBusiness" /> </property> <!-- 代理對象所使用的攔截器 --> <property name="interceptorNames"> <list> <value>matchBeforeAdvisor</value> <value>baseAfterReturn</value> <value>baseAround</value> </list> </property> </bean> </beans>
f、測試類:
public class Debug { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/schema_aop.xml"); AspectBusiness business = (AspectBusiness) context.getBean("aspectBusiness"); business.delete("貓"); } }
g、測試結果:運行下測試類,清晰明瞭。由於結果呈現太長就不貼了。
具體的代碼實現可以從代碼註釋中很容易理解 接口方式的實現。結果也可想而知,前置方法會在切入點方法之前執行,後置會在切入點方法執行之後執行,環繞則會在切入點方法執行前執行同事方法結束也會執行對應的部分。主要是調用proceed()方法來執行切入點方法。來作爲環繞通知前後方法的分水嶺。然後在實現的過程中,有幾點卻是可以細揣摩一下的。
可以看出在xml 配置 businessProxy這個bean的時候,ProxyFactoryBean類中指定了,proxyInterfaces參數。這裏我把他配置了IBaseBusiness接口。因爲在項目開發過程中,往往業務類都會有對應的接口,以方便利用IOC解耦。但Spring AOP卻也能支持沒有接口的代理。這就是爲什麼需要導入cglib.jar的包了。看過spring的源碼,知道在目標切入對象如果有實現接口,spring會默認走jdk動態代理來實現代理類。如果沒有接口,則會通過cglib來實現代理類。
這個業務類現在有 前置通知,後置通知,環繞三個通知同時作用,可能以及更多的通知進行作用。那麼這些通知的執行順序是怎麼樣的?就這個例子而言,同時實現了三個通知。在例子xml中,則顯示執行before通知,然後執行around的前處理,執行切點方法,再執行return處理。最後執行around的後處理。經過測試,知道spring 處理順序是按照xml配置順序依次處理通知,以隊列的方式存放前通知,以壓棧的方式存放後通知。所以是前通知依次執行,後通知到切入點執行完之後,從棧裏在後進先出的形式把後通知執行。
在實現過程中發現通知執行對應目標對象的整個類中的方法,如何精確到某個方法,則需要定義一個切點匹配的方式:spring提供了方法名匹配或正則方式來匹配。然後通過DefaultPointcutAdvisor來包裝通知,指定切點。
本來是想一口氣梳理完的。但是大晚上時間不夠(無奈一場奧運籃球總決賽耗費掉了2小時,不過的確相當精彩),又考慮到篇幅太長,閱讀性比較差,所以將後半部分更偏於應用的重起一篇隨筆。
利用方式一的配置起來,可見代碼還是非常的厚重的,定義一個切面就要定義一個切面類,然而切面類中,就一個通知方法,着實沒有必要。所以Spring提供了,依賴aspectj的schema配置和基於aspectj 註解方式。這兩種方式非常簡介方便使用,也是項目中普遍的使用方式。梳理之:
4、方式二:schema配置
a、業務類:
/** * 業務類 * * @author yanbin * */ public class AspectBusiness { /** * 切入點 */ public String delete(String obj) { System.out.println("==========調用切入點:" + obj + "說:你敢刪除我!===========\n"); return obj + ":瞄~"; } public String add(String obj) { System.out.println("================這個方法不能被切。。。============== \n"); return obj + ":瞄~ 嘿嘿!"; } public String modify(String obj) { System.out.println("=================這個也設置加入切吧====================\n"); return obj + ":瞄改瞄啊!"; } }
b、切面類:切面類中,包含了所有的通知
/** * 定義一個切面 * * @author yanbin * */ public class AspectAdvice { /** * 前置通知 * * @param jp */ public void doBefore(JoinPoint jp) { System.out.println("===========進入before advice============ \n"); System.out.print("準備在" + jp.getTarget().getClass() + "對象上用"); System.out.print(jp.getSignature().getName() + "方法進行對 '"); System.out.print(jp.getArgs()[0] + "'進行刪除!\n\n"); System.out.println("要進入切入點方法了 \n"); } /** * 後置通知 * * @param jp * 連接點 * @param result * 返回值 */ public void doAfter(JoinPoint jp, String result) { System.out.println("==========進入after advice=========== \n"); System.out.println("切入點方法執行完了 \n"); System.out.print(jp.getArgs()[0] + "在"); System.out.print(jp.getTarget().getClass() + "對象上被"); System.out.print(jp.getSignature().getName() + "方法刪除了"); System.out.print("只留下:" + result + "\n\n"); } /** * 環繞通知 * * @param pjp * 連接點 */ public void doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("===========進入around環繞方法!=========== \n"); // 調用目標方法之前執行的動作 System.out.println("調用方法之前: 執行!\n"); // 調用方法的參數 Object[] args = pjp.getArgs(); // 調用的方法名 String method = pjp.getSignature().getName(); // 獲取目標對象 Object target = pjp.getTarget(); // 執行完方法的返回值:調用proceed()方法,就會觸發切入點方法執行 Object result = pjp.proceed(); System.out.println("輸出:" + args[0] + ";" + method + ";" + target + ";" + result + "\n"); System.out.println("調用方法結束:之後執行!\n"); } /** * 異常通知 * * @param jp * @param e */ public void doThrow(JoinPoint jp, Throwable e) { System.out.println("刪除出錯啦"); } }
c、配置文件:
<?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" default-autowire="byName"> <!-- ==============================利用spring 利用aspectj來配置AOP================================ --> <!-- 聲明一個業務類 --> <bean id="aspectBusiness" class="aop.schema.AspectBusiness" /> <!-- 聲明通知類 --> <bean id="aspectAdvice" class="aop.schema.advice.AspectAdvice" /> <aop:config> <aop:aspect id="businessAspect" ref="aspectAdvice"> <!-- 配置指定切入的對象 --> <aop:pointcut id="point_cut" expression="execution(* aop.schema.*.*(..))" /> <!-- 只匹配add方法作爲切入點 <aop:pointcut id="except_add" expression="execution(* aop.schema.*.add(..))" /> --> <!-- 前置通知 --> <aop:before method="doBefore" pointcut-ref="point_cut" /> <!-- 後置通知 returning指定返回參數 --> <aop:after-returning method="doAfter" pointcut-ref="point_cut" returning="result" /> <aop:around method="doAround" pointcut-ref="point_cut"/> <aop:after-throwing method="doThrow" pointcut-ref="point_cut" throwing="e"/> </aop:aspect> </aop:config> </beans>
d、測試類:
public class Debug { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/schema_aop.xml"); AspectBusiness business = (AspectBusiness) context.getBean("aspectBusiness"); business.delete("貓"); } }
5、方式三:aspectj註解
註解在項目中已經到處都是了,撇開一些優劣不提,開發的便利性和可讀性是非常的方便的。用來配置Spring AOP也非常簡單便利
a、業務類:
/** * 業務類 * * @author yanbin * */ @Component public class Business { /** * 切入點 */ public String delete(String obj) { System.out.println("==========調用切入點:" + obj + "說:你敢刪除我!===========\n"); return obj + ":瞄~"; } public String add(String obj) { System.out.println("================這個方法不能被切。。。============== \n"); return obj + ":瞄~ 嘿嘿!"; } public String modify(String obj) { System.out.println("=================這個也設置加入切吧====================\n"); return obj + ":瞄改瞄啊!"; } }
b、切面類:
/** * 定義切面 * * @Aspect : 標記爲切面類 * @Pointcut : 指定匹配切點 * @Before : 指定前置通知,value中指定切入點匹配 * @AfterReturning :後置通知,具有可以指定返回值 * @AfterThrowing :異常通知 * * @author yanbin * */ @Component @Aspect public class AspectAdvice { /** * 指定切入點匹配表達式,注意它是以方法的形式進行聲明的。 */ @Pointcut("execution(* aop.annotation.*.*(..))") public void anyMethod() { } /** * 前置通知 * * @param jp */ @Before(value = "execution(* aop.annotation.*.*(..))") public void doBefore(JoinPoint jp) { System.out.println("===========進入before advice============ \n"); System.out.print("準備在" + jp.getTarget().getClass() + "對象上用"); System.out.print(jp.getSignature().getName() + "方法進行對 '"); System.out.print(jp.getArgs()[0] + "'進行刪除!\n\n"); System.out.println("要進入切入點方法了 \n"); } /** * 後置通知 * * @param jp * 連接點 * @param result * 返回值 */ @AfterReturning(value = "anyMethod()", returning = "result") public void doAfter(JoinPoint jp, String result) { System.out.println("==========進入after advice=========== \n"); System.out.println("切入點方法執行完了 \n"); System.out.print(jp.getArgs()[0] + "在"); System.out.print(jp.getTarget().getClass() + "對象上被"); System.out.print(jp.getSignature().getName() + "方法刪除了"); System.out.print("只留下:" + result + "\n\n"); } /** * 環繞通知 * * @param pjp * 連接點 */ @Around(value = "execution(* aop.annotation.*.*(..))") public void doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("===========進入around環繞方法!=========== \n"); // 調用目標方法之前執行的動作 System.out.println("調用方法之前: 執行!\n"); // 調用方法的參數 Object[] args = pjp.getArgs(); // 調用的方法名 String method = pjp.getSignature().getName(); // 獲取目標對象 Object target = pjp.getTarget(); // 執行完方法的返回值:調用proceed()方法,就會觸發切入點方法執行 Object result = pjp.proceed(); System.out.println("輸出:" + args[0] + ";" + method + ";" + target + ";" + result + "\n"); System.out.println("調用方法結束:之後執行!\n"); } /** * 異常通知 * * @param jp * @param e */ @AfterThrowing(value = "execution(* aop.annotation.*.*(..))", throwing = "e") public void doThrow(JoinPoint jp, Throwable e) { System.out.println("刪除出錯啦"); } }
c、配置:
<?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" default-autowire="byName"> <context:component-scan base-package="aop.annotation" /> <!-- 打開aop 註解 --> <aop:aspectj-autoproxy /> </beans>
d、測試類:
/** * 測試類 * * @author yanbin * */ public class Debug { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/annotation_aop.xml"); Business business = (Business) context.getBean("business"); business.delete("貓"); } }