Spring AOP學習筆記01:AOP概述
-
AOP概述
軟體開發一直在尋求更加高效、更易維護甚至更易擴充的方式。為了提高開發效率,我們對開發使用的語言進行抽象,走過了從彙編時代到現在各種進階語言繁盛之時期;為了便于維護和擴充,我們對某些相同的功能進行歸類并使之子產品化,沖出了最初的"原始部落",走過了從過程化程式設計到面向對象程式設計(OOP)的"短暫而漫長"的曆程。但不管走過的路有多長,多麼坎坷,我們一直沒有停止尋找更加完美、更加高效的軟體開發方法,過去如此,現在亦然。
當OOP被提出來,以取代過去基于過程化程式設計的開發方法時,或許那個時代的人都會以為,面向對象程式設計和面向對象的軟體開發就是我們一直追求的那顆能夠搞定一切的"銀彈"。但不得不承認的是,即使面向對象的軟體開發模式,依然不能很好地解決軟體開發中的所有問題。
軟體開發的目的,最終是為了解決各種需求,包括業務需求和系統需求。使用面向對象方法,我們可以對業務需求等普通關注點進行很好的抽象和封裝,并且使之子產品化。但對于系統需求(比如日志記錄、權限驗證、事務管理等)一類的關注點來說,情況卻有所不同。
對于業務需求而言,需求與其具體實作之間的關系基本上是一對一的。我們可以在系統中某一個确定的點找到針對這種需求的實作,無論從開發還是維護的角度,都比較友善。比如電商系統中的賬戶管理子產品、訂單子產品、支付子產品等,可以很容易地按照功能劃分子產品并完成開發。
但是,事情并沒有結束!開發中為了調試或在進入生産環境後為了對系統進行監控,我們需要為這些業務需求的實作對象添加日志記錄功能;或者,業務方法的執行需要一定的權限限制,那麼方法執行前肯定需要有相應的安全檢查功能。而這些則屬于系統需求的範疇。雖然需求都很明确(加入日志記錄、加入安全檢查),但是要将這些需求以面向對象的方式實作并內建到整個的系統中去,可就不是一個需求對應一個實作那麼簡單了,系統中的每個業務對象都需要加入日志記錄,加入相應的安全檢查,那麼,這些需求的實作代碼就會遍及所有業務對象。
對于系統中普通的業務關注點,OOP可以很好地對其進行分解并使之子產品化,但卻無法更好地避免類似于系統需求的實作在系統中各處散落這樣的問題。是以,我們要尋求一種更好的方法,它可以在OOP的基礎上更上一層樓,提出一套全新的方法論來避免以上問題,也可以提供某種方法對基于OOP的開發模式做一個補足,幫助OOP以更好的方式解決以上問題。迄今為止,我們還找不到比OOP更加有效的軟體開發模式。不過,我們找到了後者,那就是AOP,對OOP的補足。
AOP全稱為Aspect-Oriented Programming,中文通常翻譯為面向方面程式設計。使用AOP,我們可以對類似于Logging和Security等系統需求進行子產品化的組織,簡化系統需求與實作之間的對比關系,進而使得整個系統的實作更具子產品化。
對于一個軟體系統而言,日志記錄、安全檢查、事務管理等系統需求就像一把把刀“惡狠狠”地橫切到我們組織良好的各個業務功能子產品之上。以AOP的行話來說,這些系統需求是系統中的橫切關注點(cross-cutting concern)。使用傳統方法,我們無法更好地以子產品化的方式,對這些橫切關注點進行組織和實作。是以AOP引入了Aspect的概念,用來以子產品化的形式對系統中的橫切關注點進行封裝。Aspect 之對于AOP,就相當于Class之對于OOP。我們說過AOP僅是對OOP方法的一種補足,當我們把以Class形式子產品化的業務需求和以Aspect形式子產品化的系統需求拼裝到一起的時候,整個系統就算完成了。
-
AOP相關概念
在進一步學習Spring AOP之前,我們還需要了解一下AOP涉及的相關概念:
2.1 切點(JoinPoint)
在系統運作之前,AOP的功能子產品都需要織入到OOP的功能子產品中。是以,要進行這種織入過程,我們需要知道在系統的哪些執行點上進行織入操作,這些将要在其之上進行織入操作的系統執行點就稱之為切點(Joinpoint)。對應到spring中可以了解為具體攔截的某個業務點。
以下是一些較為常見的Joinpoint類型
方法調用(Method Call)。當某個方法被調用的時候所處的程式執行點。
方法調用執行(Method Call execution)。也可以稱之為方法執行,該Joinpoint類型代表的是某個方法内部執行開始時點,這需要與上面的方法調用類型的Jointpoint進行區分。方法調用(method call)是在調用對象上的執行點,而方法執行(method execution)則是在被調用到的方法邏輯執行的時點,對于同一對象,方法調用要先于方法執行。
構造方法調用(Constructor Call)。程式執行過程中對某個對象調用其構造方法進行初始化的時點。
構造方法執行(Constructor Call Execution)。構造方法執行和構造方法調用之間的關系類似于方法執行和方法調用之間的關系,指的是某個對象構造方法内部執行的開始時點。
字段設定(Field Set)。對象的某個屬性通過setter方法被設定或者直接被設定的時點。
字段擷取(Field Get)。對象的某個屬性通過getter方法擷取或者直接通路的時點。
異常處理(Exception Handler Execution)。在某些類型異常抛出後,對應的異常處理邏輯執行的時點。
類初始化(Class initialization)。類中某些靜态類型或者靜态塊的初始化時點。
基本上程式執行過程中你認為必要的執行時點都可以作為Joinpoint,但是對于一些位置,具體的AOP實作産品在捕捉的時候可能存在一定的困難,或者能夠實作但付出太多卻可能收效甚微。在Spring AOP中最常見的就是前面的方法執行類型的Joinpoint。
2.2 切面(Pointcut)
Pointcut概念代表的是JointPoint的表述方式。将橫切邏輯織入目前系統的過程中,需要參照Pointcut規定的Jointpoint資訊,才可以知道應該往系統的哪些Joinpoint上織入橫切邏輯。
一個Pointcut可以指定系統中符合條件的一組Joinpoint,但是其是如何來指定的呢?通常有如下幾種方式:
直接指定Joinpoint所在方法名稱。這種形式的Pointcut表述方式比較簡單,而且功能單一,通常隻限于支援方法級别Joinpoint的AOP架構。并且這種方式隻能一個一個指定,是以通常隻限于Joinpoint較少且較為簡單的情況。
正規表達式。這是比較普遍的Pointcut表達方式,可以充分利用正規表達式的強大功能來歸納表述符合某種條件的多組Joinpoint。幾乎現在大部分的Java平台的AOP産品都支援這種形式的Pointcut表達形式,包括Jboss AOP、Spring AOP以及AspectWerkz等。
使用特定的Pointcut表述語言。這是一種最為強大的表達Pointcut的方式,很靈活,但具體實作起來可能會很複雜,需要設計該表述語言的文法,實作相應的解釋器等許多工作。AspectJ使用這種方式來指定Pointcut,它提供了一種類似于正規表達式的針對Pointcut的表述語言,在表達Pointcut方面支援比較完善,而且Spring 2.0之後也是支援這種方式。
2.3 通知(Advice)
Advice是單一橫切關注點邏輯的載體,它代表将會織入到Joinpoint的橫切邏輯。如果将Aspect比作OOP中的Class,那麼Advice就相當于Class中的Method。
按照Advice在Jointpoint位置執行時機的差異或者完成功能的不同,Advice可以分成多種具體形式。
Before Advice
Before Advice是在Joinpoint指定位置之前執行的Advice類型。通常,它不會中斷程式執行流程,但如果必要,可以通過在Before Advice中抛出異常的方式來中斷目前程式流程。如果目前Before Advice将被織入到方法執行類型的Joinpoint,那麼這個Before Advice就會先于方法執行而執行。 通常,可以使用Before Advice做一些系統的初始化工作,比如設定系統初始值,擷取必要系統資源。
After Advice
顧名思義,After Advice就是在相應連接配接點之後執行的Advice類型,但該類型的Advice還可以細分為三種:
After returning Advice。隻有目前Joinpoint處執行流程正常完成後,After returning Advice才會執行。
After throwing Advice。又稱Throws Advice,隻有在目前Joinpoint執行過程中抛出異常的情況下,才會執行。比如某個方法執行類型的Joinpoint抛出某異常而沒有正常傳回。
After Advice。或許叫After (Finally) Advice更為确切,該類型Advice不管Joinpoint處執行流程是正常終了還是抛出異常都會執行,就好像Java中的finally塊一樣。
Around Advice
Around Advice對附加其上的Joinpoint進行"包裹",可以在Joinpoint之前和之後都指定相應的邏輯,甚至于中斷或者忽略Joinpoint處原來程式流程的執行。
2.4 Aspect
Aspect是對系統中的橫切關注點邏輯進行子產品化封裝的AOP概念實體,可以了解為攔截器類,其中會定義切點以及攔截處理邏輯。通常情況下,Aspect可以包含多個Pointcut以及相關Advice定義。在Spring中,是通過使用@AspectJ注解并結合普通POJO來聲明Aspect的。
@AspectJ
public class AspectClass{
// pointcut 定義
// advice 定義
}
2.5 目标對象
符合Pointcut所指定的條件,将在織入過程中被織入橫切邏輯的對象,稱為目标對象(Target Object)。
-
Spring AOP
AOP隻是一種理念,要實作這種理念,通常需要一種現實的方式。Spring AOP就是一款AOP的實作産品,Spring AOP是Spring核心架構的重要組成部分,通常認為它與Spring的IoC容器以及Spring架構對其他JavaEE服務的內建共同組成了Spring架構的"品質三角",足見其地位之重要。
在Java語言的基礎之上,Spring AOP對AOP的概念進行了适當的抽象和實作,使得每個AOP的概念都可以落到實處,在詳細學習Spring AOP概念實體之前,我們有必要先看一下其是如何運作的。
Spring AOP從最初釋出以來,一直延續了最初的設計,也就是采用動态代理機制和位元組碼生成技術來實作基于Java語言的簡單而強大的AOP架構。與最初的AspectJ采用編譯器将橫切邏輯織入目标對象不同,動态代理機制和位元組碼生成都是在運作期間為目标對象生成一個代理對象,再将橫切邏輯織入到這個代理對象中,系統最終使用的是織入了橫切邏輯的代理對象,而不是真正的目标對象。
要了解這種差别以及最終可以達到的效果,有必要先從動态代理機制的根源--代理模式(Proxy Pattern)開始說起。。。
3.1 設計模式之代理模式
說到代理,舉幾個簡單的例子,比如房地産中介就是一種代理,我們偶爾使用的網絡代理也是一種代理,類似例子很多,就不一一列舉了。代理處于通路者與被通路者之間,可以隔離這兩者之間的直接互動,通路者與代理打交道就好像在跟被通路者在打交道一樣,因為代理通常幾乎會全權擁有被代理者的職能,代理能夠處理的通路請求就不必要勞煩被通路者來處理了。從這個角度來講,有兩個好處:
代理可以減少被通路者的負擔;
即使代理最終要将通路請求轉發給真正的被通路者,它也可以在轉發通路請求之前或者之後加入特定的邏輯,比如安全通路限制;
在軟體系統中,代理機制的實作有現成的設計模式支援,即代理模式。在代理模式中通常涉及4種角色:
ISubject。該接口是對被通路者或者被通路資源的抽象。在嚴格的設計模式中,這樣的抽象接口是必須的。
SubjectImpl。這是被通路者或者被通路資源的具體實作類。如果你要通路某位明星,那麼SubjectImpl就是你想要通路的明星;如果你想要買房子,那麼SubjectImpl就是房主。
SubjectProxy。這是被通路者或者被通路資源的代理實作類,該類持有一個ISubject接口的具體執行個體。在這個場景中,我們要對SubjectImpl進行代理,那麼SubjectProxy現在持有的就是SubjectImpl的執行個體。
Client。這代表通路者的抽象角色,Client将會通路ISubject類型的對象或者資源。在這個場景中,Client将會請求具體的SubjectImpl執行個體,但Client無法直接請求其真正要通路的資源SubjectImpl,而是必須通過ISubject資源的通路代理類SubjectProxy進行。
SubjectImpl和SubjectProxy都實作了相同的接口ISubject,而SubjectProxy内部持有SubjectImpl的引用。當Client通過request()請求服務的時候,SubjectProxy将轉發該請求給SubjectImpl。從這個角度來說,SubjectProxy反而有多此一舉之嫌了,不過SubjectProxy的作用不隻局限于請求的轉發,更多時候是對請求添加更多通路限制。SubjectImpl和SubjectProxy之間的調用關系如下代碼所示:
public class SubjectProxy implements ISubject{
private ISubject subject; // Inject SubjectImpl to SubjectProxy
public String request(){
// add pre-process logic if necessary
String originalResult = subject.request();
// add post process logic if necessary
return "Proxy:" + originalResult;
}
public ISubject getSubject(){
return subject;
}
public void setSubject(ISubject subject){
this.subject = subject;
}
public class SubjectImpl implements ISubject{
public String request(){
// process logic
return "OK";
}
在将請求轉發給被代理對象SubjectImpl之前或者之後,都可以根據情況插入其他處理邏輯,比如在轉發之前記錄方法執行開始時間,在轉發之後記錄結束時間,這樣就能夠對SubjectImpl的request()執行的時間進行檢測。或者,可以隻在轉發之後對SubjectImpl的request()方法傳回結果進行覆寫,傳回不同的值。甚至,可以不做請求轉發,這樣,就不會有SubjectImpl的通路發生。
代理對象SubjectProxy就像是SubjectImpl的影子,隻不過這個影子通常擁有更多的功能。如果SubjectImpl是系統中Jointpoint所在的對象(即目标對象),那麼就可以為這個目标對象建立一個代理對象,然後将橫切邏輯添加到這個代理對象中。當系統使用這個代理對象的時候,原有邏輯的實作和橫切邏輯就完全融合到一個系統中。
Spring AOP本質上就是采用這種代理機制實作的,但是,具體實作細節上有所不同。我們來看一下上面的代理實作,我們是将代理類直接寫好,然後在代碼中手動初始化代理類并通過調用代理類來實作代理功能,發現沒有,如果系統裡面有很多類需要代理相同的能,那麼我們就要寫很多的代理類,盡管它們代理的内容是一樣的,這樣是有問題的。上面這種為對應的目标對象建立靜态代理的方法,原理上是可行的,但具體應用上存在問題,是以要尋找其他方法,那有沒有呢,答案是肯定有的,就是接下來我們要講的動态代理。
3.2 動态代理
JDK1.3之後,引入了動态代理(Dynamic Proxy)機制,可以在運作期間,為相應的接口(Interface)動态生成對應的代理對象,進而幫助我們走出最初使用靜态代理實作AOP的窘境。
動态代理機制的實作主要由一個類和一個接口組成,即java.lang.reflect.Proxy類和java.lang.reflect.InvocationHandler接口。InvacationHandler就是我們實作橫切邏輯的地方,它是橫切邏輯的載體,作用跟Advice是一樣的。是以在使用動态代理機制實作AOP的過程中,我們可以在InvocationHandler的基礎上細化程式結構,根據Advice的類型,分化出對應不同的Advice類型的程式結構。
是以,我們可以将橫切關注點邏輯封裝到動态代理的InvocationHandler中,然後在系統運作期間,根據橫切關注點需要織入的子產品位置,将橫切邏輯織入到相應的代理類中。以動态代理類為載體的橫切邏輯,現在當然就可以與系統其他實作子產品一起工作了。
動态代理雖好,但不能滿足所有的需求,這種方式實作的唯一缺點或者說優點就是,所有需要織入橫切關注點邏輯的子產品類都得實作相應的接口,因為動态代理機制隻針對接口有效。如果某個類沒有實作任何的接口,就無法使用動态代理機制為其生成相應的動态代理對象。對于沒有實作任何接口的目标對象我們需要尋找其他方式為其動态的生成代理對象。
預設情況下,Spring AOP發現目标對象實作了相應接口,則采用動态代理機制為其生成代理對象執行個體。而如果目标對象沒有實作任何接口,Spring AOP則會嘗試使用一個稱為CGLIB(Code Generation Library)的開源的動态位元組碼生成類庫,為目标對象生成動态的代理對象執行個體。
3.3 動态位元組碼增強
使用動态位元組碼生成技術擴充對象行為的原理是,我們可以對目标對象進行繼承擴充,為其生成相應的子類,而子類可以通過覆寫來擴充父類的行為,隻要将橫切邏輯的實作放到子類中,然後讓系統使用擴充後的目标對象的子類,就可以達到與代理模式相同的效果了。
但是使用繼承的方式來擴充對象定義,也不能像靜态代理模式那樣,為每個不同類型的目标對象都單獨建立相應的擴充子類。是以,我們要借助于CGLIB這樣的動态位元組碼生成庫,在系統運作期間動态地為目标對象生成相應的擴充子類。
我們知道,Java虛拟機加載的檔案都是符合一定規範的,是以,隻要交給Java虛拟機運作的檔案符合Java class規範,程式的運作就沒有問題。通常的class檔案都是從Java源代碼檔案使用Javac編譯器編譯而成的,但隻要符合Java class規範,我們也可以使用ASM或者CGLiB等Java工具庫,在程式運作期間,動态建構位元組碼的class檔案。
在這樣的前提下,我們可以為需要織入橫切邏輯的子產品類在運作期間,通過動态位元組碼增強技術,為這些系統子產品類生成相應的子類,而将橫切邏輯加到這些子類中,讓應用程式在執行期間使用從這些動态生成的子類,進而達到将橫切邏輯織入系統的目的。 使用動态位元組碼增強技術,即使子產品類沒有實作相應的接口,我們依然可以對其進行擴充,而不用像動态代理那樣受限于接口。不過,這種實作機制依然存在不足,如果需要擴充的類以及類中的執行個體方法等聲明為final的話,則無法對其進行子類化的擴充。
3.4 一個spring aop示例
上面說了這麼多,下面就來看一個簡單的例子,體會一下aop的魔法吧。如果隻是引用了spring-context,那麼還需要引入spring-aspects:
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>3.2.18.RELEASE</version>
這裡我們采用xml配置的方式來開啟aop功能,在resources目錄下添加一個xml配置檔案,其中是用來開啟aop的:
<?xml version="1.0" encoding="UTF-8"?>
xmlns="http://www.springframework.org/schema/beans"
xmlns:aop = "http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<aop:aspectj-autoproxy/>
<bean id = "test" class = "spring.aop.TestAopBean"/>
<bean class = "spring.aop.AspectJTest"/>
添加Aspect:
@Aspect
public class AspectJTest {
@Pointcut("execution(* *.test(..))")
public void test(){
}
@Before("test()")
public void beforeTest(){
System.out.println("beforeTest");
}
@After("test()")
public void afterTest(){
System.out.println("afterTest");
}
@Around("test()")
public Object aroundTest(ProceedingJoinPoint p){
System.out.println("before1");
Object o = null;
try{
o = p.proceed();
}catch (Throwable e){
e.printStackTrace();
}
System.out.println("after1");
return o;
}
添加測試類:
public class TestAopBean {
private String testStr = "testStr";
public String getTestStr(){
return testStr;
}
public void setTestStr(String testStr){
this.testStr = testStr;
}
public void test(){
System.out.println("hello test");
}
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aspectJTest.xml");
TestAopBean test = (TestAopBean)ctx.getBean("test");
test.test();
}
可以看到輸出結果:
before1
beforeTest
hello test
after1
afterTest
這是一個aop簡單示例,我們寫了一個切面(Pointcut),用來指定Joinpoint的位置在執行test()方法時;同時分别定義了三個Advice(Before、After、Around)用來指定要織入的動作,最後将Pointcut和Advice封裝到一個Aspect中,這樣就完成了橫切邏輯的織入。
-
總結
在深入學習Spring AOP之前,我們先對AOP的概況進行了介紹,接着一起探索了Spring AOP的實作機制,包括最原始的代理模式,直至最終的動态代理與動态位元組碼生成技術。
AOP是能夠讓我們在不影響系統原有功能前提下,為軟體系統橫向擴充功能;
Spring AOP通過兩種方式實作:JDK動态代理、動态位元組碼增強;
在了解了這些内容之後,我們将繼續深入學習Spring AOP。
原文位址
https://www.cnblogs.com/volcano-liu/p/12941259.html