簡單直白的去理解AOP,了解Spring AOP,使用 @AspectJ - 讀書筆記


AOP = Aspect Oriental Programing  面向切面編程

文章里不講AOP術語,什么連接點、切點、切面什么的,這玩意太繞,記不住也罷。旨在以簡單、直白的方式理解AOP,理解Spring AOP, 應用 @AspectJ。

  1. 什么是AOP?
  2. Spring AOP 實現機制
  3. 使用Spring AOP,並通過xml配置(這個稍微看看就行了,你不一定用它)
  4. 使用@AspectJ (未完成)

 

1、什么是AOP?

方法1 方法2 方法3
A A A
代碼x 代碼y 代碼z
B B B

 

 

 

 

從縱向看,方法1、2、3 都執行了相同的A、B代碼,這樣重復代碼是很無聊的。

一個典型的場景就是:開啟事務,更新表里數據,提交事務;  開啟事務,刪除表里數據,提交事務。

所以我們從橫向來,把重復代碼抽取出來,變為

A A A
方法1(代碼x) 方法2(代碼y) 方法3(代碼z)
B B B

 

 

 

 

AOP希望將A、B 這些分散在各個業務邏輯中的相同代碼,通過橫向切割的方式抽取到一個獨立的模塊中,還業務邏輯類一個清新的世界。

當然,將這些重復性的橫切邏輯獨立出來很容易,但是如何將獨立的橫切邏輯 融合到 業務邏輯中 來完成和原來一樣的業務操作,這是事情的關鍵,也是AOP要解決的主要問題。

 

2.Spring AOP 實現機制

Spring AOP使用動態代理技術在運行期織入增強的代碼,使用了兩種代理機制,一種是基於JDK的動態代理,另一種是基於CGLib的動態代理

織入、增強 是AOP的兩個術語,織入增強的代碼簡單點就是在你的代碼上插入另一段代碼。

JDK動態代理主要涉及到java.lang.reflect包中的兩個類:Proxy 和 InvocationHandler(接口)。

直接上代碼

package test;

public interface CalcService {
    public void add(int x, int y);
}
CalcService 接口
package test;

public class CalcServiceImpl implements CalcService{
    public void add(int x, int y) {
        System.out.println("結果為" + (x + y));
    }
}
CalcServiceImpl 實現類
package test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class CalcHandler implements InvocationHandler {
    
    public Object target;
    
    
    public CalcHandler(Object target){
        this.target = target;
    }
    
    /** 
     * 實現接口的方法
     * @param proxy 最終生成的代理實例 
     * @param method 被代理目標(也就是target)的某個具體方法
     * @param args   某個具體方法的入參參數
     * @return Object 方法返回的值*/
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("*******調用方法前執行代碼******");
        Object obj = method.invoke(this.target, args);
        System.out.println("*******調用方法后執行代碼******");
        return  obj;
    }

}
CalcHandler 實現InvocationHandler
package test;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args){
        long start = System.nanoTime();
        
        CalcService target = new CalcServiceImpl();
        CalcHandler handler = new CalcHandler(target);
        CalcService calcProxy = (CalcService)Proxy.newProxyInstance(
                target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(), 
                handler);
        System.out.println("創建時間:" + (System.nanoTime()-start));
        
        start = System.nanoTime();
        calcProxy.add(2, 3);
        System.out.println("執行時間:" + (System.nanoTime()-start));

    }
}
Test 測試

執行結果為

*******調用方法前執行代碼******
結果為2
*******調用方法后執行代碼******

但是JDK動態代理有一個限制,即它只能為接口創建代理實例*******************************

看Proxy的方法 newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

interfaces 是需要代理實例實現的接口列表

那么對於一個沒有通過接口定義業務方法的類,怎么創建代理實例?

CGLib

CGLib采用非常底層的字節碼技術,可以在運行時為一個類創建子類,並在子類中采用方法攔截的技術攔截所有父類方法的調用。

package test;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CalcProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();
    
    public Object getProxy(Class clazz){
        enhancer.setSuperclass(clazz); // 設置需要被代理的類 target
        enhancer.setCallback(this);
        return enhancer.create(); // 通過字節碼技術動態創建子類
    }
    
    // 攔截父類所有方法的調用
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("*******調用方法前執行代碼******");
        Object result = proxy.invokeSuper(obj, args); // 通過代理類調用父類中的方法
        System.out.println("*******調用方法后執行代碼******");
        return result;
    }

}
CalcProxy 實現MethodInterceptor接口
package com.ycp.framework.test.proxyPattern.sample2;

public class Test2 {
    public static void main(String[] args) {
        CalcProxy proxy = new CalcProxy();
        CalcServiceImpl calcImpl = (CalcServiceImpl)proxy.getProxy(CalcServiceImpl.class);
        calcImpl.add(2, 3);
    }
}
Test2 測試類

 

之后對兩者做了一個效率對比

我在自己本機上通過System.nanoTime()對兩者做了記錄,結果如下

                        JDK動態代理                  CGLiib
創建代理對象時間         720 1394                    1 3473 7007       (時間單位為納秒)

代理對象執行方法時間      97 7322                     15 2080

一個創建花費時間長,一個執行時間長。

 

3.使用 Spring AOP,並通過XML配置

在Spring中,定義了 AopProxy接口,並提供了兩個final類型的實現類

Cglib2AopProxy          JdkDynamicAopProxy

以一個前置增強為例,也就是說在目標方法執行前執行的代碼

package test;

import java.lang.reflect.Method;


import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;

public class CalcBeforeAdvice implements MethodBeforeAdvice {

    public void before(Method method, Object[] args, Object obj) throws Throwable {
        System.out.println("*******調用目標方法前執行代碼******");

    }
    
    public static void main (String [] args){
        CalcService target = new CalcServiceImpl();
        
        CalcBeforeAdvice advice = new CalcBeforeAdvice();
        
        // 1 spring 提供的代理工廠
        ProxyFactory pf = new ProxyFactory();
        // 2 設置代理目標
        pf.setInterfaces(target.getClass().getInterfaces());// 指定對接口進行代理,將使用JdkDynamicAopProxy
        // 下面兩行操作,有任意一行,都將使用Cglib2AopProxy
        pf.setOptimize(true);// 啟用代理優化,將使用Cglib2AopProxy
        pf.setProxyTargetClass(true); // true表示對類進行代理,將使用Cglib2AopProxy
        
        pf.setTarget(target);
        // 3 為代理目標添加增強
        pf.addAdvice(advice);
        // 4 生成代理實例
        
        CalcService proxy  = (CalcService) pf.getProxy();
        System.out.println(proxy.getClass().getName());
        proxy.add(2, 3);
    }
}
通過Spring實現前置增強

以上通過ProxyFactory創建代理,下面我們通過Spring配置來聲明代理

<bean id="calcBeforAdvice" class="test.CalcBeforeAdvice"/>
<bean id="calcTarget" class="test.CalcServiceImpl"/>
<bean id="calcProxy" class="org.springframework.aop.framework.ProxyFactoryBean"
  p:proxyInterfaces="test.CalcService" //指定代理接口,如果有多個接口,可用,隔開
  p:interceptorNames="calcBeforAdvice"//指定使用的增強,引用第一行,如果有多個可用,隔開
  p:target-ref="calcTarget"//指定對那個Bean進行代理,引用第二行
  p:proxyTargetClass="true" //指定是否對類進行代理
  p:singleton="true"//指定返回的代理是否單實例,默認為true
/>

 除了前置增強BeforeAdvice,還有后置增強AfterReturningAdvice、環繞增強MethodInterceptor、異常拋出增強

ThrowsAdvice、及引介增強IntroductionInterceptor,均為接口。

其中引介增強稍微強調一下,它會在目標類中增加一些新的方法和屬性。

 

到了這里,可能對AOP稍有些了解了,那我們簡單說一下AOP的幾個名詞

連接點Joinpoint:類初始化前、初始化后, 方法調用前、調用后,方法拋出異常后,這些特定的點,叫連接點。

切點Pointcut:想想數據庫查詢,切點就是通過其所設定的條件找到對應的連接點。

增強Advice:就是把代碼加到某個連接點上。

引介Introduction:一種特殊的增強,它為類增加一些屬性和方法,假設某個業務類沒有實現A接口,我們給它添加方法,讓其成為A的實現類。

織入Weaving:就是怎么將增強添加到連接點上。

         三種織入方式:1、編譯期織入,要求使用特殊的JAVA編譯器

        2.類裝載期織入,要求使用特殊的類裝載器

        3.動態代理織入,在運行期為目標類添加增強

        Spring采用動態代理,而AspectJ采用編譯期織入和類裝載期織入。

目標對象Target:也就是你自己的業務類,AOP就是對這個類做增強、引介。

代理Proxy: 目標對象被織入增強后產生的結果類。

切面:由切點和增強(引介)組成,Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯(也就是代碼)織入到切面所指定的連接點(也就是代碼往哪加)中。

看完了名詞,再看完之前的代碼,我們發現增強被織入到了目標類的所有方法中(XX的,都木有選擇的余地....)

現在我們要對某些類的某些方法織入增強,那這時候就涉及到切點概念了

以如下為例:我只想針對所有的以add開頭的方法做處理

靜態普通方法名匹配

package com.ycp.framework.test.proxyPattern.sample2;

import java.lang.reflect.Method;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.IntroductionInterceptor;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;

public class AddAdvisor extends StaticMethodMatcherPointcutAdvisor {
    
    //StaticMethodMatcherPointcutAdvisor 抽象類
    
    // 實現父類 matches方法
    public boolean matches(Method method, Class clazz) {
        //只匹配add方法
        return  0 == "add".indexOf(method.getName());
    }
    
//    //切點類匹配規則為 CalcServiceImpl的類或子類,
//    @Override
//    public ClassFilter getClassFilter(){
//        return new ClassFilter(){
//            public boolean matches(Class clazz){
//                return CalcServiceImpl.class.isAssignableFrom(clazz);
//            }
//        };
//    }
    
    public static void main (String [] args){
        CalcService target = new CalcServiceImpl();
        CalcBeforeAdvice advice = new CalcBeforeAdvice();
        
        // 1 spring 提供的代理工廠
        ProxyFactory pf = new ProxyFactory();
        
        // 2 設置代理目標
        pf.setInterfaces(target.getClass().getInterfaces());// 指定對接口進行代理,將使用JdkDynamicAopProxy
        // 下面兩行操作,有任意一行,都將使用Cglib2AopProxy
        pf.setOptimize(true);// 啟用代理優化,將使用Cglib2AopProxy
        pf.setProxyTargetClass(true); // true表示對類進行代理,將使用Cglib2AopProxy
        
        pf.setTarget(target);
        
        // 3 為代理目標添加增強
        AddAdvisor advisor = new AddAdvisor();
        advisor.setAdvice(advice);
        
        pf.addAdvisor(advisor);
        
        // 4 生成代理實例
        CalcService proxy  = (CalcService) pf.getProxy();
        
        System.out.println(proxy.getClass().getName());
        proxy.add(2, 3);
    }

}
AddAdvisor 繼承StaticMethodMatcherPointcutAdvisor

通過Spring配置來定義切面

<bean id="calcBeforAdvice" class="test.CalcBeforeAdvice"/>
<bean id="calcTarget" class="test.CalcServiceImpl"/>
<bean id="addAdvisor" class="test.AddAdvisor" 
   p:advice-ref="calcBeforAdvice"//向切面注入一個前置增強
 />

<bean id="parent" class="org.springframework.aop.framework.ProxyFactoryBean"
  p:interceptorNames="addAdvisor"
  p:proxyTargetClass="true" 
/>

<bean id="calc" parent="parent" p:target-ref="calcTarget"/> //CalcServiceImpl的代理

 

上面的忒麻煩,我們通過靜態正則表達式來匹配

<bean id="calcBeforAdvice" class="test.CalcBeforeAdvice"/>
<bean id="calcTarget" class="test.CalcServiceImpl"/>
<bean id="addRegexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" 
   p:advice-ref="calcBeforAdvice"//向切面注入一個前置增強 >
    <property name="patterns">
     <list>
      <value> .add*</value>//匹配模式串
    </list>
  </property> 
</bean>
<bean id="calc" class="org.springframework.aop.framework.ProxyFactoryBean"
   p:interceptorNames="addRegexpAdvisor"
    p:target-ref="calcTarget"
  p:proxyTargetClass="true" 
/>

Spring提供了6種類型的切點,靜態方法切點、動態方法切點、注解切點、表達式切點、流程切點,我能力有限,沒有研究下去,僅以靜態切點 StaticMethodMatcherPointcut 做個例子就算完事,啥時項目用到了啥時再研究吧。

 

4.使用AspectJ

Spring AOP應用是比較麻煩的,要實現這個那個接口,寫這個那個XML描述,你頭疼不?

使用@AspectJ的注解可以非常容易的定義一個切面,不需要實現任何的接口


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM