摘要
AOP是OOP的延續,是Aspect Oriented Programming的縮寫,意思是面向切面編程。可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是調用者和被調用者之間的解耦,AOP可以說也是這種目標的一種實現。
我們現在做的一些非業務,如:日志、事務、安全等都會寫在業務代碼中(也即是說,這些非業務類橫切於業務類),但這些代碼往往是重復,復制——粘貼式的代碼會給程序的維護帶來不便,AOP就實現了把這些業務需求與系統需求分開來做。這種解決的方式也稱代理機制。
先來了解一下AOP的相關概念,《Spring參考手冊》中定義了以下幾個AOP的重要概念,結合以上代碼分析如下:
-
切面(Aspect):官方的抽象定義為“一個關注點的模塊化,這個關注點可能會橫切多個對象”,在本例中,“切面”就是類TestAspect所關注的具體行為,例如,AServiceImpl.barA()的調用就是切面TestAspect所關注的行為之一。“切面”在ApplicationContext中<aop:aspect>來配置。
-
連接點(Joinpoint) :程序執行過程中的某一行為,例如,UserService.get的調用或者UserService.delete拋出異常等行為。
-
通知(Advice) :“切面”對於某個“連接點”所產生的動作,例如,TestAspect中對com.spring.service包下所有類的方法進行日志記錄的動作就是一個Advice。其中,一個“切面”可以包含多個“Advice”,例如ServiceAspect。
-
切入點(Pointcut) :匹配連接點的斷言,在AOP中通知和一個切入點表達式關聯。例如,TestAspect中的所有通知所關注的連接點,都由切入點表達式execution(* com.spring.service.*.*(..))來決定。
-
目標對象(Target Object) :被一個或者多個切面所通知的對象。例如,AServcieImpl和BServiceImpl,當然在實際運行時,Spring AOP采用代理實現,實際AOP操作的是TargetObject的代理對象。
-
AOP代理(AOP Proxy) :在Spring AOP中有兩種代理方式,JDK動態代理和CGLIB代理。默認情況下,TargetObject實現了接口時,則采用JDK動態代理,例如,AServiceImpl;反之,采用CGLIB代理,例如,BServiceImpl。強制使用CGLIB代理需要將 <aop:config>的 proxy-target-class屬性設為true。
通知(Advice)類型:
-
前置通知(Before advice):在某連接點(JoinPoint)之前執行的通知,但這個通知不能阻止連接點前的執行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素進行聲明。例如,TestAspect中的doBefore方法。
-
后置通知(After advice):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。ApplicationContext中在<aop:aspect>里面使用<aop:after>元素進行聲明。例如,ServiceAspect中的returnAfter方法,所以Teser中調用UserService.delete拋出異常時,returnAfter方法仍然執行。
-
返回后通知(After return advice):在某連接點正常完成后執行的通知,不包括拋出異常的情況。ApplicationContext中在<aop:aspect>里面使用<after-returning>元素進行聲明。
-
環繞通知(Around advice):包圍一個連接點的通知,類似Web中Servlet規范中的Filter的doFilter方法。可以在方法的調用前后完成自定義的行為,也可以選擇不執行。ApplicationContext中在<aop:aspect>里面使用<aop:around>元素進行聲明。例如,ServiceAspect中的around方法。
-
拋出異常后通知(After throwing advice):在方法拋出異常退出時執行的通知。ApplicationContext中在<aop:aspect>里面使用<aop:after-throwing>元素進行聲明。例如,ServiceAspect中的returnThrow方法。
注:可以將多個通知應用到一個目標對象上,即可以將多個切面織入到同一目標對象。
使用Spring AOP可以基於兩種方式,一種是比較方便和強大的注解方式,另一種則是中規中矩的xml配置方式。
先說注解,使用注解配置Spring AOP總體分為兩步,第一步是在xml文件中聲明激活自動掃描組件功能,同時激活自動代理功能(同時在xml中添加一個UserService的普通服務層組件,來測試AOP的注解功能):
<?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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- 激活組件掃描功能,在包cn.ysh.studio.spring.aop及其子包下面自動掃描通過注解配置的組件 --> <context:component-scan base-package="cn.ysh.studio.spring.aop"/> <!-- 激活自動代理功能 --> <aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 用戶服務對象 --> <bean id="userService" class="cn.ysh.studio.spring.aop.service.UserService" /> </beans>
第二步是為Aspect切面類添加注解:
package cn.ysh.studio.spring.aop.aspect; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * 系統服務組件Aspect切面Bean * @author Shenghany * @date 2013-5-28 */ //聲明這是一個組件 @Component //聲明這是一個切面Bean @Aspect public class ServiceAspect { private final static Log log = LogFactory.getLog(ServiceAspect.class); //配置切入點,該方法無方法體,主要為方便同類中其他方法使用此處配置的切入點 @Pointcut("execution(* cn.ysh.studio.spring.aop.service..*(..))") public void aspect(){ } /* * 配置前置通知,使用在方法aspect()上注冊的切入點 * 同時接受JoinPoint切入點對象,可以沒有該參數 */ @Before("aspect()") public void before(JoinPoint joinPoint){ if(log.isInfoEnabled()){ log.info("before " + joinPoint); } } //配置后置通知,使用在方法aspect()上注冊的切入點 @After("aspect()") public void after(JoinPoint joinPoint){ if(log.isInfoEnabled()){ log.info("after " + joinPoint); } } //配置環繞通知,使用在方法aspect()上注冊的切入點 @Around("aspect()") public void around(JoinPoint joinPoint){ long start = System.currentTimeMillis(); try { ((ProceedingJoinPoint) joinPoint).proceed(); long end = System.currentTimeMillis(); if(log.isInfoEnabled()){ log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!"); } } catch (Throwable e) { long end = System.currentTimeMillis(); if(log.isInfoEnabled()){ log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage()); } } } //配置后置返回通知,使用在方法aspect()上注冊的切入點 @AfterReturning("aspect()") public void afterReturn(JoinPoint joinPoint){ if(log.isInfoEnabled()){ log.info("afterReturn " + joinPoint); } } //配置拋出異常后通知,使用在方法aspect()上注冊的切入點 @AfterThrowing(pointcut="aspect()", throwing="ex") public void afterThrow(JoinPoint joinPoint, Exception ex){ if(log.isInfoEnabled()){ log.info("afterThrow " + joinPoint + "\t" + ex.getMessage()); } } }
測試代碼:
package cn.ysh.studio.spring.aop;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import cn.ysh.studio.spring.aop.service.UserService; import cn.ysh.studio.spring.mvc.bean.User; /** * Spring AOP測試 * @author Shenghany * @date 2013-5-28 */ public class Tester { private final static Log log = LogFactory.getLog(Tester.class); public static void main(String[] args) { //啟動Spring容器 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); //獲取service組件 UserService service = (UserService) context.getBean("userService"); //以普通的方式調用UserService對象的三個方法 User user = service.get(1L); service.save(user); try { service.delete(1L); } catch (Exception e) { if(log.isWarnEnabled()){ log.warn("Delete user : " + e.getMessage()); } } } }
控制台輸出如下:
INFO [spring.aop.aspect.ServiceAspect:40] before execution(User cn.ysh.studio.spring.aop.service.UserService.get(long)) INFO [spring.aop.service.UserService:19] getUser method . . . INFO [spring.aop.aspect.ServiceAspect:60] around execution(User cn.ysh.studio.spring.aop.service.UserService.get(long)) Use time : 42 ms! INFO [spring.aop.aspect.ServiceAspect:48] after execution(User cn.ysh.studio.spring.aop.service.UserService.get(long)) INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(User cn.ysh.studio.spring.aop.service.UserService.get(long)) INFO [spring.aop.aspect.ServiceAspect:40] before execution(void cn.ysh.studio.spring.aop.service.UserService.save(User)) INFO [spring.aop.service.UserService:26] saveUser method . . . INFO [spring.aop.aspect.ServiceAspect:60] around execution(void cn.ysh.studio.spring.aop.service.UserService.save(User)) Use time : 2 ms! INFO [spring.aop.aspect.ServiceAspect:48] after execution(void cn.ysh.studio.spring.aop.service.UserService.save(User)) INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(void cn.ysh.studio.spring.aop.service.UserService.save(User)) INFO [spring.aop.aspect.ServiceAspect:40] before execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long)) INFO [spring.aop.service.UserService:32] delete method . . . INFO [spring.aop.aspect.ServiceAspect:65] around execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long)) Use time : 5 ms with exception : spring aop ThrowAdvice演示 INFO [spring.aop.aspect.ServiceAspect:48] after execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long)) INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long)) WARN [studio.spring.aop.Tester:32] Delete user : Null return value from advice does not match primitive return type for: public boolean cn.ysh.studio.spring.aop.service.UserService.delete(long) throws java.lang.Exception
可以看到,正如我們預期的那樣,雖然我們並沒有對UserSerivce類包括其調用方式做任何改變,但是Spring仍然攔截到了其中方法的調用,或許這正是AOP的魔力所在。
再簡單說一下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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- 系統服務組件的切面Bean --> <bean id="serviceAspect" class="cn.ysh.studio.spring.aop.aspect.ServiceAspect"/> <!-- AOP配置 --> <aop:config> <!-- 聲明一個切面,並注入切面Bean,相當於@Aspect --> <aop:aspect id="simpleAspect" ref="serviceAspect"> <!-- 配置一個切入點,相當於@Pointcut --> <aop:pointcut expression="execution(* cn.ysh.studio.spring.aop.service..*(..))" id="simplePointcut"/> <!-- 配置通知,相當於@Before、@After、@AfterReturn、@Around、@AfterThrowing --> <aop:before pointcut-ref="simplePointcut" method="before"/> <aop:after pointcut-ref="simplePointcut" method="after"/> <aop:after-returning pointcut-ref="simplePointcut" method="afterReturn"/> <aop:after-throwing pointcut-ref="simplePointcut" method="afterThrow" throwing="ex"/> </aop:aspect> </aop:config