天天看點

Spring 中 AOP 的實作原理——動态代理

Spring AOP 的實作原理

​Spring​

​​的​

​AOP​

​實作原理其實很簡單,就是通過動态代理實作的。如果我們為​

​Spring​

​​的某個​

​bean​

​​配置了切面,那麼​

​Spring​

​​在建立這個​

​bean​

​​的時候,實際上建立的是這個​

​bean​

​​的一個代理對象,我們後續對​

​bean​

​​中方法的調用,實際上調用的是代理類重寫的代理方法。而​

​Spring​

​​的​

​AOP​

​使用了兩種動态代理,分别是 JDK 的動态代理,以及 CGLib 的動态代理。

(一)JDK 動态代理

Spring 預設使用 JDK 的動态代理實作 AOP,類如果實作了接口,Spring 就會使用這種方式實作動态代理。熟悉​

​Java​

​​語言的應該會對​

​JDK​

​​動态代理有所了解。​

​JDK​

​​實作動态代理需要兩個元件,首先第一個就是​

​InvocationHandler​

​​接口。我們在使用​

​JDK​

​​的動态代理時,需要編寫一個類,去實作這個接口,然後重寫​

​invoke​

​​方法,這個方法其實就是我們提供的代理方法。然後​

​JDK​

​​動态代理需要使用的第二個元件就是​

​Proxy​

​​這個類,我們可以通過這個類的​

​newProxyInstance​

​​方法,傳回一個代理對象。生成的代理類實作了原來那個類的所有接口,并對接口的方法進行了代理,我們通過代理對象調用這些方法時,底層将通過反射,調用我們實作的​

​invoke​

​方法。

(二)CGLib 動态代理

​JDK​

​​的動态代理存在限制,那就是被代理的類必須是一個實作了接口的類,代理類需要實作相同的接口,代理接口中聲明的方法。若需要代理的類沒有實作接口,此時​

​JDK​

​​的動态代理将沒有辦法使用,于是​

​Spring​

​​會使用​

​CGLib​

​​的動态代理來生成代理對象。​

​CGLib​

​直接操作位元組碼,生成類的子類,重寫類的方法完成代理。

​以上就是​

​​Spring​

​實作動态的兩種方式,下面我們具體來談一談這兩種生成動态代理的方式。​

JDK 的動态代理

  1. 實作原理

JDK的動态代理是基于反射實作。​

​JDK​

​​通過反射,生成一個代理類,這個代理類實作了原來那個類的全部接口,并對接口中定義的所有方法進行了代理。當我們通過代理對象執行原來那個類的方法時,代理類底層會通過反射機制,回調我們實作的​

​InvocationHandler​

​​接口的​

​invoke​

​方法。并且這個代理類是 Proxy 類的子類(記住這個結論,後面測試要用)。這就是​

​JDK​

​動态代理大緻的實作方式。

  1. 優點
  • ​JDK​

    ​​動态代理是​

    ​JDK​

    ​原生的,不需要任何依賴即可使用;
  • . 通過反射機制生成代理類的速度要比​

    ​CGLib​

    ​操作位元組碼生成代理類的速度更快;
  1. 缺點
  • 如果要使用​

    ​JDK​

    ​動态代理,被代理的類必須實作了接口,否則無法代理;
  • ​JDK​

    ​​動态代理無法為沒有在接口中定義的方法實作代理,假設我們有一個實作了接口的類,我們為它的一個不屬于接口中的方法配置了切面,​

    ​Spring​

    ​​仍然會使用​

    ​JDK​

    ​的動态代理,但是由于配置了切面的方法不屬于接口,為這個方法配置的切面将不會被織入。
  • ​JDK​

    ​動态代理執行代理方法時,需要通過反射機制進行回調,此時方法執行的效率比較低;

CGLib 動态代理

(一)實作原理

​CGLib​

​​實作動态代理的原理是,底層采用了​

​ASM​

​​位元組碼生成架構,直接對需要代理的類的位元組碼進行操作,生成這個類的一個子類,并重寫了類的所有可以重寫的方法,在重寫的過程中,将我們定義的額外的邏輯(簡單了解為​

​Spring​

​中的切面)織入到方法中,對方法進行了增強。而通過位元組碼操作生成的代理類,和我們自己編寫并編譯後的類沒有太大差別。

(二)優點

  1. 使用​

    ​CGLib​

    ​​代理的類,不需要實作接口,因為​

    ​CGLib​

    ​生成的代理類是直接繼承自需要被代理的類;
  2. ​CGLib​

    ​生成的代理類是原來那個類的子類,這就意味着這個代理類可以為原來那個類中,所有能夠被子類重寫的方法進行代理;
  3. ​CGLib​

    ​​生成的代理類,和我們自己編寫并編譯的類沒有太大差別,對方法的調用和直接調用普通類的方式一緻,是以​

    ​CGLib​

    ​​執行代理方法的效率要高于​

    ​JDK​

    ​的動态代理;

(三)缺點

  1. 由于​

    ​CGLib​

    ​​的代理類使用的是繼承,這也就意味着如果需要被代理的類是一個​

    ​final​

    ​​類,則無法使用​

    ​CGLib​

    ​代理;
  2. 由于​

    ​CGLib​

    ​​實作代理方法的方式是重寫父類的方法,是以無法對​

    ​final​

    ​​方法,或者​

    ​private​

    ​方法進行代理,因為子類無法重寫這些方法;
  3. ​CGLib​

    ​​生成代理類的方式是通過操作位元組碼,這種方式生成代理類的速度要比​

    ​JDK​

    ​通過反射生成代理類的速度更慢;

通過代碼進行測試

(一)測試 JDK 動态代理

下面我們通過一個簡單的例子,來驗證上面的說法。首先我們需要一個接口和它的一個實作類,然後再為這個實作類的方法配置切面,看看​

​Spring​

​​是否真的使用的是​

​JDK​

​​的動态代理。假設接口的名稱為​

​Human​

​​,而實作類為​

​Student​

​:

public interface Human {
    void display();
}      
@Component
public class Student implements Human {

    @Override
    public void display() {
        System.out.println("I am a student");
    }
}      

然後我們定義一個切面,将這個​

​display​

​方法作為切入點,為它配置一個前置通知,代碼如下:

@Aspect
@Component
public class HumanAspect {
    // 為Student這個類的所有方法,配置這個前置通知
    @Before("execution(* cn.tewuyiang.pojo.Student.*(..))")
    public void before() {
        System.out.println("before student");
    }
}      

下面可以開始測試了,我們通過​

​Java​

​類的方式進行配置,然後編寫一個單元測試方法:

// 配置類
@Configuration
@ComponentScan(basePackages = "cn.tewuyiang")
@EnableAspectJAutoProxy
public class AOPConfig {
}      

// 測試方法

@Test
public void testProxy() {
    ApplicationContext context =
        new AnnotationConfigApplicationContext(AOPConfig.class);
    // 注意,這裡隻能通過Human.class擷取,而無法通過Student.class,因為在Spirng容器中,
    // 因為使用JDK動态代理,Ioc容器中,存儲的是一個類型為Human的代理對象
    Human human =  context.getBean(Human.class);
    human.display();
    // 輸出代理類的父類,以此判斷是JDK還是CGLib
    System.out.println(human.getClass().getSuperclass());
}      

注意看上面代碼中,最長的那一句注釋。由于我們需要代理的類實作了接口,則​

​Spring​

​​會使用​

​JDK​

​​的動态代理,生成的代理類會實作相同的接口,然後建立一個代理對象存儲在​

​Spring​

​​容器中。這也就是說,在​

​Spring​

​​容器中,這個代理​

​bean​

​​的類型不是​

​Student​

​​類型,而是​

​Human​

​​類型,是以我們不能通過​

​Student.class​

​​擷取,隻能通過​

​Human.class​

​​(或者通過它的名稱擷取)。這也證明了我們上面說過的另一個問題,​

​JDK​

​​動态代理無法代理沒有定義在接口中的方法。假設​

​Student​

​​這個類有另外一個方法,它不是​

​Human​

​​接口定義的方法,此時就算我們為它配置了切面,也無法将切面織入。而且由于在​

​Spring​

​​容器中儲存的代理對象并不是​

​Student​

​​類型,而是​

​Human​

​​類型,這就導緻我們連那個不屬于​

​Human​

​​的方法都無法調用。這也說明了​

​JDK​

​動态代理的局限性。

我們前面說過,​

​JDK​

​​動态代理生成的代理類繼承了​

​Proxy​

​​這個類,而​

​CGLib​

​​生成的代理類,則繼承了需要進行代理的那個類,于是我們可以通過輸出代理對象所屬類的父類,來判斷​

​Spring​

​使用了何種代理。下面是輸出結果:

before student
I am a student
class java.lang.reflect.Proxy // 注意看,父類是Proxy      

通過上面的輸出結果,我們發現,代理類的父類是​

​Proxy​

​​,也就意味着果然使用的是​

​JDK​

​的動态代理。

(二)測試 CGLib 動态代理

好,測試完​

​JDK​

​​動态代理,我們開始測試​

​CGLib​

​​動态代理。我們前面說過,隻有當需要代理的類沒有實作接口時,​

​Spring​

​​才會使用​

​CGLib​

​​動态代理,于是我們修改​

​Student​

​這個類的定義,不讓他實作接口:

@Component
public class Student {
    public void display() {
        System.out.println("I am a student");
    }
}      

由于​

​Student​

​​沒有實作接口,是以我們的測試方法也需要做一些修改。之前我們是通過​

​Human.class​

​​這個類型從​

​Spring​

​​容器中擷取代理對象,但是現在,由于沒有實作接口,是以我們不能再這麼寫了,而是要寫成​

​Student.class​

​,如下:

@Test
public void testProxy() {
    ApplicationContext context =
        new AnnotationConfigApplicationContext(AOPConfig.class);
    // 修改為Student.class
    Student student = context.getBean(Student.class);
    student.display();
    // 同樣輸出父類
    System.out.println(student.getClass().getSuperclass());
}      

因為​

​CGLib​

​​動态代理是生成了​

​Student​

​​的一個子類,是以這個代理對象也是​

​Student​

​​類型(子類也是父類類型),是以可以通過​

​Student.class​

​擷取。下面是輸出結果:

before student
I am a student
class cn.tewuyiang.pojo.Student   // 此時,父類是Student      

可以看到,​

​AOP​

​​成功生效,并且代理對象所屬類的父類是​

​Student​

​​,驗證了我們之前的說法。下面我們修改一下​

​Student​

​​類的定義,将​

​display​

​​方法加上​

​final​

​修飾符,再看看效果:

@Component
public class Student {
    // 加上final修飾符
    public final void display() {
        System.out.println("I am a student");
    }
}      
// 輸出結果如下:
I am a student
class cn.tewuyiang.pojo.Student</code-pre> </pre></code-box>      

可以看到,輸出的父類仍然是​

​Student​

​​,也就是說​

​Spring​

​​依然使用了​

​CGLib​

​​生成代理。但是我們發現,我們為​

​display​

​​方法配置的前置通知并沒有執行,也就是代理類并沒有為​

​display​

​​方法進行代理。這也驗證了我們之前的說法,​

​CGLib​

​​無法代理​

​final​

​​方法,因為子類無法重寫父類的​

​final​

​​方法。下面我們可以試着為​

​Student​

​​類加上​

​final​

​修飾符,讓他無法被繼承,此時看看結果。運作的結果會抛出異常,因為無法生成代理類,這裡就不貼出來了,可以自己去試試。

強制 Spring 使用 CGLib

通過上面的測試我們會發現,​

​CGLib​

​​的動态代理好像更加強大,而​

​JDK​

​​的動态代理卻限制頗多。而且前面也提過,​

​CGLib​

​​的代理對象,執行代理方法的速度更快,隻是生成代理類的效率較低。但是我們使用到的​

​bean​

​​大部分都是單例的,并不需要頻繁建立代理類,也就是說​

​CGLib​

​​應該會更合适。但是為什麼​

​Spring​

​​預設使用​

​JDK​

​​呢?這我也不太清楚,網上也沒有找到相關的描述(如果有人知道,麻煩告訴我)。但是據說​

​SpringBoot​

​​現在已經預設使用​

​CGLib​

​​作為​

​AOP​

​的實作了。

那我們可以強制​

​Spring​

​​使用​

​CGLib​

​​,而不使用​

​JDK​

​​的動态代理嗎?答案當然是可以的。我們知道,如果要使用注解(​

​@Aspect​

​​)方式配置切面,則需要在​

​xml​

​​檔案中配置下面一行開啟​

​AOP​

​:

<aop:aspectj-autoproxy />      

如果我們希望隻使用​

​CGLib​

​​實作​

​AOP​

​,則可以在上面的這一行加點東西:

<!-- 将proxy-target-class配置設定為true -->
<aop:aspectj-autoproxy proxy-target-class="true"/>      

當然,如果我們是使用​

​Java​

​​類進行配置,比如說我們上面用到的​

​AOPConfig​

​​這個類,如果是通過這種方式配置,則強制使用​

​CGLib​

​的方式如下:

@Configuration
@ComponentScan(basePackages = "cn.tewuyiang")
// 如下:@EnableAspectJAutoProxy開啟AOP,
// 而proxyTargetClass = true就是強制使用CGLib
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AOPConfig {

}      

如果我們是在 xml 檔案中配置切面,則可以通過以下方式來強制使用​

​CGLib​

​:

<!-- aop:config用來在xml中配置切面,指定proxy-target-class="true" -->
<aop:config proxy-target-class="true">
  <!-- 在其中配置AOP -->
</aop:config>      

總結

四、參考

  • ​​Spring-4.3.21 官方文檔——AOP​​