Java提高班(六)反射和動態代理(JDK Proxy和Cglib)

markriver發表於2021-09-09

反射和動態代理放有一定的相關性,但單純的說動態代理是由反射機制實現的,其實是不夠全面不準確的,動態代理是一種功能行為,而它的實現方法有很多。要怎麼理解以上這句話,請看下文。

一、反射

反射機制是 Java 語言提供的一種基礎功能,賦予程式在執行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者物件,比如獲取某個物件的類定義,獲取類宣告的屬性和方法,呼叫方法或者構造物件,甚至可以執行時修改類定義。

1、獲取類(Class)物件

獲取類物件有三種方法:

  • 通過forName() -> 示例:Class.forName("PeopleImpl")
  • 通過getClass() -> 示例:new PeopleImpl().getClass()
  • 直接獲取.class -> 示例:PeopleImpl.class

2、類的常用方法

  • getName():獲取類完整方法;
  • getSuperclass():獲取類的父類;
  • newInstance():建立例項物件;
  • getFields():獲取當前類和父類的public修飾的所有屬性;
  • getDeclaredFields():獲取當前類(不包含父類)的宣告的所有屬性;
  • getMethod():獲取當前類和父類的public修飾的所有方法;
  • getDeclaredMethods():獲取當前類(不包含父類)的宣告的所有方法;

更多方法:icdn.apigo.cn/blog/class-…

3、類方法呼叫

反射要呼叫類中的方法,需要通過關鍵方法“invoke()”實現的,方法呼叫也分為三種:

  • 靜態(static)方法呼叫
  • 普通方法呼叫
  • 私有方法呼叫

以下會分別演示,各種呼叫的實現程式碼,各種呼叫的公共程式碼部分,如下:

// 此段程式碼為公共程式碼
interface People {
    int parentAge = 18;
    public void sayHi(String name);
}
class PeopleImpl implements People {
    private String privSex = "男";
    public String race = "漢族";
    @Override
    public void sayHi(String name) {
        System.out.println("hello," + name);
    }
    private void prvSayHi() {
        System.out.println("prvSayHi~");
    }
    public static void getSex() {
        System.out.println("18歲");
    }
}
複製程式碼

3.1 靜態方法呼叫

// 核心程式碼(省略了丟擲異常的宣告)
public static void main(String[] args) {
    Class myClass = Class.forName("example.PeopleImpl");
    // 呼叫靜態(static)方法
    Method getSex = myClass.getMethod("getSex");
    getSex.invoke(myClass);
}
複製程式碼

靜態方法的呼叫比較簡單,使用 getMethod(xx) 獲取到對應的方法,直接使用 invoke(xx)就可以了。

3.2 普通方法呼叫

普通非靜態方法呼叫,需要先獲取類示例,通過“newInstance()”方法獲取,核心程式碼如下:

Class myClass = Class.forName("example.PeopleImpl");
Object object = myClass.newInstance();
Method method = myClass.getMethod("sayHi",String.class);
method.invoke(object,"老王");
複製程式碼

getMethod 獲取方法,可以宣告需要傳遞的引數的型別。

3.3 呼叫私有方法

呼叫私有方法,必須使用“getDeclaredMethod(xx)”獲取本類所有什麼的方法,程式碼如下:

Class myClass = Class.forName("example.PeopleImpl");
Object object = myClass.newInstance();
Method privSayHi = myClass.getDeclaredMethod("privSayHi");
privSayHi.setAccessible(true); // 修改訪問限制
privSayHi.invoke(object);
複製程式碼

除了“getDeclaredMethod(xx)”可以看出,呼叫私有方法的關鍵是設定 setAccessible(true) 屬性,修改訪問限制,這樣設定之後就可以進行呼叫了。

4、總結

1.在反射中核心的方法是 newInstance() 獲取類例項,getMethod(..) 獲取方法,使用 invoke(..) 進行方法呼叫,通過 setAccessible 修改私有變數/方法的訪問限制。

2.獲取屬性/方法的時候有無“Declared”的區別是,帶有 Declared 修飾的方法或屬性,可以獲取本類的所有方法或屬性(private 到 public),但不能獲取到父類的任何資訊;非 Declared 修飾的方法或屬性,只能獲取 public 修飾的方法或屬性,並可以獲取到父類的資訊,比如 getMethod(..)和getDeclaredMethod(..)。

二、動態代理

動態代理是一種方便執行時動態構建代理、動態處理代理方法呼叫的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 呼叫、面向切面的程式設計(AOP)。

實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其他的實現方式,比如利用傳說中更高效能的位元組碼操作機制,類似 ASM、cglib(基於 ASM)等。

動態代理解決的問題?

首先,它是一個代理機制。如果熟悉設計模式中的代理模式,我們會知道,代理可以看作是對呼叫目標的一個包裝,這樣我們對目的碼的呼叫不是直接發生的,而是通過代理完成。通過代理可以讓呼叫者與實現者之間解耦。比如進行 RPC 呼叫,通過代理,可以提供更加友善的介面。還可以通過代理,可以做一個全域性的攔截器。

1、JDK Proxy 動態代理

JDK Proxy 是通過實現 InvocationHandler 介面來實現的,程式碼如下:

interface Animal {
    void eat();
}
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("The dog is eating");
    }
}
class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("The cat is eating");
    }
}

// JDK 代理類
class AnimalProxy implements InvocationHandler {
    private Object target; // 代理物件
    public Object getInstance(Object target) {
        this.target = target;
        // 取得代理物件
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("呼叫前");
        Object result = method.invoke(target, args); // 方法呼叫
        System.out.println("呼叫後");
        return result;
    }
}

public static void main(String[] args) {
    // JDK 動態代理呼叫
    AnimalProxy proxy = new AnimalProxy();
    Animal dogProxy = (Animal) proxy.getInstance(new Dog());
    dogProxy.eat();
}
複製程式碼

如上程式碼,我們實現了通過動態代理,在所有請求之前和之後列印了一個簡單的資訊。

注意: JDK Proxy 只能代理實現介面的類(即使是extends繼承類也是不可以代理的)。

JDK Proxy 為什麼只能代理實現介面的類?

這個問題要從動態代理的實現方法 newProxyInstance 原始碼說起:

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
// 省略其他程式碼
複製程式碼

來看前兩個原始碼引數說明:

* @param   loader the class loader to define the proxy class
* @param   interfaces the list of interfaces for the proxy class to implement
複製程式碼
  • loader:為類載入器,也就是 target.getClass().getClassLoader()
  • interfaces:介面代理類的介面實現列表

所以這個問題的源頭,在於 JDK Proxy 的原始碼設計。如果要執意動態代理,非介面實現類就會報錯:

Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to xxx

2、Cglib 動態代理

JDK 動態代理機制只能代理實現了介面的類,Cglib 是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因為採用的是繼承,所以不能對 final 修飾的類進行代理。

Cglib 可以通過 Maven 直接進行版本引用,Maven 版本地址:mvnrepository.com/artifact/cg…

本文使用的是最新版本 3.2.9 的 Cglib,在 pom.xml 新增如下引用:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.9</version>
</dependency>
複製程式碼

Cglib 程式碼實現,如下:

class Panda {
    public void eat() {
        System.out.println("The panda is eating");
    }
}
class CglibProxy implements MethodInterceptor {
    private Object target; // 代理物件
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        // 設定父類為例項類
        enhancer.setSuperclass(this.target.getClass());
        // 回撥方法
        enhancer.setCallback(this);
        // 建立代理物件
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("呼叫前");
        Object result = methodProxy.invokeSuper(o, objects); // 執行方法呼叫
        System.out.println("呼叫後");
        return result;
    }
}

public static void main(String[] args) {
    // CGLIB 動態代理呼叫
    CglibProxy proxy = new CglibProxy();
    Panda panda = (Panda)proxy.getInstance(new Panda());
    panda.eat();
}
複製程式碼

cglib 的呼叫通過實現 MethodInterceptor 介面的 intercept 方法,呼叫 invokeSuper 進行動態代理的,可以直接對普通類進行動態代理。

三、JDK Proxy VS Cglib

JDK Proxy 的優勢:

  • 最小化依賴關係,減少依賴意味著簡化開發和維護,JDK 本身的支援,更加可靠;
  • 平滑進行 JDK 版本升級,而位元組碼類庫通常需要進行更新以保證在新版上能夠使用;

Cglib 框架的優勢:

  • 可呼叫普通類,不需要實現介面;
  • 高效能;

總結: 需要注意的是,我們在選型中,效能未必是唯一考量,可靠性、可維護性、程式設計工作量等往往是更主要的考慮因素,畢竟標準類庫和反射程式設計的門檻要低得多,程式碼量也是更加可控的,如果我們比較下不同開源專案在動態代理開發上的投入,也能看到這一點。

本文所有示例程式碼:github.com/vipstone/ja…

四、參考文件

Java核心技術36講:t.cn/EwUJvWA

Java反射與動態代理:www.cnblogs.com/hanganglin/…


關注作者公眾號:

公眾號

如果覺得本文對您有幫助,請我喝杯咖啡吧。

相關文章