這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程。正則表達式
AOP是Spring提供的關鍵特性之一。AOP即面向切面編程,是OOP編程的有效補充。使用AOP技術,能夠將一些系統性相關的編程工做,獨立提取出來,獨立實現,而後經過切面切入進系統。從而避免了在業務邏輯的代碼中混入不少的系統相關的邏輯——好比權限管理,事物管理,日誌記錄等等。這些系統性的編程工做均可以獨立編碼實現,而後經過AOP技術切入進系統便可。從而達到了 將不一樣的關注點分離出來的效果。spring
切面必需要完成的工做即稱爲通知。通知定義了切面是什麼以及何時實用。編程
spring切面能夠實用的5種類型通知:bash
咱們的應用可能有數以千計的時機應用通知。這些時機被稱 爲鏈接點。鏈接點是在應用執行過程當中可以插入切面的一個點。這個點能夠是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼能夠利用這些點插入到應用的正常流程之中,並添加新的行爲。測試
切點定義了從何處切入。切點的定義會匹配通知所要織入的一個或多個鏈接點。一般使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。ui
切面是通知和切點的結合。通知和切點共同定義了切面的所有內容----它是什麼,在什麼時候和何處完成其功能。編碼
引入容許咱們向現有的類添加新方法或屬性。spa
織入是把切面應用到目標對象並建立新的代理對象的過程。切面在指定的鏈接點被織入到目標對象中。代理
Spring提供了4種類型的AOP支持:日誌
前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理基礎之上,所以,Spring對AOP的支持侷限於方法攔截。
在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。
首先定義一個接口來做爲切點:
public interface Performance {
void perform();
}
複製代碼
假設咱們想編寫Performance的perform()方法觸發的通 知。下面的表達式可以設置當perform()方法執行時觸發通知的調用。
execution(* com.wtj.springlearn.aop.Performance.perform(..))
複製代碼
execution()指示器選擇Performance的perform()方法。方法表達式以「*」號開始,代表了不關心方法返回值的類型。而後指定了全限定類名和方法名。對於方法參數列表,使用兩個點號(..)代表切點要選擇任意的perform()方法,不管該方法的入參是什麼。
若是咱們須要設置切點匹配com.wtj.springlearn.aop包,可使用within()來限定匹配。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) && within(com.wtj.springlearn.aop.*)
複製代碼
表示com.wtj.springlearn.aop包下任意類的方法被調用時。
使用「&&」操做符把execution()和within()指示器鏈接在一塊兒造成與(and)關係(切點必須匹配全部的指示器)。相似地,咱們可使用「||」操做符來標識或(or)關係,而使用「!」操做符來標識非(not)操做。
由於「&」在XML中有特殊含義,因此在Spring的XML配置裏面描述切點時,咱們可使用and來代替「&&」。一樣,or和not能夠分別用來代替「||」和「!」。
還可使用bean的ID來標識bean。bean()使用bean ID或bean名稱做爲參數來限制切點只匹配特定的bean。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) && bean('book')
複製代碼
這裏表示執行perform方法時通知,可是隻限於bean的ID爲book。
本篇主要介紹註解方式的切面定義方式
經過@Aspect進行標註,表示該Audience不只是一個POJO仍是一個切面。類中的方法表示了切面的具體行爲。
Spring提供了五種註解來定義通知時間:
首先建立一個切面:
@Aspect
public class Audience {
//表演前 手機靜音
@Before("execution(* com.wtj.springlearn.aop.Performance.perform(..))")
public void silenceCellPhone(){
System.out.println("silence Cell Phone");
}
//表演成功-clap
@AfterReturning("execution(** com.wtj.springlearn.aop.Performance.perform(..))")
public void clap(){
System.out.println("clap clap clap");
}
//表演失敗-退款
@AfterThrowing("execution(** com.wtj.springlearn.aop.Performance.perform(..))")
public void refund(){
System.out.println("refund refund refund");
}
}
複製代碼
Performance的實現類:
@Component
public class PerformanceImpl implements Performance {
public void perform() {
System.out.println("the perform is good");
}
}
複製代碼
最後還須要開啓自動代理功能,經過JavaConfig進行配置,使用@EnableAspectJAutoProxy
標籤開啓。
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AudienceConfig {
@Bean
public Audience audience(){
return new Audience();
}
}
複製代碼
最後經過一個簡單的測試用例就能夠來驗證了。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AudienceConfig.class)
public class PerformanceTest {
@Autowired
private Performance performance;
@Test
public void perTest(){
performance.perform();
}
}
複製代碼
打印結果:
silence Cell Phone
the perform is good
clap clap clap
複製代碼
你會發現上面切面的方法中,切點的聲明都是同樣的,這種狀況下可使用@Pointcut
註解來定義切點。
@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(..))")
public void per(){};
//表演前 手機靜音
@Before("per()")
public void silenceCellPhone(){
System.out.println("silence Cell Phone");
}
複製代碼
per()方法自己並不重要,該方法只是一個標識,供@PointCut註解依附。
環繞通知是最爲強大的通知類型。它可以讓你所編寫的邏輯將被通知的目標方法徹底包裝起來。實際上就像在一個通知方法中同時編寫前置通知和後置通知。
重寫Audience切面,使用環繞通知替代以前多個不一樣的前置通知和後置通知。
@Around("per()")
public void watch(ProceedingJoinPoint point) throws Throwable {
try{
System.out.println("silence Cell Phone");
point.proceed();
System.out.println("clap clap clap");
}catch (Exception e){
System.out.println("refund refund refund");
}
}
複製代碼
首先注意到的多是它接受ProceedingJoinPoint做爲參數。這個對象是必需要有的,由於你要在通知中經過它來調用被通知的方法。通知方法中能夠作任何的事情,當要將控制權交給被通知的方法時,它須要調用ProceedingJoinPoint的proceed()方法。
若是不調proceed()這個方法的話,那麼你的通知實際上會阻塞對被通知方法的調用。一樣的,你也能夠調用屢次。
上面咱們建立的切面都很簡單,沒有任何參數。那麼切面能訪問和使用傳遞給被通知方法的參數麼?
Performance中新增方法:
void perform(String name);
複製代碼
實現類:
public void perform(String name) {
System.out.println("下面請 "+name+" 開始他的表演");
}
複製代碼
修改Audience中的切點和切面
@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(String)) && args(name)")
public void per(String name){};
@Around("per(name)")
public void toWatch(ProceedingJoinPoint point,String name) throws Throwable {
try{
point.proceed();
System.out.println(name +" 上場啦");
System.out.println(name +" 演出結束");
}catch (Exception e){
System.out.println("refund refund refund");
}
}
複製代碼
表達式args(name)
限定符,它表示傳遞給perform(String name)方法的String類型參數也會傳到通知中去,參數名與切點中的參數名相同。perform(String)
指明瞭傳入參數的類型。
而後在@Around
註解中指明切點與參數名,這樣就完成了參數轉移。
最後修改一下測試用例就完成了
@Test
public void perTest(){
performance.perform("渣渣輝");
}
複製代碼
打印輸出:
下面請 渣渣輝 開始他的表演
渣渣輝 上場啦
渣渣輝 演出結束
複製代碼
若是咱們想在一個類上新增方法,一般狀況下咱們會怎麼作呢?最簡單的辦法就是在此目標類上增長此方法,可是若是原目標類很是複雜,動一發而牽全身。而且有些時候咱們是沒有目標類的源碼的,哪這個時候怎麼辦呢?
咱們能夠爲須要添加的方法創建一個類,而後建一個代理類,同時代理該類和目標類。用一個圖來表示
當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其餘對象。
仍是上面的例子,假設咱們須要讓表演者跳起來。
新建Jump接口以及實現類:
public interface Jump {
void duJump();
}
複製代碼
public class JumpImpl implements Jump {
public void duJump() {
System.out.println("do Jump");
}
}
複製代碼
而後咱們代理兩個類:
@Aspect
public class JumpIntroducer {
@DeclareParents(value = "com.wtj.springlearn.aop.Performance+",defaultImpl = JumpImpl.class)
public static Jump jump;
}
複製代碼
@DeclareParents註解由三部分組成:
經過配置將JumpIntroducer聲明
@ComponentScan
@Configuration
@EnableAspectJAutoProxy
public class JumpConfig {
@Bean
public JumpIntroducer jumpIntroducer(){
return new JumpIntroducer();
}
}
複製代碼
或者你也能夠在JumpIntroducer類上加入@Component
註解,就能夠不用聲明bean了。
最後經過測試用例進行測試:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JumpConfig.class)
public class PerformanceTest {
@Autowired
private Performance performance;
@Test
public void perTest(){
//類型轉換
Jump jump = (Jump) performance;
jump.duJump();
}
}
複製代碼
打印結果:
do Jump
複製代碼