javascript
推荐学java——Spring之AOP
tips:本文首發在公眾號逆鋒起筆 ,本文源代碼在公眾號回復aop 即可查看。
什么是AOP?
AOP (Aspect Orient Programming),直譯過來就是 面向切面編程。AOP 是一種編程思想,是面向對象編程(OOP)的一種補充。面向對象編程將程序抽象成各個層次的對象,而面向切面編程是將程序抽象成各個切面。
為什么需要AOP?
實際開發中我們應該都遇到過類似這樣的場景:在多個模塊間有某段重復的代碼,我們通常是怎么處理的?
在傳統的面向過程編程中,我們也會將這段代碼,抽象成一個方法,然后在需要的地方分別調用這個方法,這樣當這段代碼需要修改時,我們只需要改變這個方法就可以了。然而需求總是變化的,有一天,新增了一個需求,需要再多出做修改,我們需要再抽象出一個方法,然后再在需要的地方分別調用這個方法,又或者我們不需要這個方法了,我們還是得刪除掉每一處調用該方法的地方。實際上涉及到多個地方具有相同的修改的問題我們都可以通過 AOP 來解決。
AOP的本質
AOP 的本質是由 AOP 框架修改業務組件的多個方法的源代碼,看到這其實應該明白了,AOP 其實就是代理模式的典型應用。按照 AOP 框架修改源代碼的時機,可以將其分為兩類:
靜態 AOP 實現, AOP 框架在編譯階段對程序源代碼進行修改,生成了靜態的 AOP 代理類(生成的 *.class 文件已經被改掉了,需要使用特定的編譯器),比如 AspectJ。
動態 AOP 實現, AOP 框架在運行階段對動態生成代理對象(在內存中以 JDK 動態代理,或 CGlib 動態地生成 AOP 代理類),如 SpringAOP。
AOP術語
AOP 領域中的術語:
通知(Advice): AOP 框架中的增強處理。通知描述了切面何時執行以及如何執行增強處理。
連接點(join point): 連接點表示應用執行過程中能夠插入切面的一個點,這個點可以是方法的調用、異常的拋出。在 Spring AOP 中,連接點總是方法的調用。
切點(PointCut): 可以插入增強處理的連接點。
切面(Aspect): 切面是通知和切點的結合。
引入(Introduction):引入允許我們向現有的類添加新的方法或者屬性。
織入(Weaving): 將增強處理添加到目標對象中,并創建一個被增強的對象,這個過程就是織入。
這些術語不同書籍翻譯有區別,關鍵要結合程序理解之后,就明白各個術語的意思了。
第一個Spring AOP項目
新建module
名字隨著各位心情來,我這里是sping-aop
配置
在pom.xml 文件中增加下面兩個依賴:
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.14</version> </dependency><!-- 添加能提供AOP注解功能的依賴,該依賴并非Spring提供--> <dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.3.14</version> </dependency>模擬業務
1、新建包service,提供如下接口
public interface SomeService {void doSome();void doSome(String name,Integer num); }2、在該包下,新建子包impl,并提供實現類如下
public class SomeServiceImpl implements SomeService {@Overridepublic void doSome() {System.out.println("原來的業務方法,在實現類中");}@Overridepublic void doSome(String name, Integer num) {System.out.println("原來的業務方法,在實現類中,有兩個參數:" + name + "->" + num);}}3、再單獨新建一個包,名字隨意,我這里名為handle,其下有一個名為 MyAspect 的類,代碼如下:
@Aspect public class MyAspect {/*** execution() 這個叫 切點表達式* <p>* 語法依次是:方法修飾符(可省略)、方法返回類型、方法所在包名全路徑+方法名+方法參數類型列表*/@Before(value = "execution(public void com.javafirst.service.impl.SomeServiceImpl.doSome(java.lang.String, java.lang.Integer))")public void aop_before() {System.out.println("在原有業務方法之前執行邏輯,這里自動代理功能要執行的代碼.");}}這里使用了注解 @Aspect,這就是我們在前面添加的依賴,也就是 Spring 提供的注解。這里大家需要知道一個概念:切點表達式
4、Spring核心配置文件applicationConext.xml 代碼如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 目標對象 --><bean id="someService" class="com.javafirst.service.impl.SomeServiceImpl"/><!-- 切面類 --><bean id="my_handle" class="com.javafirst.handle.MyAspect"/><!-- 代理生成器 --><aop:aspectj-autoproxy proxy-target-class="true"/> </beans>這里會接觸到一個新標簽 <aspect-autoproxy> ,代理生成器,系統自動在內存中生成代理類,無需我們手動顯示寫代理類。
5、和我們上一節學習一樣,本節的測試代碼基本不會變
/*** @Before 在目標方法之前執行*/ @Test public void testBefore() {String config = "applicationContext.xml";ApplicationContext context = new ClassPathXmlApplicationContext(config);SomeService someService = (SomeService) context.getBean("someService");someService.doSome();someService.doSome("無崖子", 87); }6、結果展示:
在原有業務方法之前執行邏輯,這里自動代理功能要執行的代碼. 原來的業務方法,在實現類中,有兩個參數:無崖子->87小結
通過上面這個流程,我們簡單掌握一下通過注解來實現代理功能的神奇,當然了前面提到的切點表達式也是有語法的,且Spring AOP中的通知注解也不止@Before這一種,這兩點就是本文的重點。
接著往下看↓
Spring AOP 5 種通知注解
在學習通知注解前,我們先看下前面提到的切點表達式,以上面的例子,做幾個變形,各位可以測試一下結果,方便理解,這并不難。
execution(public void com.javafirst.service.impl.SomeServiceImpl.doSome(java.lang.String, java.lang.Integer)):這表示注解功能代碼在一個修飾符為public、返回值為void 、在這個com.javafirst.service.impl路徑下的 SomeServiceImpl 類中的doSome(java.lang.String, java.lang.Integer) 方法之前執行。
execution(void com.javafirst.service.impl.SomeServiceImpl.doSome(java.lang.String, java.lang.Integer)):方法修飾符可省略
execution(void com.javafirst.service.impl.*.doSome(java.lang.String, java.lang.Integer)):指定包下所有類的 doSome(java.lang.String, java.lang.Integer) 方法
execution(void com.javafirst.service.impl.*.*(java.lang.String, java.lang.Integer)):指定包下所有類中帶有兩個參數(java.lang.String, java.lang.Integer)的方法
execution(* com.javafirst.service.*.*(java.lang.String, java.lang.Integer)):指定包下及其子包下所有方法,不區分方法返回類型(各位可以在接口中多寫一個方法做測試)
execution(void com.javafirst.service.*.*(..)):指定包下及其子包下所有方法,不區分是否有參數。(這個可以測試出我們前面接口中定義的另一個方法在執行前是否也會執行代理方法中的內容)
多個匹配之間我們可以使用鏈接符 &&、||、!來表示 “且”、“或”、“非”的關系。但是在使用 XML 文件配置時,這些符號有特殊的含義,所以我們使用 “and”、“or”、“not”來表示。這里就不舉例了,下面看通知注解
@Before
前置通知:通知方法在目標方法之前調用。
使用我們已經學習過了,這里再了解一下該注解的參數,方法名字可以自定義,那么系統的一個參數是JoinPoint 要使用的話,必須是該方法的形參列表第一個,功能類似于Java反射中的Method類,可以獲取方法名、方法參數等,用于做不同的邏輯處理。
比如我們將前面的代碼做個變形,就可以驗證結果:
@Before(value = "execution(void com.javafirst.service.*.*(..))") public void aop_before(JoinPoint point) {System.out.println();System.out.println("指定包下及其子包下所有方法,不區分是否有參數-在原有業務方法之前執行邏輯,這里自動代理功能要執行的代碼.");System.out.println("方法名:" + point.getSignature().getName());System.out.println("方法參數:" + point.getArgs().length + " 個參數");if (point.getArgs().length == 2) {System.out.println("要執行 兩個參數的方法");} else {System.out.println("要執行 無參數的方法");} }@AfterReturning
后置通知:在目標方法執行完之后再執行。從該方法的參數也可以接收到目標方法的返回結果,推薦使用Object類型接收。
1、在原來的接口類基礎上新增一個方法:
String returnPrice(double price,float discount);我們要測試后置通知,就要在執行代理方法之前拿到目標方法的返回值,所以這里定義的是有返回值并且帶參數的方法。
2、實現類中的實現邏輯(實際開發根據業務來)示例:
@Override public String returnPrice(double price, float discount) {if (discount > 0.0f && price > 0.0) {return "折扣價:" + (price * discount);}return "原價:" + price; }3、切面類中定義后置通知方法
/*** 后置通知** @param res 參數名必須和 returning 值保持一致,表示目標方法的返回值*/ @AfterReturning(value = "execution(java.lang.String com.javafirst.service.impl.*.return*(..))", returning = "res") public void aop_afterReturning(Object res) {System.out.println();System.out.println("目標方法執行結果:" + res);System.out.println("在目標方法執行后 輸出."); }這里的切入點表達式和之前的一樣,重點是這里的returning的值和方法的形參名之間的關系,這兩者必須保持一致,否則拿不到目標方法的返回結果。
4、測試代碼就不貼了,各位自己測試
@Around
將目標方法封裝起來,簡單理解:可以在目標方法之前和只有都執行相關代碼邏輯。下面看示例:
1、定義接口方法
String doWork(String name);2、接口實現類中代碼:
@Override public String doWork(String name) {System.out.println("正在擼碼的工程師:" + name);return "員工姓名:" + name; }注意這里的返回值執行時機。
3、切面類中定義的注解通知代碼:
/*** 環繞通知* <p>* 方法的定義:* - 必須是public* - 必須有返回值,推薦是Object* - 參數必須有且是 ProceedingJoinPoint** @param joinPoint* @return*/ @Around(value = "execution(public java.lang.String com.javafirst.service.impl.SomeServiceImpl.doWork(java.lang.String))") public Object aop_around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println();System.out.println("我是@Around 方法里面的輸出");// 通過參數 ProceedJoinPoint 控制目標方法是否可以被執行Object args[] = joinPoint.getArgs();if (null != args && args.length > 0) {// 執行目標方法if (null != args[0]) {joinPoint.proceed();return "您傳入的參數是:" + args[0];}}return "我是@Around 方法return的內容"; }這里如果不通過參數 ProceedingJoinPoint 處理執行目標方法,默認目標方法是不會執行的;注意這里的輸出語句和返回值執行邏輯,從而理解環繞通知。
4、測試代碼
/*** @Around 在目標方法之前之后都可以執行相關代碼*/ @Test public void testAround() {String config = "applicationContext.xml";ApplicationContext context = new ClassPathXmlApplicationContext(config);SomeService someService = (SomeService) context.getBean("someService");String result = someService.doWork("李淳罡");//String result = someService.doWork(null);System.out.println("測試結果輸出:"+result); }結果大家親自體驗,通過改變參數觀察我們的通知代碼執行邏輯,環繞通知并不等于 ?@Before + @AfterReturning ,環繞通知是可以修改目標方法的返回值的。
@AfterThrowing(了解)
目標方法拋出異常后執行,不拋異常則不執行(也就是說**如果目標方法自己try-catch 了異常,則通知方法是不會執行的)。下面是示例:
1、定義接口方法
void doOrder(Integer num);為了驗證結果,我們就要通過參數來制造異常,為了方便簡單,容易理解,這里就在目標方法內采用 200/num ,如果參數為0,異常就出現了。
2、實現類,也就是目標類實現代碼如下:
@Override public void doOrder(Integer num) {System.out.println("目標方法內輸出,如果參數為0,則計算(2022/num)會拋出異常");num = 2022 / num; }3、切面類,定義通知方法代碼:
/*** 異常通知:目標方法拋出異常后調用,不拋出異常,則不調用* <p>* 作用:起到監控目標方法的作用,如果有異常了,方便開發人員定位問題,修復bug** @param ex*/ @AfterThrowing(value = "execution(public void com.javafirst.service.impl.SomeServiceImpl.doOrder(java.lang.Integer))", throwing = "ex") public void aop_afterThrowing(Exception ex) {System.out.println("目標方法出現異常了,才會輸出這句!\n異常信息:" + ex.getMessage()); }以上就是核心代碼了,結果各位自行測試哈。
@After(了解)
最終通知:目標方法返回或異常后調用,該通知方法始終會被調用到,適合做一些收尾工作,比如:清楚緩存、刪除某些數據等。
1、定義接口方法
void payMoney(String address);同上,我們通過方法的參數來“制造異常”。
2、實現類中代碼:
@Override public void payMoney(String address) {System.out.println("目標方法第一句內容,輸出參數:" + address);System.out.println("截取地址的前三個字:" + (address.substring(0, 3)));//try {// ? System.out.println("截取地址的前三個字:" + (address.substring(0, 3)));//}catch (Exception ex){// ? System.out.println("目標方法有try-catch");//}System.out.println("目標方法異常后的輸出語句."); }3、切面類中定義通知代碼:
/*** 最終通知:一定會被執行到,且在目標方法之后*/ @After(value = "execution(* *..SomeServiceImpl.payMoney(..))") public void aop_after() {System.out.println("\n切面類中的輸出內容!"); }結果各位自己驗證哈,這個不難理解,類似于Java中的try-catch-finally 中的 finally{} 代碼塊,總是會被執行到。
@Pointcut
解決什么問題:我們在定義通知時,會在每個方法上面加注解,注解中有切入點表達式,而當我們定義的方法多了之后,需要改動路徑或者方法名等其他時,就比較繁瑣,還容易出錯,那么 @Pointcut 注解就解決了這個問題。
使用方式:定義一個方法,方法體內無需內容,在該方法上面加 @Pointcut 注解,你應該都想到了,該注解也是有 value 屬性的,那么表達式就寫在這里,以后要改就只改這個地方;而原來加注解通知方法的地方只需把切入點表達水的值改為這里定義的方法名即可(要帶括號)
我們來演示一下,就以我們本文最后學的一個注解代碼為例:
@After(value = "aop_pointcut()") public void aop_after() {System.out.println("\naop_after() 后置通知:切面類中的輸出內容!"); }/*** 前置通知,為了測試 @Pointcut*/ @Before(value = "aop_pointcut()") public void aop_before_pointcut() {System.out.println("aop_before_pointcut() 前置通知:切面類中的輸出內容!\n"); }/*** 定義 @Pointcut 注解*/ @Pointcut(value = "execution(* *..SomeServiceImpl.payMoney(..))") private void aop_pointcut() { }測試代碼無需修改,看結果就行,這個其實沒什么難度,和我們之前學習動態SQL的時候,用到的 include 標簽是一樣的功能。
總結
看到這里,關于 Spring 的兩大核心內容我們就學習結束了,后面還會學習兩大內容:Spring 集成 MyBatis 和 Spring事務。
Spring面向切面編程是一種編程思想,其實理解起來沒有想象中那么難,尤其在你有java編程基礎時。
記住本文演示的 5 種通知注解,和其用法,以及代碼執行流程,和那個階段我們應該處理什么業務邏輯,這在以后會用到。
tips:本文的源代碼在公眾號推薦學java 中回復aop 即可查看。
學編程,推薦選 Java 語言,這是毋庸置疑的,如果你還對 Java 體系掌握不夠,可以聯系小編,跟著小編一起進階!
看完記得點贊、評論支持小編哈,碼字不易~
推薦學java——Spring第一課
推薦學java——MyBatis高級
推薦學Java——第一個MyBatis程序
推薦學java——Maven初識
推薦學Java——數據表高級操作
推薦學Java——數據表操作
推薦學Java——初識數據庫
推薦學Java—應該了解的前端內容
一文回顧 Java 入門知識(下)
一文回顧 Java 入門知識(中)
一文回顧 Java 入門知識(上)
總結
以上是生活随笔為你收集整理的推荐学java——Spring之AOP的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STM32F05 学习中.......
- 下一篇: 【微服务】SpringBoot 搭建微服