再學習之Spring(面向切面程式設計).
一、概念
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加自定義註解就可以攔截使用這個註解的方法。
@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>