SpringAOP(面向切面編程)和動態代理的總結

1、AOP概念:

主要功能:日誌記錄

2、AOP特點:

採用橫向抽取機制,取代了傳統縱向繼承體系重複性代碼。

3、AOP底層實現:

AOP底層使用動態代理實現。包括兩種方式:

  • 使用JDK動態代理實現。
  • 使用cglib來實現

 jdk和cglib的區別:

只能對實現了接口的類生成代理,而不是針對類,該目標類型實現的接口都將被代理。

JDK動態代理實現:

原理是通過在運行期間創建一個接口的實現類來完成對目標對象的代理。步驟如下:

  1. 定義一個實現接口InvocationHandler的類
  2. 通過構造函數,注入被代理類
  3. 實現invoke( Object proxy, Method method, Object[] args)方法
  4. 在主函數中獲得被代理類的類加載器
  5. 使用Proxy.newProxyInstance( )產生一個代理對象
  6. 通過代理對象調用各種方法

動態代理的實現:

我們先定義了接口Hello,但是我們並不去編寫實現類,而是直接通過JDK提供的一個Proxy.newProxyInstance()創建了一個Hello接口對象。這種沒有實現類但是在運行期動態創建了一個接口對象的方式,我們稱爲動態代碼。JDK提供的動態創建接口對象的方式,就叫動態代理。

     每一個動態代理類的調用處理程序都必須實現InvocationHandler接口,並且每個代理類的實例都關聯到了實現該接口的動態代理類調用處理程序中,當我們通過動態代理對象調用一個方法時候,這個方法的調用就會被轉發到實現InvocationHandler接口類的invoke方法來調用,看如下invoke方法:
 

/**
    * proxy:代理類代理的真實代理對象com.sun.proxy.$Proxy0
    * method:我們所要調用某個對象真實的方法的Method對象
    * args:指代代理對象方法傳遞的參數
    */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

2.Proxy類就是用來創建一個代理對象的類,它提供了很多方法,但是我們最常用的是newProxyInstance方法。

 public static Object newProxyInstance(ClassLoader loader, 
                                            Class<?>[] interfaces, 
                                            InvocationHandler h)
Returns an instance of a proxy class for the specified interfaces
     that dispatches method invocations to the specified invocation
     handler.  This method is equivalent to:

這個方法的作用就是創建一個代理類對象,它接收三個參數,我們來看下幾個參數的含義:

  • loader:一個classloader對象,定義了由哪個classloader對象對生成的代理類進行加載
  • interfaces:一個interface對象數組,表示我們將要給我們的代理對象提供一組什麼樣的接口,如果我們提供了這樣一個接口對象數組,那麼也就是聲明瞭代理類實現了這些接口,代理類就可以調用接口中聲明的所有方法。
  • h:一個InvocationHandler對象,表示的是當動態代理對象調用方法的時候會關聯到哪一個InvocationHandler對象上,並最終由其調用。

3動態代理中核心的兩個接口和類上面已經介紹完了,接下來我們就用實例來講解下具體的用法

  • 首先我們定義一個接口People
package reflect;

public interface People {

    public String work();
}
  • 定義一個Teacher類,實現People接口,這個類是真實的對象

 

package reflect;

public class Teacher implements People{

    @Override
    public String work() {
        System.out.println("老師教書育人...");
        return "教書";
    }

}
  • 現在我們要定義一個代理類的調用處理程序,每個代理類的調用處理程序都必須實現InvocationHandler接口,代理類如下:
package reflect;

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

public class WorkHandler implements InvocationHandler{

    //代理類中的真實對象  
    private Object obj;

    public WorkHandler() {
        // TODO Auto-generated constructor stub
    }
    //構造函數,給我們的真實對象賦值
    public WorkHandler(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在真實的對象執行之前我們可以添加自己的操作
        System.out.println("before invoke。。。");
        Object invoke = method.invoke(obj, args);
        //在真實的對象執行之後我們可以添加自己的操作
        System.out.println("after invoke。。。");
        return invoke;
    }

}
 上面的代理類的調用處理程序的invoke方法中的第一個參數proxy好像我們從來沒有用過,而且關於這個參數的具體用法含義請參考我的另外一篇文章Java中InvocationHandler接口中第一個參數proxy詳解
  • 接下來我們看下客戶端類
package reflect;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test {

    public static void main(String[] args) {
        //要代理的真實對象
        People people = new Teacher();
        //代理對象的調用處理程序,我們將要代理的真實對象傳入代理對象的調用處理的構造函數中,最終代理對象的調用處理程序會調用真實對象的方法
        InvocationHandler handler = new WorkHandler(people);
        /**
         * 通過Proxy類的newProxyInstance方法創建代理對象,我們來看下方法中的參數
         * 第一個參數:people.getClass().getClassLoader(),使用handler對象的classloader對象來加載我們的代理對象
         * 第二個參數:people.getClass().getInterfaces(),這裏爲代理類提供的接口是真實對象實現的接口,這樣代理對象就能像真實對象一樣調用接口中的所有方法
         * 第三個參數:handler,我們將代理對象關聯到上面的InvocationHandler對象上
         */
        People proxy = (People)Proxy.newProxyInstance(handler.getClass().getClassLoader(), people.getClass().getInterfaces(), handler);
        //System.out.println(proxy.toString());
        System.out.println(proxy.work());
    }
}

看下輸出結果:

before invoke。。。
老師教書育人...
after invoke。。。
教書

 

 一個最簡單的動態代理實現如下

 

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

interface Hello {
    void morning(String name);
}

public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(), // 傳入ClassLoader
            new Class[] { Hello.class }, // 傳入要實現的接口
            handler); // 傳入處理調用方法的InvocationHandler
        hello.morning("Bob");
    }
}

在運行期動態創建一個interface實例的方法如下:

  1. 定義一個InvocationHandler實例,它負責實現接口的方法調用;
  2. 通過Proxy.newProxyInstance()創建interface實例,它需要3個參數:
    1. 使用的ClassLoader,通常就是接口類的ClassLoader
    2. 需要實現的接口數組,至少需要傳入一個接口進去;
    3. 用來處理接口方法調用的InvocationHandler實例。
  3. 將返回的Object強制轉型爲接口。

動態代理實際上是JDK在運行期動態創建class字節碼並加載的過程,它並沒有什麼黑魔法,把上面的動態代理改寫爲靜態實現類大概長這樣:

public class HelloDynamicProxy implements Hello {
    InvocationHandler handler;
    public HelloDynamicProxy(InvocationHandler handler) {
        this.handler = handler;
    }
    public void morning(String name) {
        handler.invoke(
           this,
           Hello.class.getMethod("morning"),
           new Object[] { name });
    }
}

 其實就是JDK幫我們自動編寫了一個上述類(不需要源碼,可以直接生成字節碼),並不存在可以直接實例化接口的黑魔法。

小結

Java標準庫提供了動態代理功能,允許在運行期動態創建一個接口的實例;

動態代理是通過Proxy創建代理對象,然後將接口方法“代理”給InvocationHandler完成的。

cglib動態代理實現:

針對類實現代理,對是否實現接口無要求。原理是對指定的類生成一個子類,覆蓋其中的方法,因爲是繼承,所以被代理的類或方法最好不要聲明爲final類型。

  • 定義一個實現了MethodInterceptor接口的類
  • 實現其intercept()方法,在其中調用proxy.invokeSuper( )
  • 代理方式的選擇:

如果目標對象實現了接口,默認情況下回採用JDK的動態代理實現AOP,也可以強制使用cglib實現AOP

如果目標對象沒有實現接口,必須採用cglib庫,Spring會自動在JDK動態代理和cglib之間轉換

靜態代理和動態代理的區別:

  • 靜態代理:自己編寫創建代理類,然後再進行編譯,在程序運行前,代理類的.class文件就已經存在了。
  • 動態代理:在實現階段不用關心代理誰,而在運行階段(通過反射機制)才指定代理哪一個對象。

想要強制使用CGLIB,那麼就設置<aop:config>下面的proxy-target-class屬性爲true

<aop:config proxy-target-class="true">
        <!-- other beans defined here... -->
</aop:config>

要是使用@AspectJ強制使用CGLIB的話,可以配置<aop:aspectj-autoproxy>下的proxy-target-class屬性爲true

<aop:aspectj-autoproxy proxy-target-class="true"/>

 

原理區別:

java動態代理是利用反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。而cglib動態代理是利用asm開源包,對代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。

  1. 如果目標對象實現了接口,默認情況下會採用JDK的動態代理實現AOP
  2. 如果目標對象實現了接口,可以強制使用CGLIB實現AOP
  3. 如果目標對象沒有實現了接口,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換
#開啓對AOP的支持
spring.aop.auto=true
#設置代理模式 true(cglib) false(java JDK代理)
spring.aop.proxy-target-class=true

Spring Boot事務默認是使用的cglib代理,可以通過上面的方式去指定代理模式。

Spring 5 AOP 默認改用 CGLIB 了?從現象到源碼的深度分析

Spring5 AOP 默認使用 Cglib 了?我第一次聽到這個說法是在一個微信羣裏:

羣聊天

真的假的?查閱文檔

剛看到這個說法的時候,我是保持懷疑態度的。

大家都知道 Spring5 之前的版本 AOP 在默認情況下是使用 JDK 動態代理的,那是不是 Spring5 版本真的做了修改呢?於是我打開 Spring Framework 5.x 文檔,再次確認了一下:

文檔地址:https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/core.html#aop

 

Spring Framework 5.x 文檔

簡單翻譯一下。Spring AOP 默認使用 JDK 動態代理,如果對象沒有實現接口,則使用 CGLIB 代理。當然,也可以強制使用 CGLIB 代理。

什麼?文檔寫錯了?!

當我把官方文檔發到羣裏之後,又收到了這位同學的回覆:

文檔寫錯了?!

SpringBoot 2.x 代碼示例

爲了證明文檔寫錯了,這位同學還寫了一個 DEMO。下面,就由我來重現一下這個 DEMO 程序:

運行環境:SpringBoot 2.2.0.RELEASE 版本,內置 Spring Framework 版本爲 5.2.0.RELEASE 版本。同時添加 spring-boot-starter-aop 依賴,自動裝配 Spring AOP。

 

public interface UserService {
    void work();
}

@Service
public class UserServiceImpl implements UserService {

    @Override
    public void work() {
        System.out.println("開始幹活...coding...");
    }
}
@Component
@Aspect
public class UserServiceAspect {
    @Before("execution(* com.me.aop.UserService.work(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("UserServiceAspect.....()");
    }
}

默認使用Cglib代理了?

UserServiceImpl實現了UserService接口,同時使用UserServiceAspectUserService#work方法進行前置增強攔截。

從運行結果來看,這裏的確使用了 CGLIB 代理而不是 JDK 動態代理。

難道真的是文檔寫錯了?!

@EnableAspectJAutoProxy 源碼註釋

在 Spring Framework 中,是使用@EnableAspectJAutoProxy註解來開啓 Spring AOP 相關功能的。

Spring Framework 5.2.0.RELEASE 版本@EnableAspectJAutoProxy註解源碼如下:

 

@EnableAspectJAutoProxy源碼

通過源碼註釋我們可以瞭解到:在 Spring Framework 5.2.0.RELEASE 版本中,proxyTargetClass的默認取值依舊是false,默認還是使用 JDK 動態代理。

難道文檔和源碼註釋都寫錯了?!

@EnableAspectJAutoProxy 的 proxyTargetClass 無效了?

接下來,我嘗試使用@EnableAspectJAutoProxy來強制使用 JDK 動態代理。

運行環境:SpringBoot 2.2.0.RELEASE 版本,內置 Spring Framework 版本爲 5.2.0.RELEASE 版本。

proxyTargetClass設置無效了?

通過運行發現,還是使用了 CGLIB 代理。難道@EnableAspectJAutoProxyproxyTargetClass設置無效了?

Spring Framework 5.x

整理一下思路

  1. 有人說 Spring5 開始 AOP 默認使用 CGLIB 了
  2. Spring Framework 5.x 文檔和 @EnableAspectJAutoProxy源碼註釋都說了默認是使用 JDK 動態代理
  3. 程序運行結果說明,即使繼承了接口,設置proxyTargetClassfalse,程序依舊使用 CGLIB 代理

等一下,我們是不是遺漏了什麼?

示例程序是使用 SpringBoot 來運行的,那如果不用 SpringBoot,只用 Spring Framework 會怎麼樣呢?

運行環境:Spring Framework 5.2.0.RELEASE 版本。UserServiceImpl 和 UserServiceAspect 類和上文一樣,這裏不在贅述。

 

Spring Framework 5.x

 

Spring Framework 5.x使用CGLIB

運行結果表明:在 Spring Framework 5.x 版本中,如果類實現了接口,AOP 默認還是使用 JDK 動態代理。

再整理思路

  1. Spring5 AOP 默認依舊使用 JDK 動態代理,官方文檔和源碼註釋沒有錯。
  2. SpringBoot 2.x 版本中,AOP 默認使用 cglib,且無法通過proxyTargetClass進行修改。
  3. 那是不是 SpringBoot 2.x 版本做了一些改動呢?

再探 SpringBoot 2.x

結果上面的分析,很有可能是 SpringBoot2.x 版本中,修改了 Spring AOP 的相關配置。那就來一波源碼分析,看一下內部到底做了什麼。

源碼分析

源碼分析,找對入口很重要。那這次的入口在哪裏呢?

@SpringBootApplication是一個組合註解,該註解中使用@EnableAutoConfiguration實現了大量的自動裝配。

EnableAutoConfiguration也是一個組合註解,在該註解上被標誌了@Import。關於@Import註解的詳細用法,可以參看筆者之前的文章:https://mp.weixin.qq.com/s/7arh4sVH1mlHE0GVVbZ84Q

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

AutoConfigurationImportSelector實現了DeferredImportSelector接口。

在 Spring Framework 4.x 版本中,這是一個空接口,它僅僅是繼承了ImportSelector接口而已。而在 5.x 版本中拓展了DeferredImportSelector接口,增加了一個getImportGroup方法:

 

AutoConfigurationImportSelector#getImportGroup

在這個方法中返回了AutoConfigurationGroup類。這是AutoConfigurationImportSelector中的一個內部類,他實現了DeferredImportSelector.Group接口。

在 SpringBoot 2.x 版本中,就是通過AutoConfigurationImportSelector.AutoConfigurationGroup#process方法來導入自動配置類的。

導入配置類

通過斷點調試可以看到,和 AOP 相關的自動配置是通過org.springframework.boot.autoconfigure.aop.AopAutoConfiguration來進行配置的。

 

AopAutoConfiguration源碼

真相大白

看到這裏,可以說是真相大白了。在 SpringBoot2.x 版本中,通過AopAutoConfiguration來自動裝配 AOP。

默認情況下,是肯定沒有spring.aop.proxy-target-class這個配置項的。而此時,在 SpringBoot 2.x 版本中會默認使用 Cglib 來實現。

SpringBoot 2.x 中如何修改 AOP 實現

通過源碼我們也就可以知道,在 SpringBoot 2.x 中如果需要修改 AOP 的實現,需要通過spring.aop.proxy-target-class這個配置項來修改。

#在application.properties文件中通過spring.aop.proxy-target-class來配置
spring.aop.proxy-target-class=false

spring-configuration-metadata.json

這裏也提一下spring-configuration-metadata.json文件的作用:在使用application.propertiesapplication.yml文件時,IDEA 就是通過讀取這些文件信息來提供代碼提示的,SpringBoot 框架自己是不會來讀取這個配置文件的。

SringBoot 1.5.x 又是怎麼樣的

SringBoot 1.5.x

可以看到,在 SpringBoot 1.5.x 版本中,默認還是使用 JDK 動態代理的。

SpringBoot 2.x 爲何默認使用 Cglib

SpringBoot 2.x 版本爲什麼要默認使用 Cglib 來實現 AOP 呢?這麼做的好處又是什麼呢?筆者從網上找到了一些資料,先來看一個 issue。

Spring Boot issue #5423

Use @EnableTransactionManagement(proxyTargetClass = true) #5423 https://github.com/spring-projects/spring-boot/issues/5423

在這個 issue 中,拋出了這樣一個問題:

image.png

翻譯一下:我們應該使用@EnableTransactionManagement(proxyTargetClass = true)來防止人們不使用接口時出現討厭的代理問題。

這個"不使用接口時出現討厭的代理問題"是什麼呢?思考一分鐘。

討厭的代理問題

假設,我們有一個UserServiceImplUserService類,此時需要在UserContoller中使用UserService。在 Spring 中通常都習慣這樣寫代碼:

@Autowired
UserService userService;

 

在這種情況下,無論是使用 JDK 動態代理,還是 CGLIB 都不會出現問題。

但是,如果你的代碼是這樣的呢:

@Autowired
UserServiceImpl userService;

 

這個時候,如果我們是使用 JDK 動態代理,那在啓動時就會報錯:

啓動報錯

因爲 JDK 動態代理是基於接口的,代理生成的對象只能賦值給接口變量。

而 CGLIB 就不存在這個問題。因爲 CGLIB 是通過生成子類來實現的,代理對象無論是賦值給接口還是實現類這兩者都是代理對象的父類。

SpringBoot 正是出於這種考慮,於是在 2.x 版本中,將 AOP 默認實現改爲了 CGLIB。

更多的細節信息,讀者可以自己查閱上述 issue。

總結

  1. Spring 5.x 中 AOP 默認依舊使用 JDK 動態代理。
  2. SpringBoot 2.x 開始,爲了解決使用 JDK 動態代理可能導致的類型轉化異常而默認使用 CGLIB。
  3. 在 SpringBoot 2.x 中,如果需要默認使用 JDK 動態代理可以通過配置項spring.aop.proxy-target-class=false來進行修改,proxyTargetClass配置已無效。

延伸閱讀

issue:Default CGLib proxy setting default cannot be overridden by using core framework annotations (@EnableTransactionManagement, @EnableAspectJAutoProxy) #12194 https://github.com/spring-projects/spring-boot/issues/12194

這個 issue 也聊到了關於proxyTargetClass設置失效的問題,討論內容包括:@EnableAspectJAutoProxy@EnableCaching@EnableTransactionManagement。感興趣的讀者可以自行查閱 issue。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章