一、概念
1、理論
把橫切關注點和業務邏輯相分離是面向切面編程所要解決的問題。如果要重用通用功能的話,最常見的面向對象技術是繼承(inheritance)或 組成(delegation)。但是,如果在整個應用中都使用相同的基類,繼承往往會導致一個脆弱的對象體系;而使用組成可能需要對委托對象進行復雜的調用。切面提供了取代繼承和委托的另一種可選方案,而且在很多場景下更清晰簡潔。Spring AOP 基於動態代理,所以Spring只支持方法連接點,這與一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切點,它們還提供了字段和構造器接入點。
2、AOP術語
橫切關注點(cross-cutuing concern):散布在應用中多處的功能。

切面(aspect) : 橫切關注點模塊化為特殊的類。切面是通知和切點的結合。
通知(advice):定義了切面是什么以及何時使用。
Spring切面可以應用5種類型的通知:
前置通知(Before):在目標方法被調用之前調用通知功能;
后置通知(After):在目標方法完成之后調用通知,此時不會關心方法的輸出是什么;
返回通知(After-returning):在目標方法成功執行之后調用通知;
異常通知(After-throwing):在目標方法拋出異常后調用通知;
環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之后執行自定義的行為。
切點(pointcut):定義了切面在何處調用,會匹配通知所要織入的一個或多個連接點。
連接點(join point):在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。

織入(Weaving):織入是把切面應用到目標對象並創建新的代理對象的過程。
織入有三種方式可以實現,Spring采用的是第三種,在運行期織入的:
編譯期:切面在目標類編譯時被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標類被引入應用之前增強該目標類的字節碼。AspectJ 5的加載時織入(load-time weaving,LTW)就支持以這種方式織入切面。
運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會為目標對象動態地創建一個代理對象。代理類封裝了目標類,並攔截被通知方法的調用,再把調用轉發給真正的目標bean。SpringAOP就是以這種方式織入切面的。

3、AspectJ的切點表達式語言


注意:只有execution指示器是實際執行匹配的,而其他的指示器都是用來限制匹配的。這說明execution指示器是我們在編寫切點定義時最主要使用的指示器 。同時需要注意的是, 表達式之間允許用 &&(and)、||(or)、!(not) 來匹配復雜的被通知類。除了上面羅列的表達式外,Spring 還提供了一個Bean 表達式來匹配 Bean 的id,例如 execution(* com.service.Performance.perform(..)) && bean(performance)
@args的正確用法:自定義一個ElementType.TYPE的注解,這個注解用來修飾自定義類型(比如自己寫的一個類),一個方法以這個自定義的類的實例為參數且只能有這唯一一參數,那這個方法在調用時會被匹配@args(自定義注解)的切面攔截。
@annotation的正確用法:在切面類上用@annotation加自定義注解就可以攔截使用這個注解的方法。比如匹配 @RequestMapping 注解的類 @annotation(org.springframework.web.bind.annotation.RequestMapping)
@target (cn.javass.spring.chapter6.Secure) 任何目標對象持有Secure注解的類方法;必須是在目標對象上聲明這個注解,在接口上聲明的對它不起作用。

二、使用注解創建切面
1、添加pom.xml依賴
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.11</version> </dependency>
2、定義切面
@Aspect //表示這是一個切面類 public class Audience { //使用簡明的PointCut @Pointcut("execution(* com.service.Performance.perform(..))") public void performance(){} //前置通知 即 @Before("execution(* com.service.Performance.perform(..))") @Before("performance()") public void silenceCellPhones(){ System.out.println("Silencing cell phones"); } //前置通知 即 @Before("execution(* com.service.Performance.perform(..))") @Before("performance()") public void takeSeats(){ System.out.println("Taking seats"); } //方法調用結束通知(並不是指返回值通知,即使是void的返回值,仍然會觸發通知) 即 @AfterReturning("execution(* com.service.Performance.perform(..))") @AfterReturning("performance()") public void applause(){ System.out.println("CLAP CLAP CLAP!!!"); } //有異常拋出的時候通知,即 @AfterThrowing("execution(* com.service.Performance.perform(..))") @AfterThrowing("performance()") public void demandRefund(){ System.out.println("Demanding a refund"); } }
3、啟用AspectJ注解的自動代理
有兩種方式可以啟用AspectJ 注解的自動代理:
(1)在 Java 配置文件中顯示配置
@Configuration @EnableAspectJAutoProxy //啟用Aop自動代理 public class JavaConfig { @Bean public Audience getAudience(){ return new Audience(); } }
(2)在XML文件中配置
<!--啟用AspectJ自動代理--> <aop:aspectj-autoproxy/> <bean id="audience" class="com.aspect.Audience"/>
不管你是使用JavaConfig還是XML,AspectJ自動代理都會為使用@Aspect注解的bean創建一個代理,這個代理會圍繞着所有該切面的切點所匹配的bean。當程序執行到連接點的時候,就會由代理轉到切面觸發相應的通知。
4、創建環繞通知
@Aspect public class Audience3 { @Pointcut("execution(* com.service.Performance.perform(..))") public void performance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint joinPoint) { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); try { joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); throwable.printStackTrace(); } } }
注意 ProceedingJoinPoint 作為參數。這個對象是必須要有的,因為你要在通知中通過它來調用被通知的方法。當要將控制權交給被通知的方法時,它需要調用ProceedingJoinPoint的proceed()方法。
5、切面匹配輸入參數
@Aspect public class TrackCounter { private Map<Integer,Integer> trackCounts=new HashMap<Integer, Integer>(); //@Pointcut("execution(* com.service.CompactDisc.playTrack(int)) && args(trackNumber)") //帶入輸入參數 //@Pointcut("target(com.service.CompactDisc) && args(trackNumber)") // target 匹配目標對象(非AOP對象)為指定類型 //@Pointcut("within(com.service..*) && args(trackNumber)") //com.service 包以及子包下的所有方法都執行 //@Pointcut("within(com.service..CompactDisc+) && args(trackNumber)") //com.service 包的CompactDisc類型以及子類型 @Pointcut("this(com.service.CompactDisc) && args(trackNumber)") //匹配當前AOP代理對象類型,必須是類型全稱,不支持通配符 public void trackPlayed(int trackNumber){} @Before("trackPlayed(trackNumber)") public void countTrack(int trackNumber){ int playCount = getPlayCount(trackNumber); trackCounts.put(trackNumber,playCount+1); System.out.println(trackCounts.toString()); } public int getPlayCount(int trackNumber){ return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0; } }
* 的意思是任意類型任意名稱的一個參數
.. 的意思是任意類型,任意多個參數,並且只能放到args的后面。
6、利用切面注入新功能
Java並不是動態語言。一旦類編譯完成了,我們就很難為該類添加新的功能了。但是,我們的切面編程卻可以做到動態的添加方法...話雖如此,其實也不過是障眼法罷了。實際上,面向切面編程,不過是把方法添加到切面代理中,當要對添加的方法調用的時候,可以把被通知的 Bean 轉換成相應的接口。也就是代理會把此調用委托給實現了新接口的某個其他對象。實際上,一個bean的實現被拆分到了多個類中。(說實話,想了半天,實在想不到這個功能有什么作用......)

(1) 重新定義一個接口和實現類
public interface Encoreable { void performEncode(); }
public class DefaultEncoreable implements Encoreable { public void performEncode() { System.out.println("this is DefaultEncoreable"); } }
(2) 把接口實現類嵌入到目標類代理中
@Aspect public class EncoreableIntroducer { @DeclareParents(value = "com.service.CompactDisc+", defaultImpl = DefaultEncoreable.class) //value 表示要嵌入哪些目標類的代理 。 defaultImpl:表示要嵌入的接口的默認實現方法 public static Encoreable encoreable; }
(3) JUnit 測試
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext.xml") public class Test02 { @Autowired private CompactDisc compactDisc; @Test public void test02(){ compactDisc.playTrack(123); Encoreable compactDisc = (Encoreable) this.compactDisc; //當要調用添加的新功能的時候,這個用法相當於由代理轉換到對應類實現,不會報類型轉換錯誤 compactDisc.performEncode(); } }
三、使用XML聲明切面


1、定義切面
public class AudienceXML { public void silenceCellPhones(){ System.out.println("Silencing cell phones"); } public void takeSeats(){ System.out.println("Taking seats"); } public void applause(){ System.out.println("CLAP CLAP CLAP!!!"); } public void demandRefund(){ System.out.println("Demanding a refund"); } }
2、XML配置切面
<aop:config> <aop:aspect ref="audienceXML"> <aop:pointcut id="performance" expression="execution(* com.service.Performance.perform(..))"/> <aop:before method="silenceCellPhones" pointcut-ref="performance"/> <aop:before method="takeSeats" pointcut-ref="performance"/> <aop:after-returning method="applause" pointcut-ref="performance"/> <aop:after-throwing method="demandRefund" pointcut-ref="performance"/> </aop:aspect> </aop:config>
3、創建環繞通知
public class Audience3XML { public void watchPerformance(ProceedingJoinPoint joinPoint) { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); try { joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); throwable.printStackTrace(); } } }
<aop:config> <aop:aspect ref="audience3XML"> <aop:pointcut id="performance3" expression="execution(* com.service.Performance.perform(..))"/> <aop:around method="watchPerformance" pointcut-ref="performance3"/> </aop:aspect> </aop:config>
4、匹配輸入參數
<aop:config> <aop:aspect ref="trackCounter"> <aop:pointcut id="trackPlayed" expression="execution(* com.service.CompactDisc.playTrack(int)) and args(trackNumber)"/> <aop:before method="countTrack" pointcut-ref="trackPlayed"/> </aop:aspect> </aop:config>
5、注入新功能
<aop:config> <aop:aspect> <aop:declare-parents types-matching="com.service.CompactDisc+" implement-interface="com.service.Encoreable" default-impl="com.service.impl.DefaultEncoreable" delegate-ref="encoreableDelegate"/> </aop:aspect> </aop:config>
