【小家Spring】Spring AOP的多種使用方式以及神一樣的AspectJ-AOP使用介紹
相關閱讀
【小家java】java5新特性(簡述十大新特性) 重要一躍
【小家java】java6新特性(簡述十大新特性) 雞肋升級
【小家java】java7新特性(簡述八大新特性) 不溫不火
【小家java】java8新特性(簡述十大新特性) 飽受讚譽
【小家java】java9新特性(簡述十大新特性) 褒貶不一
【小家java】java10新特性(簡述十大新特性) 小步迭代
【小家java】java11新特性(簡述八大新特性) 首個重磅LTS版本
什麼是AOP
AOP(Aspect-OrientedProgramming,面向方面程式設計),可以說是OOP(Object-Oriented Programing,面向物件程式設計)的補充和完善。
AOP技它利用一種稱為“橫切”的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果說“物件”是一個空心的圓柱體,其中封裝的是物件的屬性和行為;那麼面向方面程式設計的方法,就彷彿一把利刃,將這些空心圓柱體剖開,以獲得其內部的訊息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。
實現AOP的技術,主要分為兩大類:一是採用動態代理技術(典型代表為Spring AOP
相關概念
- 切面(Aspect):一個關注點的模組化,這個關注點實現可能另外橫切多個物件。事務管理是J2EE應用中一個很好的橫切關注點例子。切面用spring的 Advisor或攔截器實現。
- 連線點(Joinpoint): 程式執行過程中明確的點,如方法的呼叫或特定的異常被丟擲。
- 通知(Advice): 在特定的連線點,AOP框架執行的動作。各種型別的通知包括“around”、“before”和“throws”通知。通知型別將在下面討論。許多AOP框架包括Spring都是以攔截器做通知模型,維護一個“圍繞”連線點的攔截器鏈。Spring中定義了四個advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
- 切入點(Pointcut): 指定一個通知將被引發的一系列連線點的集合。AOP框架必須允許開發者指定切入點:例如,使用正則表示式。 Spring定義了Pointcut介面,用來組合MethodMatcher和ClassFilter,可以通過名字很清楚的理解, MethodMatcher是用來檢查目標類的方法是否可以被應用此通知,而ClassFilter是用來檢查Pointcut是否應該應用到目標類上
- 引入(Introduction): 新增方法或欄位到被通知的類。 Spring允許引入新的介面到任何被通知的物件。例如,你可以使用一個引入使任何物件實現 IsModified介面,來簡化快取。Spring中要使用Introduction, 可有通過DelegatingIntroductionInterceptor來實現通知,通過DefaultIntroductionAdvisor來配置Advice和代理類要實現的介面(使用較少)
- 目標物件(Target Object): 包含連線點的物件。也被稱作被通知或被代理物件。POJO
- AOP代理(AOP Proxy): AOP框架建立的物件,包含通知。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。
- 織入(Weaving): 組裝方面來建立一個被通知物件。這可以在編譯時完成(例如使用AspectJ編譯器),也可以在執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。
AOP概念的通俗理解
1.通知(Advice): 通知定義了切面是什麼以及何時使用。描述了切面要完成的工作和何時需要執行這個工作。
2.連線點(Joinpoint): 程式能夠應用通知的一個“時機”,這些“時機”就是連線點,例如方法被呼叫時、異常被丟擲時等等。
3.切入點(Pointcut) :通知定義了切面要發生的“故事”和時間,那麼切入點就定義了“故事”發生的地點,例如某個類或方法的名稱,Spring中允許我們方便的用正則表示式來指定
4.切面(Aspect) :通知和切入點共同組成了切面:時間、地點和要發生的“故事”
5.引入(Introduction) :引入允許我們向現有的類新增新的方法和屬性(Spring提供了一個方法注入的功能)
6.目標(Target) :即被通知的物件,如果沒有AOP,那麼它的邏輯將要交叉別的事務邏輯,有了AOP之後它可以只關注自己要做的事(AOP讓他做愛做的事)
7.代理(proxy) :應用通知的物件,詳細內容參見設計模式裡面的代理模式
8.織入(Weaving) :把切面應用到目標物件來建立新的代理物件的過程,織入一般發生在如下幾個時機:
---- (1)編譯時:當一個類檔案被編譯時進行織入,這需要特殊的編譯器才可以做的到,例如AspectJ的織入編譯器
---- (2)類載入時:使用特殊的ClassLoader在目標類被載入到程式之前增強類的位元組程式碼
----(3)執行時:切面在執行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術
Spring AOP的三種實現方式(基於Spring Boot)
一、基於XML配置的Spring AOP
現在都是spring boot的時代了,因此基於xml配置的例子,本文不做介紹了,有需要的可以自己去找其餘博文閱讀
二、基於ProxyFactoryBean,編碼的方式來實現
導包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
有如下包即可正常工作了
為何Spring自己實現了AOP,還需要匯入org.aspectj:aspectjweaver的包呢?
官網解釋的原因如下:
- 原因一:spring確實有自己的AOP。功能已經基本夠用了,除非你的要在介面上動態代理或者方法攔截精確到getter和setter。這些都是寫奇葩的需求Spring做不到,但一般不使用。
- 原因二:1、如果使用xml方式,不需要任何額外的jar包。2、如果使用@Aspect的註解方式。你就可以在類上直接一個@Aspect就搞定,不用費事在xml裡配了。但是這需要額外的jar包( aspectjweaver.jar)。因為spring直接使用AspectJ的註解功能,注意只是使用了它 的註解功能而已。並不是核心功能 !!!
注意到文件上還有一句很有意思的話:文件說到 是選擇spring AOP還是使用full aspectJ?什麼是full aspectJ?如果你使用"full aspectJ"。就是說你可以實現基於介面的動態代理,等等強大的功能。而不僅僅是aspectj的 注-解-功-能 !!!
如果用full AspectJ。比如說Load-Time Weaving的方式 還 需要額外的jar包 spring-instrument.jar。。。現在明白了吧~~~ 具體詳情,後面在講述AspectJ裡可以看見~~
基本類如下:
// A類:
@Service
public class AServiceImpl implements AService {
@Override
public void sayHelloA() {
System.out.println("hello A");
}
}
// B類:
@Service
public class BServiceImpl implements BService {
@Override
public void sayHelloB() {
System.out.println("hello B");
}
}
// C類:
@Service
public class CServiceImpl implements CService {
@Override
public void sayHelloC() {
System.out.println("hello C");
}
}
通過實現介面的方式編寫的通知類
/**
* 在方法之前、之後 列印輸出日誌
*
* @author [email protected]
* @description
* @date 2018-10-29 17:42
*/
@Component //通知元件交給容器管理
public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice, MethodInterceptor {
//MethodBeforeAdvice的方法
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("MethodBeforeAdvice...before...");
}
//AfterReturningAdvice的方法
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("AfterReturningAdvice...afterReturning...");
}
//MethodInterceptor的方法
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("MethodInterceptor...invoke...start");
Object proceed = invocation.proceed();
System.out.println("MethodInterceptor...invoke...end");
return proceed;
}
}
實現介面 MethodBeforeAdvice該攔截器會在呼叫方法前執行
實現介面 AfterReturningAdvice該攔截器會在呼叫方法後執行
實現介面 MethodInterceptor該攔截器會在呼叫方法前後都執行,實現環繞效果
然後就是配置了,其中最重要的類為ProxyFactoryBean、BeanNameAutoProxyCreator、AspectJExpressionPointcutAdvisor等等代理類,能達到強大的效果。這種一般都是spring時代基於xml的書寫方式,因此這裡不做詳細講解,SpringBoot時代,建議使用優雅的註解的風格編寫,但本文提供一個參考博文:
Spring AOP之ProxyFactoryBean與BeanNameAutoProxyCreator
三、基於註解方式@AspectJ實現AOP
PS:其實springboot此配置是預設開啟的,所以根本可以不用管了,在Springboot中使用過註解配置方式的人會問是否需要在程式主類中增加@EnableAspectJAutoProxy來啟用,實際並不需要。看下面關於AOP的預設配置屬性,其中spring.aop.auto屬性預設是開啟的,也就是說只要引入了AOP依賴後,其實預設已經增加了@EnableAspectJAutoProxy。
截圖看看Boot的一些預設配置:
不過後面會有點奇怪的問題,springboot中,不管這個項是否設定為true或者false,都不會跟以前spring專案中,如果沒有設定為true,當代理類沒有繼承介面,啟動專案的時候會報錯。而springboot專案中,會自動轉換成使用CGLIB進行動態代理,其中原理是怎麼實現,就沒有去看底層程式碼了,估計底層進行了改造吧!
切面:
/**
* @author [email protected]
* @description
* @date 2018-10-30 11:32
*/
@Component //這個元件一定得加入到容器才行
@Aspect
public class SpringLogAspect {
//定義一個切入點:指定哪些方法可以被切入(如果是別的類需要使用 請用該方法的全類名)
@Pointcut("execution(* com.fsx.run.service..*.*(..))")
public void pointCut() {
}
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("AOP Before Advice...");
}
@After("pointCut()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("AOP After Advice...");
}
@AfterReturning(pointcut = "pointCut()", returning = "returnVal")
public void afterReturn(JoinPoint joinPoint, Object returnVal) {
System.out.println("AOP AfterReturning Advice:" + returnVal);
}
@AfterThrowing(pointcut = "pointCut()", throwing = "error")
public void afterThrowing(JoinPoint joinPoint, Throwable error) {
System.out.println("AOP AfterThrowing Advice..." + error);
System.out.println("AfterThrowing...");
}
// 環繞通知:此處有一個坑,當AfterReturning和Around共存時,AfterReturning是獲取不到返回值的
//@Around("pointCut()")
//public void around(ProceedingJoinPoint pjp) {
// System.out.println("AOP Aronud before...");
// try {
// pjp.proceed();
// } catch (Throwable e) {
// e.printStackTrace();
// }
// System.out.println("AOP Aronud after...");
//}
}
idea很智慧:如果切中了,會有這個小圖示的
controller層這麼測試:
@Autowired
AService aService;
@Autowired
BService bService;
@Override
public Object testDemo(String str) {
aService.sayHelloA();
bService.sayHelloB();
return str + "succuss~";
}
訪問,控制檯列印結果如下:
AOP Before Advice...
hello A
AOP After Advice...
AOP AfterReturning Advice:AServiceImpl
AOP Before Advice...
hello B
AOP After Advice...
AOP AfterReturning Advice:BServiceImpl
此處有一個坑,當AfterReturning和Around共存時,AfterReturning是獲取不到返回值的。當然,如果你的方法本來就沒有返回值,那肯定也是null咯
關於@Pointcut切點表示式的書寫,請參見:
小家Spring】Spring AOP中@Pointcut切入點表示式最全面使用介紹
神一樣的AspectJ --> AOP的領跑者
AspectJ 可以成為是AOP的鼻祖,規範的制定者。】
因此本文先進行一個簡單案例的演示,然後引出AOP中一些晦澀難懂的抽象概念,放心,通過本篇部落格,我們將會非常輕鬆地理解並掌握它們。編寫一個HelloWord的類,然後利用AspectJ技術切入該類的執行過程。
Hello類:
public class HelloWord {
public void sayHello(){
System.out.println("hello world !");
}
public static void main(String args[]){
HelloWord helloWord =new HelloWord();
helloWord.sayHello();
}
}
編寫AspectJ類,注意關鍵字為aspect(MyAspectJDemo.aj,其中aj為AspectJ的字尾),含義與class相同,即定義一個AspectJ的類(注意:字尾名是.aj)
public aspect MyAspectJDemo {
/**
* 定義切點,日誌記錄切點
*/
pointcut recordLog():call(* HelloWord.sayHello(..));
/**
* 定義切點,許可權驗證(實際開發中日誌和許可權一般會放在不同的切面中,這裡僅為方便演示)
*/
pointcut authCheck():call(* HelloWord.sayHello(..));
/**
* 定義前置通知!
*/
before():authCheck(){
System.out.println("sayHello方法執行前驗證許可權");
}
/**
* 定義後置通知
*/
after():recordLog(){
System.out.println("sayHello方法執行後記錄日誌");
}
}
這樣直接執行HelloWorld的main,是不會有AOP效果的。因為我們還需要配置:改變編譯器重新編譯後再執行,具體參考:
AspectJ——簡介以及在IntelliJ IDEA下的配置
配置好後執行結果如下:
AspectJ是一個java實現的AOP框架,它能夠對java程式碼進行AOP編譯(一般在編譯期進行),讓java程式碼具有AspectJ的AOP功能(當然需要特殊的編譯器),可以這樣說AspectJ是目前實現AOP框架中最成熟,功能最豐富的語言,更幸運的是,AspectJ與java程式完全相容,幾乎是無縫關聯,因此對於有java程式設計基礎的工程師,上手和使用都非常容易。
AspectJ的織入方式及其原理概要
對於織入這個概念,可以簡單理解為aspect(切面)應用到目標函式(類)的過程。對於這個過程,一般分為動態織入和靜態織入,動態織入的方式是在執行時動態將要增強的程式碼織入到目標類中,這樣往往是通過動態代理技術完成的,如Java JDK的動態代理(Proxy,底層通過反射實現)或者CGLIB的動態代理(底層通過繼承實現),Spring AOP採用的就是基於執行時增強的代理技術,這點另一篇博文已經有分析了,這裡主要重點分析一下靜態織入。
ApectJ採用的就是靜態織入的方式。ApectJ主要採用的是編譯期織入,在這個期間使用AspectJ的acj編譯器(類似javac)把aspect類編譯成class位元組碼後,在java目標類編譯時織入,即先編譯aspect類再編譯目標類。
關於ajc編譯器,是一種能夠識別aspect語法的編譯器,它是採用java語言編寫的,由於javac並不能識別aspect語法,便有了ajc編譯器,注意ajc編譯器也可編譯java檔案。為了更直觀瞭解aspect的織入方式,我們開啟前面案例中已編譯完成的HelloWord.class檔案,反編譯後的java程式碼如下:
//編譯後織入aspect類的HelloWord位元組碼反編譯類
public class HelloWord {
public HelloWord() {
}
public void sayHello() {
System.out.println("hello world !");
}
public static void main(String[] args) {
HelloWord helloWord = new HelloWord();
HelloWord var10000 = helloWord;
try {
//MyAspectJDemo 切面類的前置通知織入
MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541();
//目標類函式的呼叫
var10000.sayHello();
} catch (Throwable var3) {
MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
throw var3;
}
//MyAspectJDemo 切面類的後置通知織入
MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
}
}
顯然AspectJ的織入原理已很明朗了,當然除了編譯期織入,還存在連結期(編譯後)織入,即將aspect類和java目標類同時編譯成位元組碼檔案後,再進行織入處理,這種方式比較有助於已編譯好的第三方jar和Class檔案進行織入操作,由於這不是本篇的重點,暫且不過多分析,掌握以上AspectJ知識點就足以協助理解Spring AOP了
Spring AOP的優點
Spring AOP 與ApectJ 的目的一致,都是為了統一處理橫切業務,但與AspectJ不同的是,Spring AOP 並不嘗試提供完整的AOP功能(即使它完全可以實現),Spring AOP 更注重的是與Spring IOC容器的結合,並結合該優勢來解決橫切業務的問題,因此在AOP的功能完善方面,相對來說AspectJ具有更大的優勢
同時,Spring注意到AspectJ在AOP的實現方式上依賴於特殊編譯器(ajc編譯器),因此Spring很機智迴避了這點,轉向採用動態代理技術的實現原理來構建Spring AOP的內部機制(動態織入),這是與AspectJ(靜態織入)最根本的區別。
在AspectJ 1.5後,引入@Aspect形式的註解風格的開發,Spring也非常快地跟進了這種方式,因此Spring 2.0後便使用了與AspectJ一樣的註解。請注意,Spring 只是使用了與 AspectJ 5 一樣的註解,但仍然沒有使用 AspectJ 的編譯器,底層依是動態代理技術的實現,因此並不依賴於 AspectJ 的編譯器
最後
博主希望通過本文,讓讀者能夠了解到Spring AOP和AspectJ的區別與聯絡。