如果說 IOC 是 Spring 的核心,那么面向切面編程AOP就是 Spring 另外一個最為重要的核心@mikechen
AOP (Aspect Orient Programming),直譯過來就是 面向切面編程,AOP 是一種編程思想,是面向對象編程(OOP)的一種補充。
面向切面編程,實現在不修改源代碼的情況下給程序動態統一添加額外功能的一種技術,如下圖所示:
AOP可以攔截指定的方法并且對方法增強,而且無需侵入到業務代碼中,使業務與非業務處理邏輯分離,比如Spring的事務,通過事務的注解配置,Spring會自動在業務方法中開啟、提交業務,并且在業務處理失敗時,執行相應的回滾策略。
?
AOP 采取橫向抽取機制(動態代理),取代了傳統縱向繼承機制的重復性代碼,其應用主要體現在事務處理、日志管理、權限控制、異常處理等方面。
主要作用是分離功能性需求和非功能性需求,使開發人員可以集中處理某一個關注點或者橫切邏輯,減少對業務代碼的侵入,增強代碼的可讀性和可維護性。
簡單的說,AOP 的作用就是保證開發者在不修改源代碼的前提下,為系統中的業務組件添加某種通用功能。
?
比如典型的AOP的應用場景:
AOP可以攔截指定的方法,并且對方法增強,比如:事務、日志、權限、性能監測等增強,而且無需侵入到業務代碼中,使業務與非業務處理邏輯分離。
?
在深入學習SpringAOP 之前,讓我們先對AOP的幾個基本術語有個大致的概念。
?
AOP編程其實是很簡單的事情,縱觀AOP編程,程序員只需要參與三個部分:
1、定義普通業務組件
2、定義切入點,一個切入點可能橫切多個業務組件
3、定義增強處理,增強處理就是在AOP框架為普通業務組件織入的處理動作
所以進行AOP編程的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成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包下的一些接口
創建被代理對象
<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>
導入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>
//聲明當前類為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 采用了兩種混合的實現方式:JDK 動態代理和 CGLib 動態代理。
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原生的,不需要任何依賴即可使用;
通過反射機制生成代理類的速度要比CGLib操作字節碼生成代理類的速度更快;
缺點
如果要使用JDK動態代理,被代理的類必須實現了接口,否則無法代理;
JDK動態代理無法為沒有在接口中定義的方法實現代理,假設我們有一個實現了接口的類,我們為它的一個不屬于接口中的方法配置了切面,Spring仍然會使用JDK的動態代理,但是由于配置了切面的方法不屬于接口,為這個方法配置的切面將不會被織入。
JDK動態代理執行代理方法時,需要通過反射機制進行回調,此時方法執行的效率比較低;
?
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
這些框架了,這一層大家都比較熟悉所以,Cglib的實現是在字節碼的基礎上的,并且使用了開源的ASM讀取字節碼,對類實現增強功能的。
?
以上
陳睿|mikechen,10年+大廠架構經驗,《BAT架構技術500期》系列文章作者,分享十余年BAT架構經驗以及面試心得!
閱讀mikechen的互聯網架構更多技術文章合集
Java并發|JVM|MySQL|Spring|Redis|分布式|高并發|架構師
關注「mikechen 的互聯網架構」公眾號,回復【架構】領取我原創的《300 期 + BAT 架構技術系列與 1000 + 大廠面試題答案》
|