作者 | 阿文
責編 | 屠敏
在 Spring 中 AOP 是一個非常非常重要的概念,那麼什麼是AOP呢?
AOP 即面向切面編程,也可以叫做面向方向編程,AOP不是一個新東西,它是OOP,即面向對象編程的一種補充,在當前已經成為一種成熟的編程方式。
為啥要使用 AOP
在學習AOP 之前,我們先了解下為啥我們要使用AOP?
那麼,在傳統的業務處理代碼中,比如你要操作資料庫,會進行事務的處理或者列印一些日誌。雖然通過OOP 也可以實現,比如通過繼承或組合的方式來達到代碼的復用,但是如果實現某些功能,比如日誌記錄,相同的代碼會分散到各個方法中,如果後面要想關閉某個功能或進行修改就必須要修改所有的方法,非常的不方便。
那麼為了解決為了解決這個問題,AOP的思想隨之產生。它採取了橫向抽取機制。將分散在各個方法中的重複代碼提取出來,然後在程序編譯或運行時,再將這些提取出來的代碼應用到需要執行的地方。這種採用橫向提取機制的方式。是採用傳統的AOP方式下無法辦到的。因為傳統的面向對象思想只能實現父子關係的縱向重用。
在AOP思想中,通過aspect(切面)可以分別在不同的類的方法中加入,例如事務日誌權限和異常處理等功能。
使用切面這種橫向方式。能夠使開發人員在編寫業務邏輯時專注於核心業務邏輯,而不用過度的關注與其他業務邏輯的實現。這樣可以提高開發效率,同時還增強了代碼的可維護性。
目前主流的AOP 框架有2個,分別是spring aop 和aspectJ,前者是純Java 實現的,不需要專門的編譯過程和類加載器,在運行期間可以通過代理的方式向目標內植入增強的代碼。而AspectJ是一個基於Java語言的AOP框架。在Spring 2.0 開始,引入了對AspectJ 的支持,並提供了一個專門的編譯器在編譯時提供橫向代碼的植入。
相關術語
在了解AOP之前,首先要了解一下它的專業術語。這些術語包括Aspect、Joinpiont、Pointcut、Advice、Target Object、Proxy 和Weaving,對於這些專業術語具體的解釋如下:
Aspect,切面在實際的應用中,切面通常是指封裝的用於橫向插入系統功能,比如事務日誌的類,該類被spring容器識別為切面,需要在配置文件中通過\<bean\>來指定。Joinpiont,連接點,在程序執行過程中的某個階段點。它實際上是對象的一個操作,例如方法的調用或異常的拋出。在spring AOP中連接點就是指方法的調用。Pointcut,切入點,切入點是指切面與程序流程的交叉點,即那些需要處理的連接點,通常在程序中切入點是指。類或者是方法名,比如說某個通知要應用到所有的以add開頭的方法中。那麼所有滿足這一規則的方法都是切入點。Adivce,通知增強處理,AOP 框架在特定的切入點執行增強處理,即在定義好的切入點處理所需要執行的程序代碼,你可以理解其為切面類中的方法,它是切面的具體實現。Target Object ,目標對象,是指所有通知的對象。也稱為北增強對象。如果AOP框架採用的是動態的AOP實現,那麼該對象就是一個被代理對象。Proxy ,代理,將通知應用到目標對象之後被動態創建的對象。Weaving, 織入,將切面代碼插入目標對象上,從而生成代理對象的過程。
AspectJ 開發
使用AspectJ 實現AOP 的方式有
XML 聲明註解XML 聲明
這種方式是通過XML文件來定義切面、切入點以及通知等,所有的切面、切入點和通知都必須定義在<aop:config>元素中,在<beans>元素中可以包含多個<aop:config>元素,一個 <aop:config> 中又可以包含子元素和屬性,其子元素包含<aop:pointcut、<aop:advisor、<aop:aspect>,在配置時,需要嚴格按照順序來定義,在<aop:aspect>下,同樣包含屬性和多個子元素,通過使用<aop:aspect>元素以及其子元素 可以在XML中配置切面、切入點和通知。如下所示
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="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-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"><!-- 1 目標類 --><beanid="userDao"class="com.ssm.aspectj.UserDaoImpl" /><!-- 2 切面 --><beanid="myAspect"class="com.ssm.aspectj.xml.MyAspect" /><!-- 3 aop編程 --><aop:config><!-- 1.配置切面 --><aop:aspectid="aspect"ref="myAspect"><!-- 2.配置切入點 --><aop:pointcutexpression="execution(* com.ssm.aspectj.*.*(..))"id="myPointCut" /><!-- 3.配置通知 --><!-- 前置通知 --><aop:beforemethod="myBefore"pointcut-ref="myPointCut" /><!--後置通知--><aop:after-returningmethod="myAfterReturning"pointcut-ref="myPointCut"returning="returnVal"/><!--環繞通知 --><aop:aroundmethod="myAround"pointcut-ref="myPointCut" /><!--異常通知 --><aop:after-throwingmethod="myAfterThrowing"pointcut-ref="myPointCut"throwing="e" /><!--最終通知 --><aop:aftermethod="myAfter"pointcut-ref="myPointCut" /></aop:aspect></aop:config></beans>
但是 XML 的配置過於複雜,因為日常開發過程中,我們更傾向於使用註解的方式來進行AOP的開發
為了在應用中使用@AspectJ支持,Spring需要添加三個庫:
aspectjweaver.jaraspectjrt.jaraopalliance.jar因此,我們需要配置maven,如下所示
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId><version>1.6.12</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.6.12</version></dependency>
新建一個app.xml ,我們需要在Spring配置文件中做如下配置:
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd"><!-- 指定需要掃描的包,使註解生效 --><context:component-scanbase-package="com.ssm.aspectj" /><!-- 啟動基於註解的聲明式AspectJ支持 --><aop:aspectj-autoproxy /></beans>
然後,我們創建一個UserDao 的接口,如下所示
package com.ssm.aspectj;publicinterfaceUserDao{// add userpublicvoidaddUser();//delete userpublicvoiddelUser();}
將 UserDao 實例化,並加上註解@Repository("userDao"),方便後續進行調用具體的方法
package com.ssm.aspectj;import org.springframework.stereotype.Repository;@Repository("userDao")publicclassUserDaoImplimplementsUserDao{@OverridepublicvoidaddUser(){ System.out.println("add user"); }@OverridepublicvoiddelUser(){ System.out.println("delete user"); }}
定義一個切面類,在該類中編寫各種通知
package com.ssm.aspectj.xml;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;/** * 切面類,在此類中編寫通知 */@Aspect@ComponentpublicclassMyAspect{//定義切入點表達式@Pointcut("execution(* com.ssm.aspectj.*.*(..))")//使用一個返回值為void、方法體為空的方法來命名切入點private void myPointCut(){}//前置通知@Before("myPointCut()")public void myBefore(JoinPoint joinPoint){ System.out.print("前置通知:模擬執行權限檢查..,"); System.out.print("目標類是:"+joinPoint.getTarget()); System.out.println(",被植入增強處理的目標方法為:"+joinPoint.getSignature().getName()); }//後置通知@AfterReturning(value="myPointCut()")public void myAfterReturning(JoinPoint joinPoint) { System.out.print("後置通知:模擬記錄日誌..,"); System.out.println("被植入增強處理的目標方法為:" + joinPoint.getSignature().getName()); }/** * 環繞通知 * ProceedingJoinPoint是JoinPoint的子接口,表示可執行目標方法 * 1.必須是Object類型的返回值 * 2.必須接收一個參數,類型為ProceedingJoinPoint * 3.必須throws Throwable */@Around("myPointCut()")public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{//開始 System.out.println("環繞開始:執行目標方法之前,模擬開啟事務..,");//執行當前目標方法 Object obj=proceedingJoinPoint.proceed();//結束 System.out.println("環繞結束:執行目標方法之後,模擬關閉事務..,");return obj; }//異常通知@AfterThrowing(value="myPointCut()",throwing="e")public void myAfterThrowing(JoinPoint joinPoint,Throwable e){ System.out.println("異常通知:出錯了"+e.getMessage()); }//最終通知@After("myPointCut()")public void myAfter(){ System.out.println("最終通知:模擬方法結束後釋放資源.."); }}
srping 的通知包括五種通知工作:
在上述代碼中,其中 @Pointcut("execution(* com.ssm.aspectj.*.*(..))") 表示定義切入點,使用註解@Pointcut 後面的execution(* com.ssm.aspectj.*.*(..)) 表示匹配所有目標類的所有方法。第一個*代表返回類型,第二個*代表方法名,而..代表任意入參的方法,他的格式如下:
語法:execution(修飾符 返回值 包.類.方法(參數) throws 異常)
最後編寫個測試類從ClassPathXmlApplicationContext 讀取xml 文件,然後調用getBean 獲取userDao並執行addUser方法。
package com.ssm.aspectj;import org.junit.jupiter.api.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;publicclassTestXmlAspectJ{@TestpublicvoidtestAnnotation(){ ApplicationContext applicationContext=new ClassPathXmlApplicationContext("app.xml");//從容器中獲得內容 UserDao userDao= (UserDao) applicationContext.getBean("userDao");//執行方法 userDao.addUser(); }}
結果如下: