反射和動態代理放有一定的相關性,但單純的說動態代理是由反射機制實現的,其實是不夠全面不准確的,動態代理是一種功能行為,而它的實現方法有很多。要怎么理解以上這句話,請看下文。
一、反射
反射機制是 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():獲取當前類(不包含父類)的聲明的所有方法;
更多方法:http://icdn.apigo.cn/blog/class-all-method.png
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 版本地址:https://mvnrepository.com/artifact/cglib/cglib
本文使用的是最新版本 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 框架的優勢:
- 可調用普通類,不需要實現接口;
- 高性能;
總結: 需要注意的是,我們在選型中,性能未必是唯一考量,可靠性、可維護性、編程工作量等往往是更主要的考慮因素,畢竟標准類庫和反射編程的門檻要低得多,代碼量也是更加可控的,如果我們比較下不同開源項目在動態代理開發上的投入,也能看到這一點。
本文所有示例代碼:https://github.com/vipstone/java-core-example.git
四、參考文檔
Java核心技術36講:http://t.cn/EwUJvWA
Java反射與動態代理:https://www.cnblogs.com/hanganglin/p/4485999.html