1. 程式人生 > 實用技巧 >仔細想想SpringAOP也不難嘛,面試沒有必要慌

仔細想想SpringAOP也不難嘛,面試沒有必要慌

文章已託管到GitHub,大家可以去GitHub檢視閱讀,歡迎老闆們前來Star!

搜尋關注微信公眾號 碼出Offer 領取各種學習資料!


LOGO

SpringAOP


一、什麼是AOP

AOP(Aspect Oriented Programming),即面向切面程式設計,利用一種稱為"橫切"的技術,剖開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。

應用場景: 如日誌記錄、審計、宣告式事務、安全性和快取等。

二、場景分析

為了更好的理解AOP,滲透面向切面程式設計的思想。我這裡舉一個開發中很常見的例子。列印日誌

首先,我們要先理解什麼是日誌。

日誌: 日誌是一種可以追蹤某些軟體執行時所發生事件的方法,軟體開發人員可以向他們的程式碼中呼叫日誌記錄相關的方法來表明發生了某些事情。一個事件可以用一個可包含可選變數資料的訊息來描述,此外,事件也有重要性的概念,這個重要性也可以被稱為嚴重性級別(level)。開發者可以通過區分嚴重性級別來分析出想要的資訊。

瞭解了什麼是日誌,那就要知道怎麼列印日誌,在哪裡列印日誌。列印日誌,是引入依賴,使用日誌工具來實現日誌嚴重性級別和日誌資訊的列印。至於在哪裡列印日誌,當然是在我們專案程式碼中的關鍵位置了。

這裡我們舉一個例子在某一段程式碼的前後使用,有A、B、C三個方法,但是要在呼叫每一個方法之前,要求列印一行日誌“某方法被呼叫了!”,在呼叫每個方法之後,也要求列印日誌“某方法被呼叫完畢!”。

一般人會在每一個方法的開始和結尾部分都會新增一句日誌列印吧,這樣做如果方法多了,就會有很多重複的程式碼,顯得很麻煩,這時候有人會想到,為什麼不把列印日誌這個功能封裝一下,然後讓它能在指定的地方(比如執行方法前,或者執行方法後)自動的去呼叫呢?如果可以的話,業務功能程式碼中就不會摻雜這一下其他的程式碼,所以AOP就是做了這一類的工作的。

其工作原理為JDK動態代理和CGLIB動態代理,這裡就先不展開動態代理的知識了!還是先看AOP吧!

三、AOP術語

AOP作用: Spring的AOP程式設計即是通過動態代理類為原始類的方法新增輔助功能。

AOP術語 描述
連線點(Joinpoint) 連線點是程式類中客觀存在的方法,可被Spring攔截並切入內容
切入點(Pointcut) 被Spring切入連線點
通知、增強(Advice) 可以為切入點新增額外功能,分為:前置通知、後置通知、異常通知、環繞通知等。
目標物件(Target) 代理的目標物件
引介(Introduction) 一種特殊的增強,可在執行期為類動態新增Field和Method
織入(Weaving) 把通知應用到具體的類,進而建立新的代理類的過程
代理(Proxy) 被AOP織入通知後,產生的結果類
切面(Aspect) 由切點和通知組成,將橫切邏輯織入切面所指定的連線點中

四、AOP術語解析

3.1 連線點

簡單來說,就是允許你使用通知、增強的地方。就比如在方法前後列印日誌一樣,我們可以在一段程式碼的前後做操作,可以在一段程式碼前做操作,可以在一段程式碼後做操作,可以在一段程式碼拋異常之後做操作。所以,在這裡這些可以操作的一行行程式碼(方法等等)都是一個個的連線點。

3.2 切入點

把一個個方法等程式碼看作連線點,那我們從哪個位置列印日誌(增強操作)呢,而我們挑選出需要列印日誌的位置(也就是連線點的周圍),就被稱為切入點。

3.3 增強、通知

關於增強,在上面我已經說到過了,通過在切入點做的操作叫做增強,比如我們要列印日誌的話,日誌就是一個增強功能操作。

3.4 目標物件

目標物件,簡單來說是要被增強的物件。

3.5 引介

允許我們向現有的類新增新方法屬性。這不就是把切面(也就是增強定義的新方法屬性)用到目標物件中

3.6 織入

把增強應用到具體的目標物件中,進而建立新的代理類的過程

3.7 代理

代理就像我們買房子的中介一樣,也就是被AOP織入後產生的代理物件(中介物件),通過代理物件可以實現對我們的目標物件增強

3.8 切面

切面是通知(增強)和切入點的結合。通知說明瞭幹什麼和什麼時候幹,而切入點說明瞭在哪幹,這就是一個完整的切面定義。

五、SpringAOP開發步驟

5.1 pom.xml檔案引入依賴

引入Spring核心依賴(spring-context)和SpringAOP依賴(spring-aspects)

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>

5.2 建立spring-context.xml檔案並新增schema

我們需要在核心配置檔案的標頭檔案中新增aop和context的Schema

<?xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>

5.3 定義原始類

模擬建立一個原始類

publicinterfaceUserService{
publicvoidsave();
}

publicclassUserServiceImplimplementsUserService{
publicvoidsave(){
System.out.println("savemethodexecuted...");
}
}

5.4 定義通過類

定義通知類(新增額外功能做增強)

publicclassMyAdviceimplementsMethodBeforeAdvice{//實現前置通知介面
publicvoidbefore(Methodmethod,Object[]args,Objecttarget)throwsThrowable{
System.out.println("beforeadviceexecuted...");
}
}

5.5 定義bean

配置bean物件

<!--原始物件-->
<beanid="us"class="com.mylifes1110.service.impl.UserServiceImpl"/>

<!--輔助(增強)物件-->
<beanid="myAdvice"class="com.mylifes1110.advice.MyAdvice"/>

5.6 定義切入點形成切面

定義切入點(PointCut)並形成切面(Aspect)

<aop:config>
<!--切點-->
<aop:pointcutid="myPointCut"expression="execution(*save())"/>
<!--組裝切面-->
<aop:advisoradvice-ref="myAdvice"pointcut-ref="myPointCut"/>
</aop:config>

5.7 增強結果

使用的前置通知,結果增強的列印語句before advice executed...會在save()方法的前面列印save method executed...

六、通知

定義通知類,達到通知(增強)效果。實現不同的介面並覆蓋方法來達到不同的通知效果

通知名稱 介面 描述
前置通知 MethodBeforeAdvice介面 在目標物件的前面做增強
後置通知 AfterAdvice介面 注意:此介面內方法為空,後置預設使用第三種即可
後置通知 AfterReturningAdvice介面 在目標物件的後面做增強
異常通知 ThrowsAdvice 在目標物件發生異常後做增強
環繞通知 MethodInterceptor 在目標物件的前後做增強

七、通配切入點

根據表示式通配切入點

通配表示式順序: 返回值型別 全類名.方法名(形參)

注意: 可以用..來實現通配形參列表,可以使用*來通配方法名或返回值型別

<!--publicintcom.mylifes1110.service.UserServiceImpl.queryUser(int,String,com.entity.User)-->
<!--匹配引數-->
<aop:pointcutid="myPointCut"expression="execution(**(com.mylifes1110.bean.User))"/>
<!--匹配方法名(無參)-->
<aop:pointcutid="myPointCut"expression="execution(*save())"/>
<!--匹配方法名(任意引數)-->
<aop:pointcutid="myPointCut"expression="execution(*save(..))"/>
<!--匹配返回值型別-->
<aop:pointcutid="myPointCut"expression="execution(com.mylifes1110.bean.User*(..))"/>
<!--匹配類名-->
<aop:pointcutid="myPointCut"expression="execution(*com.mylifes1110.bean.UserServiceImpl.*(..))"/>
<!--匹配包名-->
<aop:pointcutid="myPointCut"expression="execution(*com.mylifes1110.bean.*.*(..))"/>
<!--匹配包名、以及子包名-->
<aop:pointcutid="myPointCut"expression="execution(*com.mylifes1110..*.*(..))"/>

八、代理模式

8.1 代理模式

將核心功能與輔助功能(事務、日誌、效能監控程式碼)分離,達到核心業務功能更純粹、輔助業務功能可複用。

功能分離

image-20190420002535800

8.2 代理模式應用場景模擬

通過代理類的物件,為原始類的物件(目標類的物件)新增輔助功能,更容易更換代理實現類、利於維護。

場景模擬: 我們在租賃房子需要走如下流程:

  1. 釋出租房資訊
  2. 帶租戶看房
  3. 籤合同
  4. 收房租

可如果你是房東,生活中還有其他的瑣事,怎麼辦呢?那你是不是可以把不重要不核心的環節交給中介(代理)去做呢?比如:釋出租房資訊和帶租戶看房。這兩件事情交給中介去做就好了,我們自己處理自己的事情,而且中間聯絡好租戶我們走比較重要的流程就可以,比如籤合同、收房租。

8.3 建立Service介面和實現類

建立Service介面和實現類來模擬動態代理的應用場景

packagecom.mylifes1110.service;

publicinterfaceLandlordService{
voidrent();
}

packagecom.mylifes1110.service.impl;

importcom.mylifes1110.service.LandlordService;

publicclassLandlordServiceImplimplementsLandlordService{
@Override
publicvoidrent(){
System.out.println("籤合同");
System.out.println("收款");
}
}

8.4 靜態代理

如下是靜態代理設計模式解決代理問題

  • 靜態代理流程,建立一個代理類並實現相同介面,建立實現類物件,在代理類中新增輔助功能並呼叫實現類物件核心方法,使得輔助功能和核心方法一起觸發,完成代理
  • 靜態代理的問題
  • 隨著輔助功能的數量增加,代理類也會增加,導致代理類數量過多,不利於專案的管理。
  • 多個代理類的輔助功能程式碼冗餘,修改時,維護性差。
靜態代理

image-20190420004330551

建立靜態代理類

packagecom.mylifes1110.advice1;

importcom.mylifes1110.service.LandlordService;
importcom.mylifes1110.service.impl.LandlordServiceImpl;

/**
*@ClassNameProxy
*@Description靜態代理類
*@AuthorZiph
*@Date2020/7/19
*@Since1.8
*@Version1.0
*/

publicclassProxyimplementsLandlordService{
privateLandlordServicelandlordService=newLandlordServiceImpl();

@Override
publicvoidrent(){
//代理事件
System.out.println("釋出訊息");
System.out.println("看房子");
//核心事件
landlordService.rent();
}
}

靜態代理實現

packagecom.mylifes1110.advice1;

importorg.junit.Test;

publicclassProxyTest{
/**
*@MethodNameproxyTest
*@Param[]
*@Description靜態代理實現
*@AuthorZiph
*@Date2020/7/10
*/
@Test
publicvoidproxyTest(){
newProxy().rent();
}
/**
*結果:
*
*釋出訊息
*看房子
*籤合同
*收款
*/
}

8.5 JDK和CGLIB的選擇

spring底層,包含了jdk代理和cglib代理兩種動態代理生成機制。

基本規則是:目標業務類如果有介面則用JDK代理,沒有介面則用CGLib代理。如果配置true:,則用CGLIB代理

classDefaultAopProxyFactory{
//該方法中明確定義了JDK代理和CGLib代理的選取規則
//基本規則是:目標業務類如果有介面則用JDK代理,沒有介面則用CGLib代理
publicAopProxycreateAopProxy(){...}
}

8.6 JDK動態代理

JDK動態代理是JDK底層基於介面實現的,也就是說我們必須通過實現JDK動態代理的介面並覆蓋方法來完成

packagecom.mylifes1110.advice2;

importcom.mylifes1110.service.LandlordService;
importcom.mylifes1110.service.impl.LandlordServiceImpl;
importorg.junit.Test;

importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.lang.reflect.Proxy;

publicclassProxyTest{
/**
*@MethodNameproxyTest
*@Param[]
*@DescriptionJDK動態代理實現
*@AuthorZiph
*@Date2020/7/10
*/
@Test
publicvoidproxyTest(){
//需要使用代理的目標
LandlordServicelandlordService=newLandlordServiceImpl();
//匿名內部類
InvocationHandlerhandler=newInvocationHandler(){
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
//代理事件
System.out.println("釋出訊息");
System.out.println("看房子");
returnmethod.invoke(landlordService,args);
}
};

//動態構建代理類
LandlordServiceproxy=(LandlordService)Proxy.newProxyInstance(ProxyTest.class.getClassLoader(),
landlordService.getClass().getInterfaces(),
handler);

proxy.rent();

/**
*結果:
*
*釋出訊息
*看房子
*籤合同
*收款
*/
}
}

8.7 CGLIB動態代理

CGLIB動態代理是Spring底層基於繼承父類實現的,也就是說我們必須通過繼承所指定的父類並覆蓋其方法來完成

packagecom.mylifes1110.advice3;

importcom.mylifes1110.service.LandlordService;
importcom.mylifes1110.service.impl.LandlordServiceImpl;
importorg.springframework.cglib.proxy.Enhancer;
importorg.springframework.cglib.proxy.InvocationHandler;

importjava.lang.reflect.Method;

/**
*@ClassNameProxyTest
*@DescriptionCGLIB動態代理實現
*@AuthorZiph
*@Date2020/7/19
*@Since1.8
*@Version1.0
*/

publicclassProxyTest{
publicstaticvoidmain(String[]args){
finalLandlordServicelandlordService=newLandlordServiceImpl();
//建立位元組碼增強物件
Enhancerenhancer=newEnhancer();
//設定父類(等價於實現原始類介面)
enhancer.setSuperclass(landlordService.getClass());
//設定回撥函式(額外功能程式碼)
enhancer.setCallback(newInvocationHandler(){
@Override
publicObjectinvoke(Objecto,Methodmethod,Object[]objects)throwsThrowable{
//代理事件
System.out.println("釋出訊息");
System.out.println("看房子");
Objectret=method.invoke(landlordService,args);
returnret;
}
});
//建立動態代理類
LandlordServiceproxy=(LandlordService)enhancer.create();
proxy.rent();
/**
*結果:
*
*釋出訊息
*看房子
*籤合同
*收款
*/
}
}

九、後處理器

9.1 後處理器的瞭解

  • spring中定義了很多後處理器;
  • 每個bean在建立完成之前 ,都會有一個後處理過程,即再加工,對bean做出相關改變和調整;
  • spring-AOP中,就有一個專門的後處理器,負責通過原始業務元件(Service),再加工得到一個代理元件。
常用後處理器

系統後處理器

9.2 定義後處理器

/**
*定義bean後處理器
*作用:在bean的建立之後,進行再加工
*/
publicclassMyBeanPostProcessorimplementsBeanPostProcessor{

/**
*在bean的init方法之前執行
*@parambean原始的bean物件
*@parambeanName
*@return
*@throwsBeansException
*/
publicObjectpostProcessBeforeInitialization(Objectbean,StringbeanName)throwsBeansException{
System.out.println("後處理器在init之前執行~~~"+bean.getClass());
returnbean;
}
/**
*在bean的init方法之後執行
*@parambeanpostProcessBeforeInitialization返回的bean
*@parambeanName
*@return
*@throwsBeansException
*/
@Override
publicObjectpostProcessAfterInitialization(Objectbean,StringbeanName)throwsBeansException{
System.out.println("後處理器在init之後執行~~~"+bean.getClass());
returnbean;//此處的返回是getBean()最終的返回值
}
}

9.3 配置後處理器

<!--配置後處理器,將對工廠中所有的bean宣告週期進行幹預-->
<beanclass="com.mylifes1110.beanpostprocessor.MyBeanPostProcessor"></bean>

9.4 Bean的生命週期

建立Bean物件 -> 構造方法 -> Setter方法注入屬性、滿足依賴 -> 後處理器前置過程 -> init初始化 -> 後處理器後置過程 -> 構建完成 -> 銷燬

9.5 動態代理原始碼(瞭解)

//AbstractAutoProxyCreator是AspectJAwareAdvisorAutoProxyCreator的父類
//該後處理器類中的wrapIfNecessary方法即動態代理生成過程
AbstractAutoProxyCreator#postProcessAfterInitialization(Objectbean,StringbeanName){
if(!this.earlyProxyReferences.contains(cacheKey)){
//開始動態定製代理
returnwrapIfNecessary(bean,beanName,cacheKey);
}
}

記得關注我哦!拜拜,下期見!