AOP的中文名稱叫做面向切面編程。這個名字非常形象,因爲你真的可以把一個系統像麪包一樣切開,並直接在麪包上增加修飾。切面可大可小,大到整個系統,小到某一個方法。
AOP有什麼用呢?舉個例子,每個組件中都可能含有安全、事務、數據庫等方面的邏輯,AOP就是把每個組件中的安全作爲一個方面進行集中處理,事務作爲一個方面,數據庫作爲一個方面等等。這樣才能做到高內聚、低耦合。AOP中有三個重要的術語:通知、切點、連接點。他們之間的關係如下圖。
AOP實現的原理是修改目標類中的代碼。至於怎麼修改,有多種方式:編譯時、類加載時、運行時。編譯時修改需要特殊的編譯器,類加載時修改需要特殊的類加載器。運行時,就是應用在運行的時候AOP框架會爲目標對象生成一個動態代理類。Spring AOP採用的就是運行時代理。Spring容器通過ObjectFactory創建所有的Bean實例,並且實例之外增加一層動態代理。SpringAOP具體實現主要涉及到反射機制中的Proxy.newProxyInstance和InvocationHandler,在後續的JVM文章中還會詳細介紹。
除了Spring AOP目前流行的AOP框架還有AspectJ、JBoss AOP。
下面是AOP的Hello World程序。目標是,在某個類的createApple方法調用之前做一些事情,但是又不能直接改變這個方法的代碼。下面這段代碼就是在createApple方法執行之前,額外執行beforeCreateApple,有點類似於Hook。代碼如下:
<bean id="appleListener" class="xxx"/>
<aop:aspect ref="appleListener">
<aop:pointcut id="apple" expression="execution(* *.createApple(..))" />
<aop:before pointcut-ref="apple" method="beforeCreateApple" />
</aop:aspect>
上面這段代碼的意思是,當程序中任何一個類的createApple方法被調用之前,都先調用appleListener中的beforeCreateApple方法。
切點表達式語言。上面例子中的execution(* *.createApple(..))就是表達式語言,第一個星號表示返回值的類型,第二個星號表示被調用的類名。支持如下語法:
- args() 將參數傳遞給切面
- @args() 匹配註解才傳遞參數
- execution() 匹配具體的方法
- this() 匹配當前bean
- target() 匹配目標對象
- @target() 匹配目標對象的註解
- within() 匹配實例的類型
- @within() 匹配實例的註解
- @annotation() 匹配註解
- bean() 匹配bean id
下面舉例說明切點表達式語言。
// 切點爲執行com.example.Apple.eat方法,返回值任意,參數任意。
execution(* com.example.Apple.eat(..))
// within表示只匹配com.example.*下的任意方法。用了and連接符號。
execution(* com.example.Apple.eat(..) and within(com.example.*))
// bean表示匹配相應的bean
execution(* com.example.Apple.eat(..) and bean(apple))
下面的例子演示了切點的各種修飾方式。
<aop:config>
<!--定義切面,test是事先定義好的一個bean-->
<aop:aspect ref="test">
<!--定義切點-->
<aop:pointcup id="apple-eat" expression="execution(* com.example.Apple.eat(..))"/>
<!--在切點之前調用test.beforeEat-->
<aop:before pointcut-ref="apple-eat" method="beforeEat"/>
<!--在切點執行成功之後調用-->
<aop:after-return pointcut-ref="apple-eat" method="eatSuccess"/>
<!--在切點執行失敗之後調用-->
<aop:after-throwing pointcut-ref="apple-eat" method="eatFailed"/>
<!--在切點之後調用,不管成功失敗-->
<aop:after pointcut-ref="apple-eat" method="afterEat"/>
<!--環繞通知,下面有詳細說明-->
<aop:around pointcut-ref="apple-eat" method="eatApple"/>
<!--動態增加接口,下面有詳細說明-->
<aop:declare-parents types-matching="com.example.Apple+" implement-interface="com.example.Fruit" default-impl="com.example.FruitImpl"/>
</aop:aspect>
</aop:config>
現代化的Spring支持註解方式的切面。下面請看例子。
// 定義切面
@Aspect
public class Test {
// 定義切點。方法中不需要寫任何代碼。
@Pointcut("execution(* com.example.Apple.eat(..))")
public void appleEat() { }
// 切面之前
@Before("appleEat()")
public void beforeEat() { }
// 切面執行成功之後
@AfterReturning("appleEat()")
public void eatSuccess() { }
// 切面執行失敗之後
@AfterThrowing("appleEat()")
public void eatFailed() { }
// 切面之後,不管成功失敗
@After("appleEat()")
public void afterEat() { }
// 環繞切面,下面有詳細說明
@Around("appleEat")
public void eatApple(ProceedingJoinPoint joinpoint) { }
// 定義傳遞參數的切點
@Pointcut("execution(* com.example.Apple.eat(..)) and args(size)")
public void appleEat2() { }
// 接收切點的參數
@Before("appleEat2")
public void beforeEat2(int size) {
// 能夠得到切點的size參數
}
}
環繞通知。它的目的是爲了解決切點前後無法通信的問題。本質是四種切點的結合體。比如我想記錄一個切點的執行時間,就需要用到環繞通知。下面是環繞通知的代碼。
public void eatApple(ProceedingJoinPoint joinPoint) {
// 在切點之前
System.out.println("before pointcut");
// 手動執行切點
joinPoint.proceed();
// 在切點之後
System.out.println("after pointcut");
}
Introduction引入,也就是動態增加新接口。它的作用就是在程序運行的過程中動態地爲一個實例增加接口。請看下面的例子。
// Introduction引入。
@DeclareParents(value="com.example.Phone+", defaultImpl="com.example.AppleWatchImpl")
public static AppleWatch appleWatch;
上面的例子中給appleWatch字段增加了一個註解,意思是讓appleWatch字段可以轉換成Phone類型,原本appleWatch是不能轉換成Phone的。