AOP(Aspect Oriented Programming)面向切面程式設計,一種程式設計正規化,指導開發者如何組織程式結構。
OOP(Object Oriented Programming)物件導向程式設計
我們都知道OOP是一種程式設計思想,那麼AOP也是一種程式設計思想,程式設計思想主要的內容就是指導程式設計師該如何編寫程式,所以它們兩個是不同的程式設計正規化
。
作用:在不驚動原始設計的基礎上為其進行功能增強。
為了能更好的理解AOP的相關概念,觀察下面的類:BookDaoImpl
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//記錄程式當前執行執行(開始時間)
Long startTime = System.currentTimeMillis();
//業務執行萬次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//記錄程式當前執行時間(結束時間)
Long endTime = System.currentTimeMillis();
//計算時間差
Long totalTime = endTime-startTime;
//輸出資訊
System.out.println("執行萬次消耗時間:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
程式碼的內容相信大家都能夠讀懂,對於save
方法中有計算萬次執行消耗的時間。
當在App類中從容器中獲取bookDao物件後,分別執行其save
,delete
,update
和select
方法後會有如下的列印結果:
這個時候,我們就應該有些疑問?
對於計算萬次執行消耗的時間只有save方法有,為什麼delete和update方法也會有呢?
delete和update方法有,那什麼select方法為什麼又沒有呢?
這個其實就使用了Spring的AOP,在不驚動(改動)原有設計(程式碼)的前提下,想給誰新增功能就給誰新增。這個也就是Spring的理念:
無入侵式/無侵入式
說了這麼多,Spring到底是如何實現的呢?
(1)前面一直在強調,Spring的AOP是對一個類的方法在不進行任何修改的前提下實現增強。對於上面的BookServiceImpl中有save
,update
,delete
和select
方法,這些方法我們給起了一個名字叫連線點
(2)在BookServiceImpl的四個方法中,update
和delete
沒有計算萬次執行消耗時間,但是在執行的時候已經有該功能,那也就是說update
和delete
方法都已經被增強,所以對於需要增強的方法我們給起了一個名字叫切入點
(3)執行BookServiceImpl的update和delete方法的時候都被新增了一個計算萬次執行消耗時間的功能,將這個功能抽取到一個方法中,換句話說就是存放共性功能的方法,我們給起了個名字叫通知
(4)通知是要增強的內容,會有多個,切入點是需要被增強的方法,也會有多個,那哪個切入點需要新增哪個通知,就需要提前將它們之間的關係描述清楚,那麼對於通知和切入點之間的關係描述,我們給起了個名字叫切面
(5)通知是一個方法,方法不能獨立存在需要被寫在一個類中,這個類我們也給起了個名字叫通知類
至此AOP中的核心概念就已經介紹完了,總結下:
連線點(JoinPoint):程式執行過程中的任意位置,粒度為執行方法、丟擲異常、設定變數等
這個概念很大,在SpringAOP中,一般理解為方法的執行
切入點(Pointcut):匹配連線點的式子
在SpringAOP中,一個切入點可以描述一個具體方法,也可也匹配多個方法
一個具體的方法:如dao包下的BookDao介面中的無形參無返回值的save方法
匹配多個方法:所有的save方法,所有的get開頭的方法,所有以Dao結尾的介面中的任意方法,所有帶有一個引數的方法
連線點範圍要比切入點範圍大,是切入點的方法也一定是連線點,但是是連線點的方法就不一定要被增強,所以可能不是切入點。
通知(Advice):在切入點處執行的操作,也就是共性功能
在SpringAOP中,功能最終以方法的形式呈現
通知類:定義通知的類
切面(Aspect):描述通知與切入點的對應關係。
案例設定:測算介面執行效率,但是這個稍微複雜了點,我們對其進行簡化。
簡化設定:在方法執行前輸出當前系統時間。
需求明確後,具體該如何實現,有哪些步驟?
1.匯入座標(pom.xml)
2.製作連線點(原始操作,Dao介面與實現類)
3.製作共性功能(通知類與通知)
4.定義切入點
5.繫結切入點與通知關係(切面)
建立一個Maven專案
pom.xml新增Spring依賴
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
新增BookDao和BookDaoImpl類
public interface BookDao {
public void save();
public void update();
}
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
}
建立Spring的設定類
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}
編寫App執行類
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}
最終建立好的專案結構如下:
說明:
目前列印save方法的時候,因為方法中有列印系統時間,所以執行的時候是可以看到系統時間
對於update方法來說,就沒有該功能
我們要使用SpringAOP的方式在不改變update方法的前提下讓其具有列印系統時間的功能。
pom.xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
因為spring-context
中已經匯入了spring-aop
,所以不需要再單獨匯入spring-aop
匯入AspectJ的jar包,AspectJ是AOP思想的一個具體實現,Spring有自己的AOP實現,但是相比於AspectJ來說比較麻煩,所以我們直接採用Spring整合ApsectJ的方式進行AOP開發。
環境準備的時候,BookDaoImpl已經準備好,不需要做任何修改
通知就是將共性功能抽取出來後形成的方法,共性功能指的就是當前系統時間的列印。
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
類名和方法名沒有要求,可以任意。
BookDaoImpl中有兩個方法,分別是save和update,我們要增強的是update方法,該如何定義呢?
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}
說明:
切入點定義依託一個不具有實際意義的方法進行,即無引數、無返回值、方法體無實際邏輯。
execution及後面編寫的內容,後面在介紹,這裡只是介紹下用法。
切面是用來描述通知和切入點之間的關係,如何進行關係的繫結?
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
繫結切入點與通知關係,並指定通知新增到原始連線點的具體執行位置
說明:@Before翻譯過來是之前,也就是說通知會在切入點方法執行之前執行,除此之前還有其他四種型別,後面會介紹。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
看到在執行update方法之前列印了系統時間戳,說明對原始方法進行了增強,AOP程式設計成功。
名稱 | @EnableAspectJAutoProxy |
---|---|
型別 | 設定類註解 |
位置 | 設定類定義上方 |
作用 | 開啟註解格式AOP功能 |
名稱 | @Aspect |
---|---|
型別 | 類註解 |
位置 | 切面類定義上方 |
作用 | 設定當前類為AOP切面類 |
名稱 | @Pointcut |
---|---|
型別 | 方法註解 |
位置 | 切入點方法定義上方 |
作用 | 設定切入點方法 |
屬性 | value(預設):切入點表示式 |
名稱 | @Before |
---|---|
型別 | 方法註解 |
位置 | 通知方法定義上方 |
作用 | 設定當前通知方法與切入點之間的繫結關係,當前通知方法在原始切入點方法前執行 |
AOP的入門案例已經完成,對於剛才案例的執行過程,我們就得來分析分析,主要是兩個知識點:AOP工作流程
和AOP核心概念
。
由於AOP是基於Spring容器管理的bean做的增強,所以整個工作過程需要從Spring載入bean說起:
容器啟動就需要去載入bean,哪些類需要被載入呢?
需要被增強的類,如:BookServiceImpl
通知類,如:MyAdvice
注意此時bean物件還沒有建立成功
上面這個例子中有兩個切入點的設定,但是第一個ptx()
並沒有被使用,所以不會被讀取。
判定bean對應的類中的方法是否匹配到任意切入點
注意第1步在容器啟動的時候,bean物件還沒有被建立成功。
要被範例化bean物件的類中的方法和切入點進行匹配
匹配失敗,建立原始物件,如UserDao
匹配失敗說明不需要增強,直接呼叫原始物件的方法即可。
匹配成功,建立原始物件(目標物件)的代理物件,如:BookDao
匹配成功說明需要對其進行增強
對哪個類做增強,這個類對應的物件就叫做目標物件
因為要對目標物件進行功能增強,而採用的技術是動態代理,所以會為其建立一個代理物件
最終執行的是代理物件的方法,在該方法中會對原始方法進行功能增強
獲取的bean是原始物件時,呼叫方法並執行,完成操作
獲取的bean是代理物件時,根據代理物件的執行模式執行原始方法與增強的內容,完成操作
為了驗證IOC容器中建立的物件和我們剛才所說的結論是否一致,首先先把結論理出來:
如果目標物件中的方法會被增強,那麼容器中將存入的是目標物件的代理物件
如果目標物件中的方法不被增強,那麼容器中將存入的是目標物件本身。
1.要執行的方法,不被定義的切入點包含,即不要增強,列印當前類的getClass()方法
2.要執行的方法,被定義的切入點包含,即要增強,列印出當前類的getClass()方法
3.觀察兩次列印的結果
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
因為定義的切入點中,被修改成update1
,所以BookDao中的update方法在執行的時候,就不會被增強,
所以容器中的物件應該是目標物件本身。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update1())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
因為定義的切入點中,被修改成update
,所以BookDao中的update方法在執行的時候,就會被增強,
所以容器中的物件應該是目標物件的代理物件
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
至此對於剛才的結論,我們就得到了驗證,這塊大家需要注意的是:
不能直接列印物件,從上面兩次結果中可以看出,直接列印物件走的是物件的toString方法,不管是不是代理物件列印的結果都是一樣的,原因是內部對toString方法進行了重寫。
在上面介紹AOP的工作流程中,我們提到了兩個核心概念,分別是:
目標物件(Target):原始功能去掉共性功能對應的類產生的物件,這種物件是無法直接完成最終工作的
代理(Proxy):目標物件無法直接完成工作,需要對其進行功能回填,通過原始物件的代理物件實現
上面這兩個概念比較抽象,簡單來說,
目標物件就是要增強的類[如:BookServiceImpl類]對應的物件,也叫原始物件,不能說它不能執行,只能說它在執行的過程中對於要增強的內容是缺失的。
SpringAOP是在不改變原有設計(程式碼)的前提下對其進行增強的,它的底層採用的是代理模式實現的,所以要對原始物件進行增強,就需要對原始物件建立代理物件,在代理物件中的方法把通知[如:MyAdvice中的method方法]內容加進去,就實現了增強,這就是我們所說的代理(Proxy)。
本文來自部落格園,作者:|舊市拾荒|,轉載請註明原文連結:https://www.cnblogs.com/xiaoyh/p/16412308.html