Spring AOP全面詳解(超級詳細)
- 2022 年 8 月 18 日
- 筆記
- 大廠架構面試乾貨合集
如果說 IOC 是 Spring 的核心,那麼面向切面編程AOP就是 Spring 另外一個最為重要的核心@mikechen
AOP的定義
AOP (Aspect Orient Programming),直譯過來就是 面向切面編程,AOP 是一種編程思想,是面向對象編程(OOP)的一種補充。
面向切面編程,實現在不修改源代碼的情況下給程序動態統一添加額外功能的一種技術,如下圖所示:
AOP可以攔截指定的方法並且對方法增強,而且無需侵入到業務代碼中,使業務與非業務處理邏輯分離,比如Spring的事務,通過事務的註解配置,Spring會自動在業務方法中開啟、提交業務,並且在業務處理失敗時,執行相應的回滾策略。
AOP的作用
AOP 採取橫向抽取機制(動態代理),取代了傳統縱向繼承機制的重複性代碼,其應用主要體現在事務處理、日誌管理、權限控制、異常處理等方面。
主要作用是分離功能性需求和非功能性需求,使開發人員可以集中處理某一個關注點或者橫切邏輯,減少對業務代碼的侵入,增強代碼的可讀性和可維護性。
簡單的說,AOP 的作用就是保證開發者在不修改源代碼的前提下,為系統中的業務組件添加某種通用功能。
AOP的應用場景
比如典型的AOP的應用場景:
- 日誌記錄
- 事務管理
- 權限驗證
- 性能監測
AOP可以攔截指定的方法,並且對方法增強,比如:事務、日誌、權限、性能監測等增強,而且無需侵入到業務代碼中,使業務與非業務處理邏輯分離。
Spring AOP的術語
在深入學習SpringAOP 之前,讓我們先對AOP的幾個基本術語有個大致的概念。
AOP核心概念
Spring AOP 通知分類
Spring AOP 織入時期
Spring AOP三種使用方式
AOP編程其實是很簡單的事情,縱觀AOP編程,程序員只需要參與三個部分:
1、定義普通業務組件
2、定義切入點,一個切入點可能橫切多個業務組件
3、定義增強處理,增強處理就是在AOP框架為普通業務組件織入的處理動作
所以進行AOP編程的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理對象的方法=增強處理+被代理對象的方法。
方式1:使用Spring自帶的AOP
public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice,MethodInterceptor { @Override public void before(Method method, Object[] objects, Object target) throws Throwable { //前置通知 } @Override public void afterReturning(Object result, Method method, Object[] objects, Object target) throws Throwable { //後置通知 } @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { //環繞通知 //目標方法之前執行 methodInvocation.proceed(); //目標方法 //目標方法之後執行 return resultVal; } }
配置通知時需實現org.springframework.aop包下的一些接口
- 前置通知:MethodBeforeAdvice
- 後置通知:AfterReturningAdvice
- 環繞通知:MethodInterceptor
- 異常通知:ThrowsAdvice
創建被代理對象
<bean id="orderServiceBean" class="com.apesource.service.impl.OrderServiceImpl"/> <bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>
通知(Advice)
<bean id="logAdviceBean" class="com.apesource.log.LogAdvice"/> <bean id="performanceAdviceBean" class="com.apesource.log.PerformanceAdvice"/>
切入點(Pointcut):通過正則表達式描述指定切入點(某些 指定方法)
<bean id="createMethodPointcutBean" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<!--注入正則表達式:描述那些方法為切入點-->
<property name="pattern" value=".*creat.*"/>
</bean>
Advisor(高級通知) = Advice(通知) + Pointcut(切入點)
<bean id="performanceAdvisorBean" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<!--注入切入點-->
<property name="pointcut" ref="createMethodPointcutBean"/>
<!--注入通知-->
<property name="advice" ref="performanceAdviceBean"/>
</bean>
創建自動代理
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<!--Bean名稱規則(數組):指定那些bean創建自動代理-->
<property name="beanNames">
<list>
<value>*ServiceBean</value>
<value>*TaskBean</value>
</list>
</property>
<!--通知列表:需要執行那些通知-->
<property name="interceptorNames">
<list>
<value>logAdviceBean</value>
<value>performanceAdvisorBean</value>
</list>
</property>
</bean>
方式2:使用Aspectj實現切面(普通POJO的實現方式)
導入Aspectj相關依賴
<!--aop依賴1:aspectjrt --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.5</version> </dependency> <!--aop依賴2: aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency>
通知方法名隨便起,沒有限制
public class LogAspectj { //前置通知 public void beforeAdvice(JoinPoint joinPoint){ System.out.println("========== 【Aspectj前置通知】 =========="); } //後置通知:方法正常執行後,有返回值,執行該後置通知:如果該方法執行出現異常,則不執行該後置通知 public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){ System.out.println("========== 【Aspectj後置通知】 =========="); } public void afterAdvice(JoinPoint joinPoint){ System.out.println("========== 【Aspectj後置通知】 =========="); } //環繞通知 public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("##########【環繞通知中的前置通知】##########"); Object returnVale = joinPoint.proceed(); System.out.println("##########【環繞通知中的後置通知】##########"); return returnVale; } /** * 異常通知:方法出現異常時,執行該通知 */ public void throwAdvice(JoinPoint joinPoint, Exception ex){ System.out.println("出現異常:" + ex.getMessage()); } }
使用Aspectj實現切面,使用Spring AOP進行配置
<!--業務組件bean--> <bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/> <!--日誌Aspect切面--> <bean id="logAspectjBean" class="com.apesource.log.LogAspectj"/> <!--使用Aspectj實現切面,使用Spring AOP進行配置--> <aop:config> <!--配置切面--> <!--注入切面bean--> <aop:aspect ref="logAspectjBean"> <!--定義Pointcut:通過expression表達式,來查找 特定的方法(pointcut)--> <aop:pointcut id="pointcut" expression="execution(* com.apesource.service.impl.*.create*(..))"/> <!--配置"前置通知"--> <!--在pointcut切入點(serviceMethodPointcut)查找到 的方法執行"前", 來執行當前logAspectBean的doBefore--> <aop:before method="beforeAdvice" pointcut-ref="pointcut"/> <!--配置「後置通知」--> <!--returning屬性:配置當前方法中用來接收返回值的參數名--> <aop:after-returning returning="returnVal" method="afterReturningAdvice" pointcut-ref="pointcut"/> <aop:after method="afterAdvice" pointcut-ref="pointcut"/> <!--配置"環繞通知"--> <aop:around method="aroundAdvice" pointcut-ref="pointcut"/> <!--配置「異常通知」--> <!--throwing屬性:配置當前方法中用來接收當前異常的參數名--> <aop:after-throwing throwing="ex" method="throwAdvice" pointcut-ref="pointcut"/> </aop:aspect> </aop:config>
方式3:使用Aspectj實現切面(基於註解的實現方式)
//聲明當前類為Aspect切面,並交給Spring容器管理 @Component @Aspect public class LogAnnotationAspectj { private final static String EXPRESSION = "execution(* com.apesource.service.impl.*.create*(..))"; //前置通知 @Before(EXPRESSION) public void beforeAdvice(JoinPoint joinPoint){ System.out.println("========== 【Aspectj前置通知】 =========="); } //後置通知:方法正常執行後,有返回值,執行該後置通知:如果該方法執行出現異常,則不執行該後置通知 @AfterReturning(value = EXPRESSION,returning = "returnVal") public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){ System.out.println("========== 【Aspectj後置通知】 =========="); } //後置通知 @After(EXPRESSION) public void afterAdvice(JoinPoint joinPoint){ System.out.println("========== 【Aspectj後置通知】 =========="); } //環繞通知 @Around(EXPRESSION) public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("##########【環繞通知中的前置通知】##########"); Object returnVale = joinPoint.proceed(); System.out.println("##########【環繞通知中的後置通知】##########"); return returnVale; } // 異常通知:方法出現異常時,執行該通知 @AfterThrowing(value = EXPRESSION,throwing = "ex") public void throwAdvice(JoinPoint joinPoint, Exception ex){ System.out.println("********** 【Aspectj異常通知】執行開始 **********"); System.out.println("出現異常:" + ex.getMessage()); System.out.println("********** 【Aspectj異常通知】執行結束 **********"); } } <!-- 自動掃描器 --> <context:component-scan base-package="com.apesource"/> <!--配置Aspectj的自動代理--> <aop:aspectj-autoproxy/>
Spring AOP的實現原理
Spring的AOP實現原理其實很簡單,就是通過動態代理實現的。
Spring AOP 採用了兩種混合的實現方式:JDK 動態代理和 CGLib 動態代理。
- JDK動態代理:Spring AOP的首選方法。 每當目標對象實現一個接口時,就會使用JDK動態代理。目標對象必須實現接口
- CGLIB代理:如果目標對象沒有實現接口,則可以使用CGLIB代理。
JDK動態代理
Spring默認使用JDK的動態代理實現AOP,類如果實現了接口,Spring就會使用這種方式實現動態代理。
JDK實現動態代理需要兩個組件,首先第一個就是InvocationHandler接口。
我們在使用JDK的動態代理時,需要編寫一個類,去實現這個接口,然後重寫invoke方法,這個方法其實就是我們提供的代理方法。
如下源碼所示:
/** * 動態代理 * * @author mikechen */ public class JdkProxySubject implements InvocationHandler { private Subject subject; public JdkProxySubject(Subject subject) { this.subject = subject; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before 前置通知"); Object result = null; try { result = method.invoke(subject, args); }catch (Exception ex) { System.out.println("ex: " + ex.getMessage()); throw ex; }finally { System.out.println("after 後置通知"); } return result; } }
然後JDK動態代理需要使用的第二個組件就是Proxy這個類,我們可以通過這個類的newProxyInstance方法,返回一個代理對象。
生成的代理類實現了原來那個類的所有接口,並對接口的方法進行了代理,我們通過代理對象調用這些方法時,底層將通過反射,調用我們實現的invoke方法。
public class Main { public static void main(String[] args) { //獲取InvocationHandler對象 在構造方法中注入目標對象 InvocationHandler handler = new JdkProxySubject(new RealSubject()); //獲取代理類對象 Subject proxySubject = (Subject)Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Subject.class}, handler); //調用目標方法 proxySubject.request(); proxySubject.response(); }
運行結果:
before 前置通知
執行目標對象的request方法......
after 後置通知
before 前置通知
執行目標對象的response方法......
after 後置通知
JDK動態代理優缺
優點
JDK動態代理是JDK原生的,不需要任何依賴即可使用;
通過反射機制生成代理類的速度要比CGLib操作位元組碼生成代理類的速度更快;
缺點
如果要使用JDK動態代理,被代理的類必須實現了接口,否則無法代理;
JDK動態代理無法為沒有在接口中定義的方法實現代理,假設我們有一個實現了接口的類,我們為它的一個不屬於接口中的方法配置了切面,Spring仍然會使用JDK的動態代理,但是由於配置了切面的方法不屬於接口,為這個方法配置的切面將不會被織入。
JDK動態代理執行代理方法時,需要通過反射機制進行回調,此時方法執行的效率比較低;
CGLib代理
CGLIB組成結構
Cglib是一個強大的、高性能的代碼生成包,它廣泛被許多AOP框架使用,為他們提供方法的攔截,如下圖所示Cglib與Spring等應用的關係:
- 最底層的是位元組碼
Bytecode
,位元組碼是Java為了保證「一次編譯、到處運行」而產生的一種虛擬指令格式,例如iload_0、iconst_1、if_icmpne、dup等 - 位於位元組碼之上的是
ASM
,這是一種直接操作位元組碼的框架,應用ASM需要對Java位元組碼、Class結構比較熟悉 - 位於
ASM
之上的是CGLIB
、Groovy
、BeanShell
,後兩種並不是Java體系中的內容而是腳本語言,它們通過ASM框架生成位元組碼變相執行Java代碼,這說明在JVM中執行程序並不一定非要寫Java代碼—-只要你能生成Java位元組碼,JVM並不關心位元組碼的來源,當然通過Java代碼生成的JVM位元組碼是通過編譯器直接生成的,算是最「正統」的JVM位元組碼 - 位於
CGLIB
、Groovy
、BeanShell
之上的就是Hibernate
、Spring AOP
這些框架了,這一層大家都比較熟悉 - 最上層的是Applications,即具體應用,一般都是一個Web項目或者本地跑一個程序
所以,Cglib的實現是在位元組碼的基礎上的,並且使用了開源的ASM讀取位元組碼,對類實現增強功能的。
以上
作者簡介
陳睿|mikechen,10年+大廠架構經驗,《BAT架構技術500期》系列文章作者,分享十餘年BAT架構經驗以及面試心得!
閱讀mikechen的互聯網架構更多技術文章合集
Java並發|JVM|MySQL|Spring|Redis|分佈式|高並發|架構師