反射和動態代理放有一定的相關性,但單純的說動態代理是由反射機制實現的,其實是不夠全面不準確的,動態代理是一種功能行為,而它的實現方法有很多。要怎麼理解以上這句話,請看下文。
一、反射
反射機制是 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/…
關注作者公眾號:
如果覺得本文對您有幫助,請我喝杯咖啡吧。