Spring系列(四) 面向切面的Spring
除了IOC外, AOP是Spring的另一個核心. Spring利用AOP解決應用橫切關注點(cross-cutting concern)與業務邏輯的分離, 目的是解耦合. 橫切關注點是指散佈於程式碼多處的同一種功能, 比如日誌, 事務, 安全, 快取等.
AOP程式設計的基本概念
在OOP中, 如果要複用同一功能, 一般的做法是使用繼承或委託. 繼承容易導致脆弱的物件體系, 而委託實現起來比較麻煩, 需要對委託物件進行復雜呼叫. AOP提供了另外一種思路, 使用AOP我們仍然可以在一個地方定義功能, 並通過宣告的方式告知何處以哪種方式使用這個功能. 這樣我們既可以對功能做統一管理和維護, 同時也簡化了業務邏輯模組, 使其更關注自身的業務邏輯. 此外, AOP還可以將新的功能行為新增到現有物件.
Spring中AOP的術語
- 切面(Aspect): 切面定義了橫切關注點的功能以及使用該功能的宣告. 它包含了另外兩個術語, 通知(Advice, 功能邏輯程式碼)和切點(Pointcut,宣告). 切面定義了它是什麼(what), 以及在何時何處(when,where)完成其功能.
- 通知(Advice): 通知定義了切面的具體功能, 以及何時使用.
when,何時使用? 前置(Before), 後置(After), 返回(After-returning), 異常(After-throwing), 環繞(Around)
- 切點(Pointcut): 定義了切面定義的功能在哪裡(Where)發生作用, 看起來就像從某個點把切面插入進去一樣. 切點應該屬於連線點中的一個或多個.
- 連線點(Join point): 定義了程式執行過程中可以應用切面的具體時機, 比如方法呼叫前, 呼叫後, 結果返回時, 異常丟擲時等, 通常某個具體切面只會選擇其中一個或幾個連線點作為切點.
- 引入(Introduction): 為現有的類新增新的方法或屬性叫引入.
- 織入(Weaving): 織入是把切面應用到目標物件並建立新代理物件的過程.
織入的方式有三種:
- 編譯期: 需要特殊的編譯器支援, 如AspectJ的織入編譯器
- 類載入期: 需要特殊的類載入器ClassLoader
- 執行時: Spring AOP 使用該方式織入. AOP容器為物件動態建立一個代理物件.
Spring 對 AOP的支援
Spring對AOP的支援很多借鑑了AspectJ的方式.
Spring支援四種方式的織入:
- 基於代理的經典AOP; (方式太老舊, 不建議使用)
- 純POJO切面;(需要XML配置)
- @AspectJ 註解驅動的切面; (沒啥說的,很好用)
- 注入式AspectJ切面;
- 前三種都是基於動態代理實現, 因此Spring對AOP的支援侷限於方法攔截. 如果前三種滿足不了需求(比如攔截構造器方法或者欄位修改), 可以使用第四種.
- 與AspectJ不同, Spring的切面就是Java類, Spring使用執行時動態代理, 而AspectJ需要學習特殊的語法以支援特殊的編譯器織入.
通過切點來選擇連線點
Spring 借鑑了AspectJ的切點表示式語言. 如前所述, Spring基於動態代理,只能在方法上攔截, 所以Spring只支援這個層面的表示式來定義.
spring支援的AspectJ指示器如下, 其中execution來執行匹配, 其他均為限制匹配的.
切點表示式更多使用可以參考官方文件
- spring新增了個bean()指示器
使用註解建立切面
一. 定義切面類, 並用 @Aspect
註解, 該註釋用來標記這個類是個切面
二. 定義切面的方法(what), 並使用註解標記方法(when), 可用的註解: @Before
,@After
,@AfterReturning
,@AfterThrowing
,@Around
(功能最強大,後面將單獨使用這種通知)
一,二步完成後的程式碼:
@Aspect
public class Audience{
@Before("execution(** com.xlx.Performance.perform(...))")
public void silencephone(){
System.out.println("silencephone");
}
@Before("execution(** com.xlx.Performance.perform(...))")
public void takeSeats(){
System.out.println("takeSeats");
}
@AfterReturning("execution(** com.xlx.Performance.perform(...))")
public void applause(){
System.out.println("applause");
}
@AfterThrowing("execution(** com.xlx.Performance.perform(...))")
public void refund(){
System.out.println("refund");
}
}
上面的程式碼中切面表示式被重複定義了四次, 無論如何這已經是重複程式碼了, 下一步優化一下.
三. 使用註解@Pointcut
定義切點
@Aspect
public class Audience{
//定義切點並修改其他方法重用該切點
@Pointcut("execution(** com.xlx.Performance.perform(...))")
public void performance(){
}
@Before("performance()")
public void silencephone(){
System.out.println("silencephone");
}
@Before("performance()")
public void takeSeats(){
System.out.println("takeSeats");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("applause");
}
@AfterThrowing("performance()")
public void refund(){
System.out.println("refund");
}
}
@Aspect
註解的類依然是個普通java類, 它可以被裝配為bean
@Bean
public Audience getAudience(){
return new Audience();
}
四. 使用@EnableAspectJAutoProxy
註解啟用自動代理功能, 如果是XML Config ,對應的節點是<aop:aspectj-autoproxy />
@Configuration
@ComponentScan // 包掃描
@EnableAspenctJAutoProxy // 啟動自動代理
public class MyConfig{
// 如果Audience上加了@Component就不需要這個程式碼了
@Bean
public Audience getAudience(){
return new Audience();
}
}
五. 使用環繞通知@Around
, 環繞通知同時兼具了@Before
,@After
... 等註解的方法的功能, 下面程式碼演示了這種能力. 如可以使用它記錄方法執行時長.
@Aspect
public class Audience{
//定義切點並修改其他方法重用該切點
@Pointcut("execution(** com.xlx.Performance.perform(...))")
public void performance(){
}
@Around("performance()")
public void silencephone(ProcdedingJoinPoint jp){
System.out.println("silencephone");
System.out.println("takeSeats");
try{
// 如果不是刻意為之, 一定要記得呼叫jp.proceed();否則實際的方法Performance.perform()將會阻塞
jp.proceed();
System.out.println("applause");
}catch(Exception e){
System.out.println("refund");
}
}
}
六. 引數傳遞 , 在切點表示式中使用args(paramName)
結合切點方法可以為切面方法傳遞引數
@Aspect
public class Audience{
//定義切點並修改其他方法重用該切點
@Pointcut("execution(** com.xlx.Performance.perform(int) && args(actornum)))")
public void performance(int actornum){
}
@Before("performance(actornum)")
public void countActor(int actornum){
System.out.println("countActor"+actornum);
}
}
通過註解引用新功能
除了攔截物件已有的方法呼叫, 還可以使用AOP來為物件新增新的屬性和行為(引入). 其實現就是通過動態代理生成代理類來實現.
一. 定義要新增的功能介面
public interface Encoreable{}
二. 定義切面(引入) @Aspect
註解切面類. @DeclareParents
註解功能介面靜態變數
@Aspect
public class EncoreableIntroducer{
// 可以解釋為: 為Performace的所有子類引入介面Encoreable, 並使用預設實現類DefaultEncoreableImpl
@DeclareParents(value="xlx.Performace+",defaultImpl=DefaultEncoreableImpl.class)
public static Encoreable encoreable;
}
基於XML配置的切面
如果沒有辦法為類添加註解, 比如沒有原始碼, 那就不得不使用xml來配置了.
- 示例1
<aop:config>
<aop:aspect ref="aspectBean">
<aop:pointcut id="pcId" expression="execution(** com.xlx.Performance.perform(int) and args(actornum)))" />
<aop:before pointcut-ref="pcId" method="count" />
</aop:aspect>
</aop:config>
- 示例2
<aop:config>
<aop:aspect>
<aop:declare-parents type-matching="xlx.Performace+" implement-interface="xlx.Encoreable" delegate-ref="defaultImpl" />
</aop:aspect>
</aop:config>
AspectJ 注入
使用AspectJ注入的方式可以解決使用動態代理無法解決的問題(應該比較少見,大多應用使用Spring AOP就可以實現了), 但需要使用AspectJ的特殊語法. 定義好的類需要用xml配置為bean, 使用factory-method="aspectOf"
屬性來制定bean的產生方式.
<bean factory-method="aspectOf" class="...ClassName">
<property name="other" ref="otherref"/>
</bean>