一直想着怎麼去通俗的講解AOP,這兩篇博客給了我啓發
(https://blog.csdn.net/qukaiwei/article/details/50367761),(https://blog.csdn.net/q982151756/article/details/80513340)下面我加入自己的理解,咱們來說說AOP!
目錄
大白話從思想上理解AOP
一、什麼是AOP(面向切面編程)?
百度百科:在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
我的大白話:無論在學習或者面試的時候,大家都會張口說spring的特性AOP和IOC(控制反轉上一篇有講),有些大神理解的很到位,但是對於大多數初中級工程師來講還是模糊階段,但是爲什麼會有AOP這種技術呢?(使用AOP的原因,AOP的好處)那就是爲了開發者的方便!!!就是爲了我們少寫代碼,省勁!要記住上面我說的話!
然後我們舉一個比較容易理解的例子(來自:Spring 之 AOP):
要理解切面編程,就需要先理解什麼是切面。用刀把一個西瓜分成兩瓣,切開的切口就是切面;炒菜,鍋與爐子共同來完成炒菜,鍋與爐子就是切面。web層級設計中,web層->網關層->服務層->數據層,每一層之間也是一個切面。編程中,對象與對象之間,方法與方法之間,模塊與模塊之間都是一個個切面。
我們一般做活動的時候,一般對每一個接口都會做活動的有效性校驗(是否開始、是否結束等等)、以及這個接口是不是需要用戶登錄。
按照正常的邏輯,我們可以這麼做。
這有個問題就是,有多少接口,就要多少次代碼copy。對於一個“懶人”,這是不可容忍的。好,提出一個公共方法,每個接口都來調用這個接口。這裏有點切面的味道了。
同樣有個問題,我雖然不用每次都copy代碼了,但是,每個接口總得要調用這個方法吧。於是就有了切面的概念,我將方法注入到接口調用的某個地方(切點)。
這樣接口只需要關心具體的業務,而不需要關注其他非該接口關注的邏輯或處理。
紅框處,就是面向切面編程。
二、AOP中的相關概念
看過了上面的例子,我想大家腦中對AOP已經有了一個大致的雛形,但是又對上面提到的切面之類的術語有一些模糊的地方,接下來就來講解一下AOP中的相關概念,瞭解了AOP中的概念,才能真正的掌握AOP的精髓。
初看這麼多術語,一下子都不好接受,慢慢來,很快就會搞懂。
1.通知(Advice)
就是你想要的功能,也就是上面說的 安全,事物,日誌等。你給先定義好把,然後在想用的地方用一下。
2.連接點(JoinPoint)
這個更好解釋了,就是spring允許你使用通知的地方,那可真就多了,基本每個方法的前,後(兩者都有也行),或拋出異常時都可以是連接點,spring只支持方法連接點.其他如aspectJ還可以讓你在構造器或屬性注入時都行,不過那不是咱關注的,只要記住,和方法有關的前前後後(拋出異常),都是連接點。
3.切入點(Pointcut)
上面說的連接點的基礎上,來定義切入點,你的一個類裏,有15個方法,那就有幾十個連接點了對把,但是你並不想在所有方法附近都使用通知(使用叫織入,以後再說),你只想讓其中的幾個,在調用這幾個方法之前,之後或者拋出異常時乾點什麼,那麼就用切點來定義這幾個方法,讓切點來篩選連接點,選中那幾個你想要的方法。
4.切面(Aspect)
切面是通知和切入點的結合。現在發現了吧,沒連接點什麼事情,連接點就是爲了讓你好理解切點,搞出來的,明白這個概念就行了。通知說明了幹什麼和什麼時候幹(什麼時候通過方法名中的before,after,around等就能知道),而切入點說明了在哪幹(指定到底是哪個方法),這就是一個完整的切面定義。
5.引入(introduction)
允許我們向現有的類添加新方法屬性。這不就是把切面(也就是新方法屬性:通知定義的)用到目標類中嗎
6.目標(target)
引入中所提到的目標類,也就是要被通知的對象,也就是真正的業務邏輯,他可以在毫不知情的情況下,被咱們織入切面。而自己專注於業務本身的邏輯。
7.代理(proxy)
怎麼實現整套aop機制的,都是通過代理,這個一會給細說。
8.織入(weaving)
把切面應用到目標對象來創建新的代理對象的過程。有3種方式,spring採用的是運行時,爲什麼是運行時,後面解釋。
關鍵就是:切點定義了哪些連接點會得到通知
我所理解的AOP原理
spring用代理類包裹切面,把他們織入到Spring管理的bean中。也就是說代理類僞裝成目標類,它會截取對目標類中方法的調用,讓調用者對目標類的調用都先變成調用僞裝類,僞裝類中就先執行了切面,再把調用轉發給真正的目標bean。
現在可以自己想一想,怎麼搞出來這個僞裝類,纔不會被調用者發現(過JVM的檢查,JAVA是強類型檢查,哪裏都要檢查類型)。
1.實現和目標類相同的接口,我也實現和你一樣的接口,反正上層都是接口級別的調用,這樣我就僞裝成了和目標類一樣的類(實現了同一接口,咱是兄弟了),也就逃過了類型檢查,到java運行期的時候,利用多態的後期綁定(所以spring採用運行時),僞裝類(代理類)就變成了接口的真正實現,而他裏面包裹了真實的那個目標類,最後實現具體功能的還是目標類,只不過僞裝類在之前幹了點事情(寫日誌,安全檢查,事物等)。
這就好比,一個人讓你辦件事,每次這個時候,你弟弟就會先出來,當然他分不出來了,以爲是你,你這個弟弟雖然辦不了這事,但是他知道你能辦,所以就答應下來了,並且收了點禮物(寫日誌),收完禮物了,給把事給人家辦了啊,所以你弟弟又找你這個哥哥來了,最後把這是辦了的還是你自己。但是你自己並不知道你弟弟已經收禮物了,你只是專心把這件事情做好。
順着這個思路想,要是本身這個類就沒實現一個接口呢,你怎麼僞裝我,我就壓根沒有機會讓你搞出這個雙胞胎的弟弟,那麼就用第2種代理方式,創建一個目標類的子類,生個兒子,讓兒子僞裝我
2.生成子類調用,這次用子類來做爲僞裝類,當然這樣也能逃過JVM的強類型檢查,我繼承的嗎,當然查不出來了,子類重寫了目標類的所有方法,當然在這些重寫的方法中,不僅實現了目標類的功能,還在這些功能之前,實現了一些其他的(寫日誌,安全檢查,事物等)。
這次的對比就是,兒子先從爸爸那把本事都學會了,所有人都找兒子辦事情,但是兒子每次辦和爸爸同樣的事之前,都要收點小禮物(寫日誌),然後纔去辦真正的事。當然爸爸是不知道兒子這麼幹的了。這裏就有件事情要說,某些本事是爸爸獨有的(final的),兒子學不了,學不了就辦不了這件事,辦不了這個事情,自然就不能收人家禮了。
前一種兄弟模式,spring會使用JDK的java.lang.reflect.Proxy類,它允許Spring動態生成一個新類來實現必要的接口,織入通知,並且把對這些接口的任何調用都轉發到目標類。
後一種父子模式,spring使用CGLIB庫生成目標類的一個子類,在創建這個子類的時候,spring織入通知,並且把對這個子類的調用委託到目標類。
相比之下,還是兄弟模式好些,他能更好的實現松耦合,尤其在今天都高喊着面向接口編程的情況下,父子模式只是在沒有實現接口的時候,也能織入通知,應當做一種例外。
那在繼續學習之前,什麼是代理模式以及上述兩種AOP動態代理的底層實現都在這篇博客中,我強烈建議先看完,不然很難理解後面的內容:https://blog.csdn.net/qq_28863191/article/details/101517631
讓我們通過代碼來學習把:
1.導入aop所需要的對應Jar包
一會我們需要使用JUnit來測試,所以需要導入Spring整合單元測試的jar包
2.然後在配置文件 application.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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
----http://www.springframework.org/schema/aop
----http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
橫線部分就是aop的約束,我們學習到這裏發現什麼了麼,沒錯,引入新的約束在多數情況下是添加一個xmlns:XXX的變量,然後在 xsi:schemaLocation 中加入其對應的 url,比如 aop 就是添加
----http://www.springframework.org/schema/aop
----http://www.springframework.org/schema/aop/spring-aop.xsd
3.接着,我們就要開始寫接口和實現類了
/**
* 一個產品的接口
* @author hp
*
*/
public interface ProductDAO {
public void save();
public void update();
public void find();
public void delete();
}
/**
* 產品接口的實現類
* @author hp
*
*/
public class ProductDAOImpl implements ProductDAO {
@Override
public void save() {
// TODO Auto-generated method stub
System.out.println("保存商品。。。");
}
@Override
public void update() {
// TODO Auto-generated method stub
System.out.println("修改商品。。。");
}
@Override
public void find() {
// TODO Auto-generated method stub
System.out.println("查找商品。。。");
}
@Override
public void delete() {
// TODO Auto-generated method stub
System.out.println("刪除商品。。。");
}
}
4.定義一個切面類,將通知(你所需要插入的功能)寫成方法:
/**
* 定義切面類,它有一個checkPrice的權限校驗方法
* @author hp
*
*/
public class MyAspectXML {
public void checkPrice() {
System.out.println("權限校驗==========");
}
}
5.我們將產品類和切面類都交給Spring管理
<!-- 配置目標對象,被增強對象=================== -->
<bean id="productDao" class="com.ysx.spring.demo2.ProductDAOImpl" />
<!-- 將切面類交給Spring -->
<bean id="myAspect" class="com.ysx.spring.demo2.MyAspectXML" />
<!-- 通過AOP的配置完成對目標類產生的代理 -->
<aop:config>
<!-- 設置一個切入點在表達式execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))這裏 -->
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
<!-- 配置切面 -->
<aop:aspect ref="myAspect">
<!-- aop:before表示在表達式之前
pointcut-ref對應着ID所代表的表達式 -->
<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
6.測試
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//RunWith是固定格式
@RunWith(SpringJUnit4ClassRunner.class)
//加載配置文件
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {
//注入到需要被增強的類
@Resource(name="productDao")
private ProductDAO productDao;
@Test
public void demo2() {
productDao.save();
}
}
可以在自己的IDE上試一下,得到的結果是
Spring中的通知類型
前置通知:在目標方法執行之前進行操作
前置通知可以獲得切入點信息
在切面類加入
public class MyAspectXML {
public void checkPrice(JoinPoint joinPoint) {
System.out.println("權限校驗==========" + joinPoint);
}
}
結果就會變成
後置通知:在目標方法執行之後進行操作
後置通知除了可以獲得切入點信息之外,還可以獲得方法的返回值
這次我們拿 update() 方法舉例
接口和實現類
/**
* 一個產品的接口
* @author hp
*
*/
public interface ProductDAO {
public void save();
public void update();
public void find();
public String delete();
}
/**
* 產品接口的實現類
* @author hp
*
*/
public class ProductDAOImpl implements ProductDAO {
@Override
public void save() {
// TODO Auto-generated method stub
System.out.println("保存商品。。。");
}
@Override
public void update() {
// TODO Auto-generated method stub
System.out.println("修改商品。。。");
}
@Override
public void find() {
// TODO Auto-generated method stub
System.out.println("查找商品。。。");
}
@Override
public String delete() {
// TODO Auto-generated method stub
System.out.println("刪除商品。。。");
return "楊大俠";
}
}
切面類
public class MyAspectXML {
/**
* 前置通知
* @param joinPoint
*/
public void checkPrice(JoinPoint joinPoint) {
System.out.println("權限校驗==========" + joinPoint);
}
/**
* 後置通知
* @param r
*/
public void writeLog(Object result) {
System.out.println("日誌記錄==========" + result);
}
}
application.xml配置文件
<aop:config>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
<!-- 配置連接到delete方法 -->
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.delete(..))" id="pointcut2"/>
<!-- 配置切面 -->
<aop:aspect ref="myAspect">
<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
<!-- pointcut-ref 通過上面id連接
method 表示要連接的切面類裏面的通知方法
returning 裏面是返回值,這個變量的名字必須和writeLog方法的參數一致 -->
<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
</aop:aspect>
</aop:config>
測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {
@Resource(name="productDao")
private ProductDAO productDao;
@Test
public void demo2() {
productDao.save();
productDao.delete();
}
}
最後是我們得到的結果,可以看到“楊大俠”名字在日誌記錄的時候被輸出出來了。
環繞通知:在目標方法執行之前和之後進行操作
環繞通知除了可以獲得切入點信息和方法的返回值之外,還可以阻止目標方法的執行
這次我們拿 update() 方法舉例
接口和實現類
/**
* 一個產品的接口
* @author hp
*
*/
public interface ProductDAO {
public void save();
public void update();
public void find();
public String delete();
}
/**
* 產品接口的實現類
* @author hp
*
*/
public class ProductDAOImpl implements ProductDAO {
@Override
public void save() {
// TODO Auto-generated method stub
System.out.println("保存商品。。。");
}
@Override
public void update() {
// TODO Auto-generated method stub
System.out.println("修改商品。。。");
}
@Override
public void find() {
// TODO Auto-generated method stub
System.out.println("查找商品。。。");
int i = 1/0;
}
@Override
public String delete() {
// TODO Auto-generated method stub
System.out.println("刪除商品。。。");
return "楊大俠";
}
}
切面類
/**
* 定義切面類,它有一個checkPrice的權限校驗方法
* @author hp
*
*/
public class MyAspectXML {
/**
* 前置通知
* @param joinPoint
*/
public void checkPrice(JoinPoint joinPoint) {
System.out.println("權限校驗==========" + joinPoint);
}
/**
* 後置通知
* @param r
*/
public void writeLog(Object result) {
System.out.println("日誌記錄==========" + result);
}
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("環繞前通知============");
//這裏就相當於執行目標程序,如果有返回值接收返回,沒有就void
Object obj = joinPoint.proceed();
System.out.println("環繞前通知============");
return obj;
}
/**
* 異常拋出通知
*/
public void afterThrowing(Throwable ex) {
System.out.println("異常拋出通知===============" + ex);
}
public void after() {
System.out.println("最終通知==============");
}
}
application.xml配置文件
<!-- 通過AOP的配置完成對目標類產生的代理 -->
<aop:config>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.delete(..))" id="pointcut2"/>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.update(..))" id="pointcut3"/>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.find(..))" id="pointcut4"/>
<!-- 配置切面 -->
<aop:aspect ref="myAspect">
<!-- 前置通知=========== -->
<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
<!-- 後置通知=========== -->
<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
<!-- 環繞通知=========== -->
<aop:around method="around" pointcut-ref="pointcut3"/>
<!-- 異常拋出通知=========== -->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"/>
<!-- 最終通知 -->
<aop:after method="after" pointcut-ref="pointcut4"/>
</aop:aspect>
</aop:config>
測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {
@Resource(name="productDao")
private ProductDAO productDao;
@Test
public void demo2() {
productDao.save();
productDao.delete();
productDao.update();
productDao.find();
}
}
運行結果
異常拋出通知:在程序出現異常的時候,進行操作
可以獲得異常信息
最終通知:無論代碼是否會有異常,總是會執行
相當於執行 finally 代碼塊裏的內容,跟異常拋出通知一起講,相當於try/catch/finally
這次我們拿 find() 方法舉例
接口和實現類
/**
* 一個產品的接口
* @author hp
*
*/
public interface ProductDAO {
public void save();
public void update();
public void find();
public String delete();
}
/**
* 產品接口的實現類
* @author hp
*
*/
public class ProductDAOImpl implements ProductDAO {
@Override
public void save() {
// TODO Auto-generated method stub
System.out.println("保存商品。。。");
}
@Override
public void update() {
// TODO Auto-generated method stub
System.out.println("修改商品。。。");
}
@Override
public void find() {
// TODO Auto-generated method stub
System.out.println("查找商品。。。");
}
@Override
public String delete() {
// TODO Auto-generated method stub
System.out.println("刪除商品。。。");
return "楊大俠";
}
}
切面類
public class MyAspectXML {
/**
* 前置通知
* @param joinPoint
*/
public void checkPrice(JoinPoint joinPoint) {
System.out.println("權限校驗==========" + joinPoint);
}
/**
* 後置通知
* @param r
*/
public void writeLog(Object result) {
System.out.println("日誌記錄==========" + result);
}
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("環繞前通知============");
//這裏就相當於執行目標程序,如果有返回值接收返回,沒有就void
Object obj = joinPoint.proceed();
System.out.println("環繞前通知============");
return obj;
}
}
application.xml配置文件
<aop:config>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.delete(..))" id="pointcut2"/>
<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.update(..))" id="pointcut3"/>
<!-- 配置切面 -->
<aop:aspect ref="myAspect">
<!-- 前置通知=========== -->
<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
<!-- 後置通知=========== -->
<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
<!-- 環繞通知=========== -->
<aop:around method="around" pointcut-ref="pointcut3"/>
</aop:aspect>
</aop:config>
測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {
@Resource(name="productDao")
private ProductDAO productDao;
@Test
public void demo2() {
productDao.save();
productDao.delete();
productDao.update();
}
}
運行結果
引介通知:(不會用)
Spring切入點的表達式寫法
切入點的表達式語法
基於execution的函數完成的
語法:
- [訪問修飾符] 方法返回值 包名.類名.方法名(參數)(注:[]括號表示可以省略)
- public void com.ysx.spring.CustomerDAO.save(..) (參數位置兩個點表示任意參數)
- 我們還可以用 * 表示, * 代表任意類型,比如在上面通知的代碼中 用 * 表示任意返回類型
- * com.ysx.spring.CustomerDAO. *(..) 表示CustomerDAO中的每一個方法,返回類型任意
- * com.ysx.spring.*.*(..)表示com.ysx.spring包下的每一個類的每一個方法
- * com.ysx.spring.*DAO.*(..)表示com.ysx.spring包下的每一個叫 XXXDAO 的類的每一個方法