首頁 > 軟體

Spring超詳細講解物件導向到面向切面

2022-08-03 22:04:53

前言

Object object = new Object();

世間萬物的本質都可看作類的物件,物件導向(OOP)的模式讓程式易維護、易複用、易擴充套件,而面向切面(AOP)則是物件導向的補充,讓物件的功能更加強大

對比前面的紀錄檔框架技術二者非常相似,他的特點就是在不影響業務的前提下將程式的執行情況輸出到控制檯,總體來看是起一個輔助的作用,所謂的AOP亦是如此——是在不改原有程式碼的前提下對其進行增強

一.OOP&AOP

OOP將元件視為物件,AOP將物件的切面視為“物件”

OOP&AOP讓程式通過極其簡單的方式變得更加全面、強大

AOP(Aspect Oriented Programming)面向切面程式設計、OOP(Object Oriented Programming)物件導向程式設計

OOP是一種程式設計思想,AOP也是一種程式設計思想,程式設計思想主要的內容就是指導程式設計師該如何編寫程式,兩者都是不同的程式設計正規化各有特色

二.AOP核心

通過以下一個計算程式執行時間的功能,引出AOP相關概念

@Repository
public class AImpl implements A {
    public void save() {
        //記錄程式當前執行執行(開始時間)
        Long startTime = System.currentTimeMillis();
        //業務執行萬次
        for (int i = 0;i<10000;i++) {
            System.out.println("START ...");
        }
        //記錄程式當前執行時間(結束時間)
        Long endTime = System.currentTimeMillis();
        //計算時間差
        Long totalTime = endTime-startTime;
        //輸出資訊
        System.out.println("執行萬次程式消耗時間:" + totalTime + "ms");
    }
    public void m1(){ System.out.println(" m1 ..."); }
    public void m2(){ System.out.println(" m2 ..."); }
}

(1)save,m1m2方法,這些方法我們給起了一個名字叫連線點

(2)對於需要增強的方法我們給起了一個名字叫切入點

(3)將功能抽取到一個方法中,換句話說就是存放共性功能的方法,我們給起了個名字叫通知

(4)通知是要增強的內容,會有多個,切入點是需要被增強的方法,也會有多個,那哪個切入點需要新增哪個通知,就需要提前將它們之間的關係描述清楚,那麼對於通知和切入點之間的關係描述,我們給起了個名字叫切面

(5)通知是一個方法,方法不能獨立存在需要被寫在一個類中,這個類我們也給起了個名字叫通知類

三.第一個AOP案例

1.環境準備

  • 建立一個Maven專案
  • pom.xml新增Spring依賴spring-context
  • 新增A和AImpl類
public interface A {
    public void save();
    public void m1();
}
@Repository
public class AImpl implements A {
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }
    public void m1(){
        System.out.println("book dao m1 ...");
    }
}

建立Spring的設定類

@Configuration
@ComponentScan("yu7daily")
public class Config {
}

編寫Show執行類

public class Show {
    public static void main(String[] args) {
        ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class);
        A A = ctx.getBean(A.class);
        A.save();
    }
}

2.AOP實現步驟

1.@EnableAspectJAutoProxy 開啟註解格式AOP功能

2.@Aspect設定當前類為AOP切面類

3.@Pointcut 設定切入點方法

4.@Before設定當前通知方法與切入點之間的繫結關係,當前通知方法在原始切入點方法前執行

**1.新增依賴

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開發。

2.定義介面與實現類:環境準備的時候,AImpl已經準備好,不需要做任何修改

3.定義通知類和通知

通知就是將共性功能抽取出來後形成的方法,共性功能指的就是當前系統時間的列印

public class Test {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

類名和方法名沒有要求,可以任意。

4.定義切入點

AImpl中有兩個方法,分別是save和m1,我們要增強的是m1方法,該如何定義呢?

public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

說明:

切入點定義依託一個不具有實際意義的方法進行,即無引數、無返回值、方法體無實際邏輯。

execution及後面編寫的內容

5.製作切面

切面是用來描述通知和切入點之間的關係,如何進行關係的繫結?

public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    @Before("po1()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

繫結切入點與通知關係,並指定通知新增到原始連線點的具體執行位置

說明:@Before翻譯過來是之前,也就是說通知會在切入點方法執行之前執行,除此之前還有其他四種型別

6.將通知類配給容器並標識其為切面類

@Component
@Aspect
public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    @Before("po1()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

7.開啟註解格式AOP功能

@Configuration
@ComponentScan("yu7daily")
@EnableAspectJAutoProxy
public class Config {
}

8.執行程式

public class Show {
    public static void main(String[] args) {
        ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class);
        A A = ctx.getBean(A.class);
        A.m1();
    }
}

看到在執行m1方法之前列印了系統時間戳,說明對原始方法進行了增強,AOP程式設計成功!!!

四.切入點表示式

前面的案例中,有涉及到如下內容:

對於AOP中切入點表示式,我們總共會學習三個內容,分別是語法格式、萬用字元和書寫技巧。

1.語法格式

首先我們先要明確兩個概念:

切入點:要進行增強的方法

切入點表示式:要進行增強的方法的描述方式

描述方式一:執行yu7daily.dao包下的A介面中的無引數m1方法

execution(void yu7daily.dao.A.m1())

描述方式二:執行yu7daily.dao.impl包下的AImpl類中的無引數m1方法

execution(void yu7daily.dao.impl.AImpl.m1())

因為呼叫介面方法的時候最終執行的還是其實現類的方法,所以上面兩種描述方式都是可以的。

對於切入點表示式的語法為:

切入點表示式標準格式:動作關鍵字(存取修飾符 返回值 包名.類/介面名.方法名(引數) 異常名)

execution(public User yu7daily.service.UserService.findById(int))

切入點表示式就是要找到需要增強的方法,所以它就是對一個具體方法的描述,但是方法的定義會有很多,所以如果每一個方法對應一個切入點表示式,極其複雜可以通過以下方式進行簡化

2.萬用字元

使用萬用字元描述切入點,主要的目的就是簡化之前的設定

*:單個獨立的任意符號,可以獨立出現,也可以作為字首或者字尾的匹配符出現

execution(public * yu7daily.*.UserService.find*(*))

匹配yu7daily包下的任意包中的UserService類或介面中所有find開頭的帶有一個引數的方法

..:多個連續的任意符號,可以獨立出現,常用於簡化包名與引數的書寫

execution(public User com..UserService.findById(..))

匹配com包下的任意包中的UserService類或介面中所有名稱為findById的方法

+:專用於匹配子類型別

execution(* *..*Service+.*(..))

使用切入點表示式來分析下:

execution(void yu7daily.dao.A.m1())
匹配介面,能匹配到
execution(void yu7daily.dao.impl.AImpl.m1())
匹配實現類,能匹配到
execution(* yu7daily.dao.impl.AImpl.m1())
返回值任意,能匹配到
execution(* yu7daily.dao.impl.AImpl.m1(*))
返回值任意,但是m1方法必須要有一個引數,無法匹配,要想匹配需要在m1介面和實現類新增引數
execution(void com.*.*.*.*.m1())
返回值為void,com包下的任意包三層包下的任意類的m1方法,匹配到的是實現類,能匹配
execution(void com.*.*.*.m1())
返回值為void,com包下的任意兩層包下的任意類的m1方法,匹配到的是介面,能匹配
execution(void *..m1())
返回值為void,方法名是m1的任意包下的任意類,能匹配
execution(* *..*(..))
匹配專案中任意類的任意方法,能匹配,但是不建議使用這種方式,影響範圍廣
execution(* *..u*(..))
匹配專案中任意包任意類下只要以u開頭的方法,m1方法能滿足,能匹配
execution(* *..*e(..))
匹配專案中任意包任意類下只要以e結尾的方法,m1和save方法能滿足,能匹配
execution(void com..*())
返回值為void,com包下的任意包任意類任意方法,能匹配,*代表的是方法
execution(* yu7daily.*.*Service.find*(..))
將專案中所有業務層方法的以find開頭的方法匹配
execution(* yu7daily.*.*Service.save*(..))
將專案中所有業務層方法的以save開頭的方法匹配

五.AOP通知型別

它所代表的含義是將通知新增到切入點方法執行的前面。

除了這個註解外,還有沒有其他的註解,換個問題就是除了可以在前面加,能不能在其他的地方加?

(1)前置通知,追加功能到方法執行前,類似於在程式碼1或者程式碼2新增內容

(2)後置通知,追加功能到方法執行後,不管方法執行的過程中有沒有丟擲異常都會執行,類似於在程式碼5新增內容

(3)返回後通知,追加功能到方法執行後,只有方法正常執行結束後才進行,類似於在程式碼3新增內容,如果方法執行丟擲異常,返回後通知將不會被新增

(4)丟擲異常後通知,追加功能到方法丟擲異常後,只有方法執行出異常才進行,類似於在程式碼4新增內容,只有方法丟擲異常後才會被新增

(5)環繞通知,環繞通知功能比較強大,它可以追加功能到方法執行的前後,這也是比較常用的方式,它可以實現其他四種通知型別的功能

環境準備

1.pom.xml新增Spring依賴spring-context、aspectjweaver

2.新增A和AImpl類

public interface A {
    public void m1();
    public int m2();
}
@Repository
public class AImpl implements A {
    public void m1(){
        System.out.println(" m1 ...");
    }
    public int m2() {
        System.out.println(" m2 is running ...");
        return 1;
    }
}

建立Spring的設定類

@Configuration
@ComponentScan("yu7daily")
@EnableAspectJAutoProxy
public class Config {
}

建立通知類

@Component
@Aspect
public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

編寫Show執行類

public class Show {
    public static void main(String[] args) {
        ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class);
        A A = ctx.getBean(A.class);
        A.m1();
    }
}

環繞通知

(1)原始方法有返回值的處理

修改Test,對A中的m2方法新增環繞通知,

@Component
@Aspect
public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){}
    @Around("po2()")
    public void aroundM2(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示對原始操作的呼叫
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

修改Show類,呼叫m2方法

@Component
@Aspect
public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){}
    @Around("po2()")
    public Object aroundM2(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示對原始操作的呼叫
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
}

說明:

返回的是Object而不是int的主要原因是Object型別更通用隨時可以轉型

在環繞通知中是可以對原始方法返回值就行修改的

1.返回後通知

@Component
@Aspect
public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){}
    @AfterReturning("po2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}

注意:返回後通知是需要在原始方法m2正常執行後才會被執行,如果m2()方法執行的過程中出現了異常,那麼返回後通知是不會被執行。後置通知則是不管原始方法有沒有丟擲異常都會被執行

2.異常後通知

@Component
@Aspect
public class Test {
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){}
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){}
    @AfterReturning("po2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

環繞通知注意事項

1. 環繞通知必須依賴形參ProceedingJoinPoint才能實現對原始方法的呼叫,進而實現原始方法呼叫前後同時新增通知

2. 通知中如果未使用ProceedingJoinPoint對原始方法進行呼叫將跳過原始方法的執行

3. 對原始方法的呼叫可以不接收返回值,通知方法設定成void即可,如果接收返回值,最好設定為Object型別

4. 原始方法的返回值如果是void型別,通知方法的返回值型別可以設定成void,也可以設定成Object

5. 由於無法預知原始方法執行後是否會丟擲異常,因此環繞通知方法必須要處理Throwable異常

到此這篇關於Spring超詳細講解物件導向到面向切面的文章就介紹到這了,更多相關Spring物件導向內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com