網(wǎng)上很多人在介紹AOP時都這樣說:面向切面編程,通過預編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)棚壁。個人認為這句話是錯誤齿尽。AOP和OOP一樣,是一種程序設(shè)計思想亮元,而非技術(shù)手段猛计。
程序設(shè)計有六大原則,其中第一原則就是單一職責原則爆捞。意思就是一個類只負責一件事情奉瘤。這與OOP的封裝特性相得益彰。在這個條件下煮甥,我們的程序會被分散到不同的類盗温、不同的方法中去。這樣做的好處是降低了類的復雜性成肘,提高了程序的可維護性卖局。但是同時,它也使代碼變得啰嗦了双霍。例如砚偶,我們要為方法添加調(diào)用日志,那就必須為所有類的所有方法添加日志調(diào)用洒闸,盡管它們都是相同的染坯。為了解決上述問題,AOP應運而生了顷蟀。
AOP旨在將橫切關(guān)注點與業(yè)務主體進行分類酒请,從而提高程序代碼的模塊化程度。橫切關(guān)注點是一個抽象的概念鸣个,它是指那些在項目中貫穿多個模塊的業(yè)務羞反。上個例子中日志功能就是一個典型的橫切關(guān)注點。
AOP的幾種實現(xiàn)方式
動態(tài)代理
動態(tài)代理是一種設(shè)計模式囤萤。它有以下特征:
我們不需要自己寫代理類昼窗。
運行期通過接口直接生成代理對象。
運行期間才確定代理哪個對象涛舍。
以下面這個例子為例澄惊,我們看一下動態(tài)代理的類圖結(jié)構(gòu)。
通常我們的APP都有一部分功能要求用戶登錄之后才能訪問。如修改密碼掸驱、修改用戶名等功能肛搬。當用戶打算使用這些功能時,我們一般要對用戶的登錄狀態(tài)進行判斷毕贼,只有用戶登錄了温赔,才能正常使用這些功能。而如果用戶未登錄鬼癣,我們的APP要跳轉(zhuǎn)到登錄頁陶贼。就以修改密碼為例我們看一下動態(tài)代理的類圖。
InvocationHandler是Java JDK提供的動態(tài)代理的入口待秃,用來對被代理對象的方法做處理拜秧。
代碼如下:
public static class LoginCheckHandler implements InvocationHandler { private static T proxy(Ssource, Class tClass) {return(T) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{tClass}, new LoginCheckHandler(source)); }? private Object mSource; LoginCheckHandler(Objectsource) { this.mSource =source; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if(!checkLogin()){ jumpToLoginActivity();returnnull; }returnmethod.invoke(mSource, args); } private booleancheckLogin(){ System.out.println("用戶未登錄");returnfalse; } private voidjumpToLoginActivity(){ System.out.println("跳轉(zhuǎn)到登錄頁"); } } public class Client {? public static void main(String[] args) { IUserSettingsource= new UserSetting(); IUserSetting iUserSetting = LoginCheckHandler.proxy(source,IUserSetting.class); iUserSetting.changePwd("new Password"); } }
經(jīng)過這樣封裝之后,檢查登錄跳轉(zhuǎn)登錄頁的邏輯作為橫切關(guān)注點就和業(yè)務主體進行了分離章郁。當有新的需求需要登錄檢查時枉氮,我們只需要通過LoginCheckHandler生成新的代理對象即可。
APT
APT(Annotation Processing Tool)是一種編譯期注解處理技術(shù)暖庄。它通過定義注解和處理器來實現(xiàn)編譯期生成代碼的功能嘲恍,并且將生成的代碼和源代碼一起編譯成.class文件。通過APT技術(shù)雄驹,我們將橫切關(guān)注點封裝到注解處理器中佃牛,從而實現(xiàn)橫切關(guān)注點與業(yè)務主體的分離。更詳細的介紹請移步
Android編譯期插樁医舆,讓程序自己寫代碼(一)
俘侠。
AspectJ
AspectJ就是一種編譯器,它在Java編譯器的基礎(chǔ)上增加了關(guān)鍵字識別和編譯方法蔬将。因此爷速,AspectJ可以編譯Java代碼。它還提供了Aspect程序霞怀。在編譯期間惫东,將開發(fā)者編寫的Aspect程序織入到目標程序中,擴展目標程序的功能毙石。開發(fā)者通過編寫AspectJ程序?qū)崿F(xiàn)AOP功能廉沮。更詳細的介紹請移步
Android編譯期插樁,讓程序自己寫代碼(二)
徐矩。
Transform + Javassist/ASM
Transform是Android Gradle提供的滞时,可以操作字節(jié)碼的一種方式。App編譯時滤灯,源代碼首先會被編譯成class坪稽,然后再被編譯成dex曼玩。在class編譯成dex的過程中,會經(jīng)過一系列 Transform 處理窒百。Javassist/ASM是一個能夠非常方便操作字節(jié)碼的庫黍判。我們通過它們可以修改編譯的.class文件。
橫切關(guān)注點
影響應用多處的功能(日志篙梢、事務样悟、安全)
增強(Advice)
增強定義了切面要完成的功能以及什么時候執(zhí)行這個功能。
Spring 切面可以應用 5 種類型的增強:
前置增強(Before) 在目標方法被調(diào)用前調(diào)用增強功能
后置增強(After) 在目標方法完成之后調(diào)用增強庭猩,不關(guān)注方法輸出是什么。
返回增強(After-returning) 在目標方法成功執(zhí)行之后調(diào)用增強
異常增強(After-throwing) 在目標方法拋出異常后調(diào)用增強
環(huán)繞增強(Around) 在被增強的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義行為陈症,即包括前置增強和后置增強蔼水。
連接點(Join Point)
應用中每一個有可能會被增強的點被稱為連接點。
切點(Pointcut)
切點是規(guī)則匹配出來的連接點录肯。
切面(Aspect)
切面是增強和切點的結(jié)合趴腋,定義了在何時和何處完成其功能。
引入(Introduction)
引入允許我們向現(xiàn)有的類中添加新方法和屬性论咏∮啪妫可以在不修改現(xiàn)有的類的情況下,讓類具有新的行為和狀態(tài)厅贪。
織入(Weaving)
織入是把切面應用到目標對象中并創(chuàng)建新的代理對象的過程蠢护。在目標對象的生命周期里有多個點可以進行織入:
編譯器:切面在目標類編譯時織入。這種方式需要特殊的編譯器养涮。AspectJ 的織入編譯器就是以這種方式織入切面的葵硕。
類加載器:切面在目標類加載到 JVM 時被織入。這種方式需要特殊的類加載器(ClassLoader)贯吓,它可以在目標類被引入應用之前增強該目標類的字節(jié)碼懈凹。AspectJ5 的加載時織入(LTW)支持以這種方式織入。
運行期:切面在應用運行時的某個時刻被織入悄谐。一般情況下介评,在織入切面時,AOP 容器會為目標對象動態(tài)地創(chuàng)建一個代理對象爬舰。Spring AOP 就是以這種方式織入切面的们陆。
Spring 對 AOP 的支持
Spring 對 AOP 的支持在很多方面借鑒了 AspectJ 項目。目前 Spring 提供了 4 種類型的 AOP 支持:
基于代理的經(jīng)典 AOP
純 POJO 切面
@AspectJ 注解驅(qū)動的切面
注入式 AspectJ 切面
Spring AOP 構(gòu)建在動態(tài)代理基礎(chǔ)之上情屹,因此 Spring 對 AOP 的支持局限于方法攔截棒掠。
運行時增強
通過在代理中包裹切面,Spring 在運行期把切面織入到 Spring 管理的 bean 中屁商。代理類封裝了目標類烟很,并攔截被增強方法的調(diào)用颈墅,再把調(diào)用轉(zhuǎn)發(fā)給真正的目標 bean。在代理攔截到方法調(diào)用時雾袱,在調(diào)用目標 bean 方法之前恤筛,會執(zhí)行切面邏輯。
直到應用需要代理的 bean 時芹橡,Spring 才創(chuàng)建代理對象毒坛。如果使用 ApplicationContext 的話,在 ApplicationContext 從 BeanFactory 中加載所有 bean 的時候林说,Spring 才會創(chuàng)建被代理的對象煎殷。
方法級別的連接點
Spring 基于動態(tài)代理實現(xiàn) AOP,所以 Spring 只支持方法連接點腿箩。其他的 AOP 框架比如 AspectJ 與 JBoss豪直,都提供了字段和構(gòu)造器接入點,允許創(chuàng)建細粒度的增強珠移。
切點表達式
Spring AOP 中弓乙,使用 AspectJ 的切點表達式來定義切點。Spring 只支持 AspectJ 切點指示器(pointcut designator)的一個子集钧惧。
指示器
AspectJ 指示器描述arg( )限制連接點匹配參數(shù)為指定類型的執(zhí)行方法execution( )用于匹配連接點this指定匹配 AOP 代理的 bean 引用的類型target指定匹配對象為特定的類within( )指定連接點匹配的類型@annotation匹配帶有指定注解的連接點
編寫切點
package concert;public interface Performance { public void perform();}復制代碼復制代碼
Performance 類可以代表任何類型的現(xiàn)場表演暇韧,比如電影、舞臺劇等∨ǖ桑現(xiàn)在編寫一個切點表達式來限定 perform() 方法執(zhí)行時觸發(fā)的增強懈玻。
execution(* concert.Performance.perform(..))復制代碼復制代碼
每個部分的意義如下圖所示:
也可以引入其他注解對匹配規(guī)則做進一步限制。比如
execution(* concert.Performance.perform(..)) && within(concert.*)復制代碼復制代碼
within() 指示器限制了切點僅匹配 concert 包乾颁。
Spring 還有一個 bean() 指示器酪刀,允許我們在切點表達式中使用 bean 的 ID 表示 bean。
execution(* concert.Performance.perform(..)) && bean('woodstock')復制代碼復制代碼
以上的切點就表示限定切點的 bean 的 ID 為 woodstock 钮孵。
給自己的Java技術(shù)交流群打波廣告吧骂倘,想要學習Java架構(gòu)技術(shù)的朋友可以加我的群:710373545,群內(nèi)每晚都會有阿里技術(shù)大牛講解的最新Java架構(gòu)技術(shù)巴席。并會錄制錄播視頻分享在群公告中历涝,作為給廣大朋友的加群的福利——分布式(Dubbo、Redis漾唉、RabbitMQ荧库、Netty、RPC赵刑、Zookeeper分衫、高并發(fā)、高可用架構(gòu))/微服務(Spring Boot般此、Spring Cloud)/源碼(Spring蚪战、Mybatis)/性能優(yōu)化(JVM牵现、TomCat、MySQL)
使用注解創(chuàng)建切面
定義切面
在一場演出之前邀桑,我們需要讓觀眾將手機靜音且就座瞎疼,觀眾在表演之后鼓掌,在表演失敗之后可以退票壁畸。在觀眾類中定義這些功能。
@Aspectpublic class Audience {? @Pointcut("execution(* concert.Performance.perform(..)))") public voidperformance(){} @Before("performance()") public voidsilenceCellPhones() { System.out.println("Silencing cell phones"); } @Before("performance()") public voidtakeSeats() { System.out.println("Taking seats"); } @AfterReturning("performance()") public voidapplause() { System.out.println("CLAP CLAP CLAP!!!"); } @AfterThrowing("performance()") public voiddemandRefund() { System.out.println("Demanding a refund"); }}復制代碼復制代碼
@AspectJ 注解表名了該類是一個切面捏萍。 @Pointcut 定義了一個類中可重用的切點太抓,寫切點表達式時走敌,如果切點相同给赞,可以重用該切點片迅。 其余方法上的注解定義了增強被調(diào)用的時間,根據(jù)注解名可以知道具體調(diào)用時間驱闷。
到目前為止空另, Audience 仍然只是 Spring 容器中的一個 bean摄杂。即使使用了 AspectJ 注解,但是這些注解仍然不會解析映挂,因為目前還缺乏代理的相關(guān)配置咪辱。
如果使用 JavaConfig,在配置類的類級別上使用 @EnableAspectJAutoProxy 注解啟用自動代理功能专筷。
@Configuration@EnableAspectJAutoProxy@ComponentScanpublic class ConcertConfig { @Bean public Audienceaudience() {returnnew Audience(); } }復制代碼復制代碼
如果使用 xml 味咳,那么需要引入 <aop:aspectj-autoproxy> 元素。
<?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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="concert"/> <aop:aspectj-autoproxy/> <bean class="concert.Audience"/></beans>
環(huán)繞增強
環(huán)繞增強就像在一個增強方法中同時編寫了前置增強和后置增強。
@Aspectpublic class Audience { @Pointcut("execution(* concert.Performance.perform(..)))") public voidperformance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } }}
可以看到,這個增強達到的效果與分開寫前置增強與后置增強是一樣的辱姨,但是現(xiàn)在所有的功能都位于同一個方法內(nèi)。 注意該方法接收 ProceedingJoinPoint 作為參數(shù),這個對象必須要有,因為需要通過它來調(diào)用被增強的方法醇份。 注意,在這個方法中,我們可以控制不調(diào)用 proceed() 方法允趟,從而阻塞對增強方法的訪問恼策。同樣,我們也可以在增強方法失敗后潮剪,多次調(diào)用 proceed() 進行重試涣楷。
增強方法參數(shù)
修改 Perform#perform() 方法,添加參數(shù)
package concert;public interface Performance { public void perform(int audienceNumbers);}復制代碼復制代碼
我們可以通過切點表達式來獲取被增強方法中的參數(shù)抗碰。
@Pointcut("execution(* concert.Performance.perform(int)) && args(audienceNumbers)))") public void performance(int audienceNumbers){}復制代碼復制代碼
注意狮斗,此時方法接收的參數(shù)為 int 型, args(audienceNumbers) 指定參數(shù)名為 audienceNumbers 改含,與切點方法簽名中的參數(shù)匹配情龄,該參數(shù)不一定與增強方法的參數(shù)名一致迄汛。
引入增強
切面不僅僅能夠增強現(xiàn)有方法捍壤,也能為對象新增新的方法。 我們可以在代理中暴露新的接口鞍爱,當引入接口的方法被調(diào)用時鹃觉,代理會把此調(diào)用委托給實現(xiàn)了新接口的某個其他對象。實際上睹逃,就是一個 bean 的實現(xiàn)被拆分到多個類中了盗扇。 定義 Encoreable 接口,將其引入到 Performance 的實現(xiàn)類中沉填。
public interface Encoreable { void performEncore();}
創(chuàng)建一個新的切面
@Aspectpublic class EncoreableIntroducer { @DeclareParents(value ="concert.Performance+",defaultImpl = DefaultEncoreable.class) public static Encoreable encoreable;}
我們使用了 @Aspect 將 EncoreableIntroducer 標記為一個切面疗隶,但是它沒有提供前置、后置或環(huán)繞增強翼闹。通過 @DeclareParents 注解將 Encoreable 接口引入到了 Performance bean 中斑鼻。
@DeclareParents 注解由三部分組成:
value 屬性指定了哪種類型的 bean 要引入該接口。在上述代碼中猎荠,類名后面的 + 號表示是 Performance 的所有子類型坚弱,而不是它本身蜀备。
defaultImpl 屬性指定了為引入功能提供實現(xiàn)的類。
@DeclareParents 注解所標注的靜態(tài)屬性指明了要引入的接口荒叶。
同樣地碾阁,我們在 Spring 應用中將該類聲明為一個 bean:
<bean class="concert.EncoreableIntroducer" />
Spring 的自動代理機制將會獲取到它的聲明,并創(chuàng)建相應的代理些楣。然后將調(diào)用委托給被代理的 bean 或者被引入的實現(xiàn)脂凶,具體取決于調(diào)用的方法屬于被代理的 bean 還是屬于被引入的接口。
在 XML 中聲明切面
更新一下 Audience 類戈毒,將它的 AspectJ 注解全部移除艰猬。
public class Audience {? public voidsilenceCellPhones() { System.out.println("Silencing cell phones"); } public voidtakeSeats() { System.out.println("Taking seats"); } public voidapplause() { System.out.println("CLAP CLAP CLAP!!!"); } public voiddemandRefund() { System.out.println("Demanding a refund"); }}
聲明前置與后置增強
<?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"> <aop:config> <aop:aspect ref="audience"> <aop:before pointcut="execution(* concert.Performance.perform(..))" method="silenceCellPhone"/> <aop:before pointcut="execution(* concert.Performance.perform(..))" method="takeSeats"/> <aop:after-returning pointcut="execution(* concert.Performance.perform(..))" method="applause"/> <aop:after-throwing pointcut="execution(* concert.Performance.perform(..))" method="demandRefund"/> </aop:aspect> </aop:config></beans>
如上所示,就將一個普通方法變?yōu)榱嗽鰪姟?大多數(shù)的 AOP 配置元素都必須在 <aop:config>元素的上下文內(nèi)使用埋市。元素名基本上都與注解名相對應冠桃。 這里,我們同樣將同一個切點表達式寫了四遍道宅,將它提取出來食听。
<?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"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:before pointcut-ref="performance" method="silenceCellPhone"/> <aop:before pointcut-ref="performance" method="takeSeats"/> <aop:after-returning pointcut-ref="performance" method="applause"/> <aop:after-throwing pointcut-ref="performance" method="demandRefund"/> </aop:aspect> </aop:config></beans>
注意,此時 <aop:pointcut> 標簽位于 <aop:aspect> 下層污茵,故只能在該切面中引用樱报。如果想要一個切點能夠被多個切面引用,可以將 <aop:aspect> 元素放在 <aop:config> 下第一層泞当。
環(huán)繞增強
定義環(huán)繞增強方法
public class Audience { public void performance(int audienceNumbers){} public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } }}
在 xml 中使用 <aop:around> 指定方法名與切點即可迹蛤。
<?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"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:around pointcut-ref="performance" method="watchPerformance"/> </aop:aspect> </aop:config></beans>
為增強傳遞參數(shù)
獲取參數(shù)主要就在于切點表達式。
<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(int)) and args(audienceNumbers)"/>
這樣能在 xml 中定位到一個參數(shù)類型為 int 襟士,參數(shù)名為 audienceNumbers 的切點盗飒。 注意在 xml 中使用了 and 代替 && (在 XML 中, & 符號會被解析為實體的開始)陋桂。
引入增強
<aop:declare-parents types-matching="concert.Performance+" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable"/>
types-matching 指定了要匹配的類型逆趣,與注解中的 value 值功能相同。
注入 AspectJ 切面
AspectJ 切面提供了 Spring AOP 所不能支持的許多類型的切點嗜历。 切面很有可能依賴其他類來完成它們的工作宣渗。我們可以借助 Spring 的依賴注入把 bean 裝配進 AspectJ 切面中。
創(chuàng)建一個新切面梨州。
public aspect CriticAspect { private CriticismEngine criticismEngine; publicCriticAspect() { } pointcut performance():execution(* perform(..)); afterReturning() :performance() { System.out.println(criticismEngine.getCriticism()); } public voidsetCriticismEngine(CriticismEngine criticismEngine) { this.criticismEngine = criticismEngine; }}
注入的 CritismEngine 的實現(xiàn)類
public class CriticismEngineImple implements CriticismEngine { publicCriticismEngineImple() { } public StringgetCriticism() { int i = (int) (Math.random() * criticismPool.length);returncriticismPool[i]; }? private String[] criticismPool; public voidsetCriticismPool(String[] criticismPool) { this.criticismPool = criticismPool; }}
CriticAspect 主要作用是在表演結(jié)束后為表演發(fā)表評論痕囱。 實際上, CriticAspect 是調(diào)用了 CriticismEngine 的方法來發(fā)表評論暴匠。通過 setter 依賴注入為 CriticAspect 設(shè)置 CriticismEngine 鞍恢。
在配置文件中將 CriticismEngine bean 注入到 CriticAspect 中。
<bean class="om.springinaction.springidol.CriticAspect" factory-method="aspectOf"> <property name="criticismEngine" ref="criticismEngine"/> </bean>
一般情況下,Spring bean 由 Spring 容器初始化有序,但是 AspectJ 切面是由 AspectJ 在運行期創(chuàng)建的抹腿。所以在運行期間,AspectJ 創(chuàng)建好了 CriticAspect 實例旭寿,每個 AspectJ 都會提供一個靜態(tài)的 aspectOf() 方法警绩,返回切面的的單例。 使用 factory-method 調(diào)用 aspectOf() 方法向 CriticAspect 中注入 CriticismEngine 盅称。