1. 程式人生 > 實用技巧 >面試官:連Spring AOP都說不明白,自己走還是我送你?

面試官:連Spring AOP都說不明白,自己走還是我送你?

前言

因為假期原因,有一段時間沒給大家更新了!和大家說個事吧,放假的時候一位粉絲和我說了下自己的被虐經歷,在假期前他去某網際網路公司面試,結果直接被人家面試官Spring AOP三連問給問的一臉懵逼!其實我覺著吧,這玩意不是挺簡單的嗎?

大家在學習 AOP 之前,如果清楚代理模式的話,則學習起來非常輕鬆,接下來就由我為大家介紹 AOP 這個重要的知識點!

代理模式

代理模式在 Java 開發中是一種比較常見的設計模式。設計目的旨在為服務類與客戶類之間插入其他功能,插入的功能對於呼叫者是透明的,起到偽裝控制的作用。如租房的例子:房客、中介、房東。對應於代理模式中即:客戶類、代理類 、委託類(被代理類)。

為某一個物件(委託類)提供一個代理(代理類),用來控制對這個物件的訪問。委託類和代理類有一個共同的父類或父介面。代理類會對請求做預處理、過濾,將請求分配給指定物件。

生活中常見的代理情況: 租房中介、婚慶公司等

代理模式的兩個設計原則:

  1. 代理類與委託類具有相似的行為(共同)
  2. 代理類增強委託類的行為

常用的代理模式:

  1. 靜態代理
  2. 動態代理

靜態代理

某個物件提供一個代理,代理角色固定,以控制對這個物件的訪問。 代理類和委託類有共同的父類或父介面,這樣在任何使用委託類物件的地方都可以用代理物件替代。代理類負責請求的預處理、過濾、將請求分派給委託類處理、以及委託類執行完請求後的後續處理。

代理的三要素

  • 有共同的行為(結婚) - 介面

  • 目標角色(新人) - 實現行為

  • 代理角色(婚慶公司) - 實現行為 增強目標物件行為

靜態代理的特點

  1. 目標角色固定
  2. 在應用程式執行前就得到目標角色
  3. 代理物件會增強目標物件的行為
  4. 有可能存在多個代理,引起"類爆炸"(缺點)

靜態代理的實現

定義行為(共同)定義介面

/**
* 定義⾏為
*/
public interface Marry {
 public void toMarry();
}

目標物件(實現行為)

/**
* 靜態代理 ——> ⽬標物件
*/
public class You implements Marry {
 // 實現⾏為
 @Override
 public void toMarry() {
 System.out.println("我要結婚了...");
 }
}

代理物件(實現行為、增強目標物件的行為)

/**
* 靜態代理 ——> 代理物件
*/
public class MarryCompanyProxy implements Marry {
 // ⽬標物件
 private Marry marry;
 // 通過構造器將⽬標物件傳⼊
 public MarryCompanyProxy(Marry marry) {
 this.marry = marry;
 }
 // 實現⾏為
 @Override
 public void toMarry() {
 // 增強⾏為
 before();
 
 // 執⾏⽬標物件中的⽅法
 marry.toMarry();
// 增強⾏為
 after();
 }
 /**
 * 增強⾏為
 */
 private void after() {
 System.out.println("新婚快樂,早⽣貴⼦!");
 }
 /**
 * 增強⾏為
 */
 private void before() {
 System.out.println("場地正在佈置中...");
 }
}

通過代理物件實現目標物件的功能

// ⽬標物件
You you = new You();
// 構造代理⻆⾊同時傳⼊真實⻆⾊
MarryCompanyProxy marryCompanyProxy = new MarryCompanyProxy(you);
// 通過代理物件調⽤⽬標物件中的⽅法
marryCompanyProxy.toMarry();

靜態代理對於代理的角色是固定的,如 dao 層有20個 dao 類,如果要對方法的訪問許可權進行代理,此時需要建立20個靜態代理角色,引起類爆炸,無法滿足生產上的需要,於是就催生了動態代理的思想。

動態代理

相比於靜態代理,動態代理在建立代理物件上更加的靈活,動態代理類的位元組碼在程式執行時,由 Java 反射機制動態產生。它會根據需要,通過反射機制在程式執行期,動態的為目標物件建立代理物件,無需程式設計師手動編寫它的原始碼。動態代理不僅簡化了程式設計工作,二且提高了軟體系統的可擴充套件性,因為反射機制可以生成任意型別的動態代理類。代理的行為可以代理多個方法,即滿足生產需要的同時又達到程式碼通用的目的。

動態代理的兩種實現方式:

  1. JDK 動態代理
  2. CGLIB動態代理

動態代理的特點

  1. 目標物件不固定
  2. 在應用程式執行時動態建立目標物件
  3. 代理物件會增強目標物件的行為

JDK動態代理

注:JDK動態代理的目標物件必須有介面實現

newProxyInstance

Proxy 類:

Proxy類是專門完成代理的操作類,可以通過此類為一個或多個介面動態地生成實現類,此類提供瞭如下操作方法:

/*
 返回⼀個指定接⼝的代理類的例項⽅法調⽤分派到指定的調⽤處理程式。 (返回代理物件)
 loader:⼀個ClassLoader物件,定義了由哪個ClassLoader物件來對⽣成的代理物件進⾏載入
 interfaces:⼀個Interface物件的陣列,表示的是我將要給我需要代理的物件提供⼀組什麼接⼝,如果
 我提供了⼀組接⼝給它,那麼這個代理物件就宣稱實現了該接⼝(多型),這樣我就能調⽤這組接⼝中的⽅法了
 h:⼀個InvocationHandler接⼝,表示代理例項的調⽤處理程式實現的接⼝。每個代理例項都具有⼀個關聯
的調⽤處理程式。對代理例項調⽤⽅法時,將對⽅法調⽤進⾏編碼並將其指派到它的調⽤處理程式的 invoke ⽅法
(傳⼊InvocationHandler接⼝的⼦類)
*/
public static Object newProxyInstance(ClassLoader loader,
 Class<?>[] interfaces,
 InvocationHandler h)

獲取代理物件

public class JdkHandler implements InvocationHandler {
 // ⽬標物件
 private Object target; // ⽬標物件的型別不固定,建立時動態⽣成
 // 通過構造器將⽬標物件賦值
 public JdkHandler(Object target) {
 this.target = target;
 }
 /**
 * 1、調⽤⽬標物件的⽅法(返回Object)
 * 2、增強⽬標物件的⾏為
 * @param proxy 調⽤該⽅法的代理例項
 * @param method ⽬標物件的⽅法
 * @param args ⽬標物件的⽅法形參
 * @return
 * @throws Throwable
 */
@Override
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
 // 增強⾏為
 System.out.println("==============⽅法前執⾏");
 // 調⽤⽬標物件的⽅法(返回Object)
 Object result = method.invoke(target,args);
 // 增強⾏為
 System.out.println("⽅法後執⾏==============");
 return result;
 }
 /**
 * 得到代理物件
 * public static Object newProxyInstance(ClassLoader loader,
 * Class<?>[] interfaces,
 * InvocationHandler h)
 * loader:類載入器
 * interfaces:接⼝陣列
 * h:InvocationHandler接⼝ (傳⼊InvocationHandler接⼝的實現類)
 *
 *
 * @return
 */
 public Object getProxy() {
 return
Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterface
s(),this);
 }
}

通過代理物件實現目標物件的功能

// ⽬標物件
You you = new You();
// 獲取代理物件
JdkHandler jdkHandler = new JdkHandler(you);
Marry marry = (Marry) jdkHandler.getProxy();
// 通過代理物件調⽤⽬標物件中的⽅法
marry.toMarry(); 

問:Java 動態代理類中的 invoke 是怎麼呼叫的?

答:在生成的動態代理類 $Proxy0.class 中,構造方法呼叫了父類Proxy.class 的構造方法,給成員變數 invocationHandler 賦值,$Proxy0.class的 static 模組中建立了被代理類的方法,呼叫相應方法時方法體中呼叫了父類中的成員變數 InvocationHandler 的 invoke ()方法。

注:JDK 的動態代理依靠介面實現,如果有些類並沒有介面實現,則不能使用 JDK 代理。

CGLIB 動態代理

JDK 的動態代理機制只能代理實現了介面的類,而不能實現介面的類就不能使用 JDK 的動態代理,cglib 是針對類來實現代理的,它的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因為採用的是繼承,所以不能對 final 修飾的類進行代理。

新增依賴

在 pom.xml 檔案中引入 cglib 的相關依賴

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>2.2.2</version>
</dependency>

定義類

實現 MethodInterceptor 介面

public class CglibInterceptor implements MethodInterceptor {
 // ⽬標物件
 private Object target;
 // 通過構造器傳⼊⽬標物件
 public CglibInterceptor(Object target) {
 this.target = target;
 }
 /**
 * 獲取代理物件
 * @return
 */
 public Object getProxy() {
 // 通過Enhancer物件的create()⽅法可以⽣成⼀個類,⽤於⽣成代理物件
 Enhancer enhancer = new Enhancer();
 // 設定⽗類 (將⽬標類作為其⽗類)
 enhancer.setSuperclass(target.getClass());
 // 設定攔截器 回撥物件為本身物件
enhancer.setCallback(this);
 // ⽣成⼀個代理類物件,並返回
 return enhancer.create();
 }
 /**
 * 攔截器
 * 1、⽬標物件的⽅法調⽤
 * 2、增強⾏為
 * @param object 由CGLib動態⽣成的代理類例項
 * @param method 實體類所調⽤的被代理的⽅法引⽤
 * @param objects 引數值列表
 * @param methodProxy ⽣成的代理類對⽅法的代理引⽤
 * @return
 * @throws Throwable
 */
 @Override
 public Object intercept(Object object, Method method, Object[] objects,
 MethodProxy methodProxy) throws Throwable {
 // 增強⾏為
 System.out.println("==============⽅法前執⾏");
 // 調⽤⽬標物件的⽅法(返回Object)
 Object result = methodProxy.invoke(target,objects);
 // 增強⾏為
 System.out.println("⽅法後執⾏==============");
 return result;
 }
}

呼叫方法

// ⽬標物件
You you = new You();
CglibInterceptor cglibInterceptor = new CglibInterceptor(you);
Marry marry = (Marry) cglibInterceptor.getProxy();
marry.toMarry();
User user = new User();
CglibInterceptor cglibInterceptor = new CglibInterceptor(user);
User u = (User) cglibInterceptor.getProxy();
u.test();

JDK代理與CGLIB代理的區別

  • JDK 動態代理實現介面,Cglib 動態代理繼承思想
  • JDK 動態代理(目標物件存在介面時)執行效率高於 Ciglib
  • 如果目標物件有介面實現,選擇 JDK 代理,如果沒有介面實現選擇 Cglib 代理

Spring AOP

日誌處理帶來的問題

我們有一個 Pay (介面) 然後兩個實現類 DollarPay 和 RmbPay,都需要重寫 pay ()方法, 這時我們需要對 pay 方法進行效能監控,日誌的新增等等怎麼做?

最容易想到的方法

對每個字元方法均做日誌程式碼的編寫處理,如下面方式

缺點: 程式碼重複太多, 新增的日誌程式碼耦合度太高(如果需要更改日誌記錄程式碼功能需求,類中方法需要全部改動,工程量浩大)

使用裝飾器模式 /代理模式改進解決方案

裝飾器模式:動態地給一個物件新增一些額外的職責。

代理模式:以上剛講過。於是得出以下結構:

仔細考慮過後發現雖然對原有內部程式碼沒有進行改動,對於每個類做日誌處理,並引用目標類,但是如果待新增日誌的業務類的數量很多,此時手動為每個業務類實現一個裝飾器或建立對應的代理類,同時代碼的耦合度也加大,需求一旦改變,改動的工程量也是可想而知的。

有沒有更好的解決方案,只要寫一次程式碼,對想要新增日誌記錄的地方能夠實現程式碼的複用,達到鬆耦合的同時,又能夠完美完成功能?

答案是肯定的,存在這樣的技術,aop 已經對其提供了完美的實現!

什麼是AOP?

Aspect Oriented Programing 面向切面程式設計,相比較 oop 面向物件程式設計來說,Aop 關注的不再是程式程式碼中某個類,某些方法,而 aop 考慮的更多的是一種面到面的切入,即層與層之間的一種切入,所以稱之為切面。聯想大家吃的漢堡(中間夾肉)。那麼 aop 是怎麼做到攔截整個面的功能呢?考慮前面學到的 servlet filter /* 的配置 ,實際上也是 aop 的實現。

AOP能做什麼?

AOP 主要應用於日誌記錄,效能統計,安全控制,事務處理等方面,實現公共功能性的重複使用。

AOP的特點

  1. 降低模組與模組之間的耦合度,提高業務程式碼的聚合度。(高內聚低耦合)
  2. 提高了程式碼的複用性
  3. 提高了程式碼的複用性
  4. 可以在不影響原有的功能基礎上新增新的功能

AOP的底層實現

動態代理(JDK + CGLIB)

AOP基本概念

被攔截到的每個點,spring 中指被攔截到的每一個方法,spring aop 一個連線點即代表一個方法的執行。

Pointcut(切入點)

對連線點進行攔截的定義(匹配規則定義 規定攔截哪些方法,對哪些方法進行處理),spring 有專門的表示式語言定義。

Advice(通知)

攔截到每一個連線點即(每一個方法)後所要做的操作

  1. 前置通知 (前置增強)— before() 執行方法前通知
  2. 返回通知(返回增強)— afterReturn 方法正常結束返回後的通知
  3. 異常丟擲通知(異常丟擲增強)— afetrThrow()
  4. 最終通知 — after 無論方法是否發生異常,均會執行該通知。
  5. 環繞通知 — around 包圍一個連線點(join point)的通知,如方法呼叫。這是最強大的一種通知型別。 環繞通知可以在方法呼叫前後完成自定義的行為。它也會選擇是否繼續執行連線點或直接返回它們自己的返回值或丟擲異常來結束執行。

Aspect(切面)

切入點與通知的結合,決定了切面的定義,切入點定義了要攔截哪些類的哪些方法,通知則定義了攔截過方法後要做什麼,切面則是橫切關注點的抽象,與類相似,類是對物體特徵的抽象,切面則是橫切關注點抽象。

Target(目標物件)

被代理的目標物件

Weave(織入)

將切面應用到目標物件並生成代理物件的這個過程即為織入

Introduction(引入)

在不修改原有應用程式程式碼的情況下,在程式執行期為類動態新增方法或者欄位的過程稱為引入

Spring AOP的實現

Spring AOP環境搭建

座標依賴引入

<!--Spring AOP-->
<dependency>
 <groupId>org.aspectj</groupId>
 <artifactId>aspectjweaver</artifactId>
 <version>1.8.9</version>
</dependency>

新增 spring.xml 的配置

新增名稱空間

xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

註解實現

定義切面

/**
* 切⾯
* 切⼊點和通知的抽象 (與⾯向物件中的 類 相似)
* 定義 切⼊點和通知 (切⼊點定義了要攔截哪些類的哪些⽅法,通知則定義了攔截過⽅法後要做什麼)
*/
@Component // 將物件交給IOC容器去例項化
@Aspect // 聲明當前類是⼀個切⾯
public class LogCut {
 /**
 * 切⼊點:
 * 匹配規則。規定什麼⽅法被攔截、需要處理什麼⽅法
 * 定義切⼊點
 * @Pointcut("匹配規則")
 *
 * Aop 切⼊點表示式簡介
 * 1. 執⾏任意公共⽅法:
 * execution(public *(..))
 * 2. 執⾏任意的set⽅法
 * execution(* set*(..))
 * 3. 執⾏com.xxxx.service包下任意類的任意⽅法
 * execution(* com.xxxx.service.*.*(..))
* 4. 執⾏com.xxxx.service 包 以及⼦包下任意類的任意⽅法
 * execution(* com.xxxx.service..*.*(..))
 *
 * 注:表示式中的第⼀個* 代表的是⽅法的修飾範圍
 * 可選值:private、protected、public (* 表示所有範圍)
 */
 @Pointcut("execution (* com.xxxx.service..*.*(..) )")
 public void cut(){}
 /**
 * 宣告前置通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法執⾏前 執⾏該通知
 *
 */
 @Before(value = "cut()")
 public void before() {
 System.out.println("前置通知.....");
 }
 /**
 * 宣告返回通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法(⽆異常)執⾏後 執⾏該通知
 *
 */
 @AfterReturning(value = "cut()")
 public void afterReturn() {
 System.out.println("返回通知.....");
 }
 /**
 * 宣告最終通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法(⽆異常或有異常)執⾏後 執⾏該通知
 *
 */
 @After(value = "cut()")
 public void after() {
 System.out.println("最終通知.....");
 }
 /**
 * 宣告異常通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法出現異常時 執⾏該通知
 */
 @AfterThrowing(value="cut()",throwing = "e")
 public void afterThrow(Exception e) {
 System.out.println("異常通知....." + " 異常原因:" + e.getCause());
 }
 /**
 * 宣告環繞通知 並將通知應⽤到切⼊點上
 * ⽅法執⾏前後 通過環繞通知定義相應處理
 * 需要通過顯式調⽤對應的⽅法,否則⽆法訪問指定⽅法 (pjp.proceed();)
* @param pjp
 * @return
 */
 @Around(value = "cut()")
 public Object around(ProceedingJoinPoint pjp) {
 System.out.println("前置通知...");
 Object object = null;
 try {
 object = pjp.proceed();
 System.out.println(pjp.getTarget() + "======" + pjp.getSignature());
 // System.out.println("返回通知...");
 } catch (Throwable throwable) {
 throwable.printStackTrace();
 System.out.println("異常通知...");
 }
 System.out.println("最終通知...");
 return object;
 }
}

配置檔案(spring.xml)

<!--配置AOP代理-->
<aop:aspectj-autoproxy/>

XML實現

定義切面

**
* 切⾯
* 切⼊點和通知的抽象 (與⾯向物件中的 類 相似)
* 定義 切⼊點和通知 (切⼊點定義了要攔截哪些類的哪些⽅法,通知則定義了攔截過⽅法後要做什麼)
*/
@Component // 將物件交給IOC容器去例項化
public class LogCut02 {
 public void cut(){}
 /**
 * 宣告前置通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法執⾏前 執⾏該通知
 */
public void before() {
 System.out.println("前置通知.....");
 }
 /**
 * 宣告返回通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法(⽆異常)執⾏後 執⾏該通知
 *
 */
 public void afterReturn() {
 System.out.println("返回通知.....");
 }
 /**
 * 宣告最終通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法(⽆異常或有異常)執⾏後 執⾏該通知
 *
 */
 public void after() {
 System.out.println("最終通知.....");
 }
 /**
 * 宣告異常通知 並將通知應⽤到定義的切⼊點上
 * ⽬標類⽅法出現異常時 執⾏該通知
 */
 public void afterThrow(Exception e) {
 System.out.println("異常通知....." + " 異常原因:" + e.getCause());
 }
 /**
 * 宣告環繞通知 並將通知應⽤到切⼊點上
 * ⽅法執⾏前後 通過環繞通知定義相應處理
 * 需要通過顯式調⽤對應的⽅法,否則⽆法訪問指定⽅法 (pjp.proceed();)
 * @param pjp
 * @return
 */
 public Object around(ProceedingJoinPoint pjp) {
 System.out.println("前置通知...");
 Object object = null;
 try {
 object = pjp.proceed();
 System.out.println(pjp.getTarget() + "======" + pjp.getSignature());
 // System.out.println("返回通知...");
 } catch (Throwable throwable) {
throwable.printStackTrace();
 System.out.println("異常通知...");
 }
 System.out.println("最終通知...");
 return object;
 }
}

配置檔案(spring.xml)

<!--aop相關配置-->
<aop:config>
 <!--aop切⾯-->
 <aop:aspect ref="logCut02">
 <!-- 定義aop 切⼊點 -->
 <aop:pointcut id="cut" expression="execution(* com.xxxx.service..*.*(..))"/>
 <!-- 配置前置通知 指定前置通知⽅法名 並引⽤切⼊點定義 -->
 <aop:before method="before" pointcut-ref="cut"/>
 <!-- 配置返回通知 指定返回通知⽅法名 並引⽤切⼊點定義 -->
 <aop:after-returning method="afterReturn" pointcut-ref="cut"/>
 <!-- 配置異常通知 指定異常通知⽅法名 並引⽤切⼊點定義 -->
 <aop:after-throwing method="afterThrow" throwing="e" pointcut-ref="cut"/>
 <!-- 配置最終通知 指定最終通知⽅法名 並引⽤切⼊點定義 -->
 <aop:after method="after" pointcut-ref="cut"/>
 <!-- 配置環繞通知 指定環繞通知⽅法名 並引⽤切⼊點定義 -->
 <aop:around method="around" pointcut-ref="cut"/>
 </aop:aspect>
</aop:config>

Spring AOP總結

代理模式實現三要素

  1. 介面定義
  2. 目標物件與代理物件必須實現統一介面
  3. 代理物件持有目標物件的引用,增強目標物件行為

代理模式實現分類以及對應區別

  1. 靜態代理:手動為目標物件製作代理物件,即在程式編譯階段完成代理物件的建立
  2. 動態代理:在程式執行期動態建立目標物件對應代理物件。
  3. jdk 動態代理:被代理目標物件必須實現某一或某一組介面實現方式通過回撥建立代理物件。
  4. cglib 動態代理:被代理目標物件可以不必實現介面,繼承的方式實現

動態代理相比較靜態代理,提高開發效率,可以批量化建立代理,提高程式碼複用率。

Aop 理解

  1. 面向切面,相比 oop 關注的是程式碼中的層或面
  2. 解耦,提高系統擴充套件性
  3. 提高程式碼複用

Aop 關鍵詞

  1. 連線點:每一個方法
  2. 切入點:匹配的方法集合
  3. 切面:連線點與切入點的集合決定了切面,橫切關注點的抽象
  4. 通知:幾種通知
  5. 目標物件:被代理物件
  6. 織入:程式執行期將切面應用到目標物件並生成代理物件的過程
  7. 引入:在不修改原始程式碼情況下,在程式執行期為程式動態引入方法或欄位的過程

最後

感謝你看到這裡,文章有什麼不足還請指正,覺得文章對你有幫助的話記得給我點個贊!