黑莲技术资源论坛

作者: 顺势而为47
查看: 72|回复: 0

手把手教會「Spring」從面向對象再到面向切面,直接易懂

手把手教會「Spring」從面向對象再到面向切面,直接易懂

[复制链接]
顺势而为47 | 显示全部楼层 发表于: 2022-8-5 22:39:02
顺势而为47 发表于: 2022-8-5 22:39:02 | 显示全部楼层 |阅读模式
查看: 72|回复: 0
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相關概念
@Repositorypublic 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類publicinterfaceA { publicvoidsave(); publicvoidm1(); } @RepositorypublicclassAImplimplementsA { publicvoidsave() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } publicvoidm1(){ System.out.println("book dao m1 ..."); } }
  • 創建Spring的配置類@Configuration@ComponentScan("yu7daily") public class Config { }
  • 編寫Show運行類publicclass Show { publicstaticvoidmain(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@Aspectpublic 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")@EnableAspectJAutoProxypublic 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();}@Repositorypublic 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運行類
  • publicclass Show { publicstaticvoidmain(String[] args) { ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class); A A = ctx.getBean(A.class); A.m1(); } }
環繞通知

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

  • 修改Test,對A中的m2方法添加環繞通知,
@Component@Aspectpublic 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@Aspectpublic 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@Aspectpublic 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@Aspectpublic 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異常

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|小黑屋|黑莲技术资源论坛 ( 闽ICP备18016623号-7 )|网站地图

GMT+8, 2022-8-19 04:52 , Processed in 1.128617 second(s), 26 queries .

Powered by BBS.HL1.NET X3.4 © 2020-2022

本站IT社区(bbs.hl1.net)所有的资源教程均来自网友分享及互联网收集

快速回复 返回顶部 返回列表