Java類型信息與應用--動態代理
本文結構
- 一、前言
- 二、爲什麼需要RTTI
- 三、RTTI在java中的工作原理
- 四、類型轉化前先做檢測
- 五、動態代理
- 六、動態代理的不足
一、前言
運行時信息使你可以在程序運行時發現和使用類型信息
Java在運行時識別對象和類的信息的方式:
1.一種是RTTI,它假定我們在編譯時已經知道了所有的類型。
2.另一種是“反射“機制,它允許我們在運行時發現和使用類的信息。
這帶來的好處是,你可以在程序運行時發現和使用類型信息
二、爲什麼需要RTTI
以多態爲例,如下圖基類是Shape(泛型),而派生出來的具體類有Circle,Square和Triangle。
abstract class Shape {
// this 調用當前類的toString()方法,返回實際的內容
void draw(){ System.out.println(this + "draw()"); }
abstract public String toString();
}
class Circle extends Shape {
public String toString(){ return "Circle"; }
}
class Square extends Shape {
public String toString(){ return "Square"; }
}
class Triangle extends Shape {
public String toString(){ return "Triangle"; }
}
public static void main(String[] args){
// 把Shape對象放入List<Shape>的數組的時候會向上轉型爲Shape,從而丟失了具體的類型信息
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
// 從數組中取出時,這種容器,實際上所有的元素都當成Object持有,會自動將結果轉型爲Shape,這就是RTTI的基本的使用。
for(Shape shape : shapeList){
shape.draw();
}
}
打印出以下結果
Circledraw()
Squaredraw()
Triangledraw()
RTTI在運行時識別一個對象類型,Shape對象具體執行什麼的代碼,由引用所指向的具體對象Circle、Square、或Triangle而決定
三、RTTI在java中的工作原理
在運行時獲取類型信息是通過Class對象實現的,java通過Class對象(每個類都有一個Class對象)來執行其RTTI,而Java虛擬機通過“類加載器“的子系統,生成Class對象。
所有的類在第一次使用時,都會動態加載到JVM中。流程如下圖:
class Bird{
public static final int foot=2;
static{
System.out.println("Bird coming");
}
}
class Tiger{
public static int hair=(int)(Math.random()*100);
static{
System.out.println("tiger coming");
}
}
public class Zoo {
public static void main(String[] args) throws ClassNotFoundException{
Class tiger=Class.forName("com.jaking.RTTI.Tiger");
System.out.println("Class forName初始化");
print(Tiger.hair);
//--------------------------
Bird cat= new Bird();
System.out.println("new 初始化");
print(Bird.foot);
}
public static void print(int str) {
System.out.println(str);
}
}
打印出以下結果
tiger coming
Class forName初始化
57
Bird coming
new 初始化
2
可以看出通過用forName()或是new構造器都會觸發上述流程,
3.2、類字面常量
java有兩種獲取class對象的方法
- Class.getName(類的全限定名);
- FancyToy.class
萬物皆對象,這句話在java中體現的淋漓盡致,
基本類型也可以通過方法二獲取Class對象,如int.class
而通過方法二獲取Class對象在加載中會有些區別,我們先看下面代碼
class Dog{
public static final int foot=4;
public static final int hair=(int)(Math.random()*100);//runtime
static{
System.out.println("dog coming");
}
}
public class Zoo {
public static void main(String[] args) throws ClassNotFoundException{
Class<Dog> dog=Dog.class;//
System.out.println("類字面常量初始化");
print(Dog.foot);//compile constant
print(Dog.hair);//jiazai
}
public static void print(int str) {
System.out.println(str);
}
}
打印出了
類字面常量初始化
4
dog coming
75
“類字面常量初始化“在“dog coming“前面打印,可以看出通過方法二獲取Class對象時,不會立刻初始化,而是被延遲到了對靜態方法或非常量靜態域進行首次引用纔開始執行。
3.3、泛化的Class引用
Class引用表示的是它所指向的對象的確切類型,而該對象便是Class類的一個對象。在JavaSE5中,可以通過泛型對Class引用所指向的Class對象進行限定,並且可以讓編譯器強制執行額外的類型檢查:
Class intCls = int.class;
// 使用泛型限定Class指向的引用
Class<Integer> genIntCls = int.class;
// 沒有使用泛型的Clas可以重新賦值爲指向任何其他的Class對象
intCls = double.class;
// 下面的編譯會出錯
// genIntCls = double.class;
使用通配符?放鬆泛型的限定:
Class<?> genIntCls = int.class;
genIntCls = double.class;//編譯通過
? extends Number將引用範圍限制爲Number及其子類
Class<? extends Number> num = int.class;
// num的引用範圍爲Number及其子類
num = double.class;
num = Number.class;
? super B將引用範圍限制爲B及其父類
class A{}
class B extends A{}
class C extends A{}
public class Zoo {
public static void main(String[] args){
Class<? super B> aClass=A.class;//將引用
Class<? super B> aClass=B.class;
//Class<? super B> aClass=C.class;//編譯不通過
}
}
四、類型轉化前先做檢測
有些類型如果你強制進行類型轉化,如果不匹配的話就會拋出ClassCastException異常,通過關鍵字instanceof,告訴我們對象是不是某個特定類型的實例,再執行轉化。
class A{}
class B extends A{}
class C extends A{}
public class Zoo {
public static void main(String[] args){
A a=new B();
if (a instanceof B) {
a=(B)a;//上轉型爲B
}
}
}
五、動態代理
5.1靜態代理模式
下面是展示代理結構的簡單例子:
interface Subject {
public void doSomething();
}
//被代理類
class RealSub implements Subject {
public void doSomething(){
System.out.println( "RealSub call doSomething()" );
}
}
//代理類
class SubjectProxy implements Subject {
Subject subimpl = new RealSub();
public void doSomething(){
subimpl.doSomething();
}
}
public class ProxyClient {
public static void main(String[] args) {
Subject sub = new SubjectProxy();
sub.doSomething();
}
}
代理模式的類圖
- 抽象主題角色(Subject):該類的主要職責是聲明真實主題與代理的共同接口方法。
- 真實主題角色(RealSub):也稱爲委託角色或者被代理角色。定義了代理對象所代表的真實對象。
- 代理主題角色(SubjectProxy):也叫委託類、代理類。該類持有真實主題類的引用,再實現接口的方法中調用真實主題類中相應的接口方法執行,以此起到代理作用。
客戶類(ProxyClient) :即使用代理類的類型
代理模式又分爲靜態代理和動態代理。靜態代理是由用戶創建或特定工具自動生成源代碼,再對其編譯。在程序運行前,代理類的.class文件就已經存在了。動態代理是在程序運行時,通過運用反射機制動態的創建而成。
5.2、動態代理
Java 動態代理機制的出現,使得 Java 開發人員不用手工編寫代理類,只要簡單地指定一組接口及委託類對象,便能動態地獲得代理類。代理類會負責將所有的方法調用分派到委託對象上反射執行,在分派執行的過程中,開發人員還可以按需調整委託類對象及其功能,這是一套非常靈活有彈性的代理框架。
5.2.1、Jdk動態代理
Jdk的動態代理是基於接口的。現在想要爲RealSubject這個類創建一個動態代理對象,Jdk主要會做一下工作:
- 獲取RealSubject上的所有接口列表
- 確定要生成的代理類的類名,默認爲:com.sun.proxy.$ProxyN(包名與這些接口的包名相同,生成代理類的類名,格式“$ProxyN”,其中 N 是一個逐一遞增的阿拉伯數字,代表 Proxy 類第 N 次生成的動態代理類,值得注意的一點是,並不是每次調用 Proxy 的靜態方法創建動態代理類都會使得 N 值增加,原因是如果對同一組接口(包括接口排列的順序相同)試圖重複創建動態代理類,它會很聰明地返回先前已經創建好的代理類的類對象,而不會再嘗試去創建一個全新的代理類,這樣可以節省不必要的代碼重複生成,提高了代理類的創建效率);
- 根據需要實現的接口信息,在代碼中動態創建該Proxy類的字節碼;
- 創建InvocationHandler實例handler,用來處理Proxy所有方法的調用;
- Proxy的class對象以創建的handler對象爲參數,實例化一個proxy對象;
- Jdk通過java.lang.reflect.Proxy包來支持動態代理,在Java中要創建一個代理對象,必須調用Proxy類的靜態方法newProxyInstance()獲取代理對象。
下面爲實例代碼
package com.jaking.dynamicproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//抽象主題類B ,JDK的動態代理是基於接口的,所以一定要是interface
interface SubjectA {
public abstract void request();
}
// 抽象主題類B
interface SubjectB {
public abstract void doSomething();
}
// 真實主題類A,即被代理類
class RealSubjectA implements SubjectA {
public void request() {
System.out.println("RealSubjectA request() ...");
}
}
// 真實主題類B,即被代理類
class RealSubjectB implements SubjectB {
public void doSomething() {
System.out.println("RealSubjectB doSomething() ...");
}
}
// 動態代理類,實現InvocationHandler接口
class DynamicProxy implements InvocationHandler {
Object obj = null;
public DynamicProxy(Object obj) {
this.obj = obj;
}
/**
* 覆蓋InvocationHandler接口中的invoke()方法
*
* 更重要的是,動態代理模式可以使得我們在不改變原來已有的代碼結構 的情況下,對原來的“真實方法”進行擴展、增強其功能,並且可以達到
* 控制被代理對象的行爲,下面的before、after就是我們可以進行特殊 代碼切入的擴展點了。
*
* @param proxy
* ,表示執行這個方法的代理對象;
* @param method
* ,表示真實對象實際需要執行的方法(關於Method類參見Java的反射機制);
* @param args
* ,表示真實對象實際執行方法時所需的參數。
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
/*
* before :doSomething();
*/
Object result = method.invoke(this.obj, args);
/*
* after : doSomething();
*/
return result;
}
}
// 測試類
public class Client {
public static void main(String[] args) {
// 被代理類的實例
SubjectA realSubjectA = new RealSubjectA();
// loader,表示類加載器,對於不同來源(系統庫或網絡等)的類需要不同的類加載器來加載,這是Java安全模型的一部分。
// 可以使用null來使用默認的加載器;
ClassLoader loader = realSubjectA.getClass().getClassLoader();
// interfaces,表示接口或對象的數組,它就是前述代理對象和真實對象都必須共有的父類或者接口;
Class<?>[] interfaces = realSubjectA.getClass().getInterfaces();
// handler,表示調用處理器,它必須是實現了InvocationHandler接口的對象,其作用是定義代理對象中需要執行的具體操作。
InvocationHandler handler = new DynamicProxy(realSubjectA);
// 獲得代理的實例 A
SubjectA proxyA = (SubjectA) Proxy.newProxyInstance(loader, interfaces,
handler);
proxyA.request();
RealSubjectB realSubjectB = new RealSubjectB();
// 獲得代理的實例 B
SubjectB proxyB = (SubjectB) Proxy.newProxyInstance(realSubjectB
.getClass().getClassLoader(), realSubjectB.getClass()
.getInterfaces(), new DynamicProxy(realSubjectB));
proxyB.doSomething();
// 打印生成代理類的類名
System.out.println(proxyA.getClass().getSimpleName());
System.out.println(proxyB.getClass().getSimpleName());
}
}
運行打印出
RealSubjectA request() ...
RealSubjectB doSomething() ...
$Proxy0
$Proxy1
控制檯打印出$Proxy0,$Proxy1可以證明$Proxy0和$Proxy1是JVM在運行時生成的動態代理類,這也就是動態代理的核心所在,我們不用爲每個被代理類實現一個代理類,只需要實現接口InvocationHandler,並在客戶端中調用靜態方法Proxy.newProxyInstance( )就能獲取到代理類對象
下圖爲動態代理類$ProxyN的繼承圖
由圖可見,
- Proxy 類是它的父類,這個規則適用於所有由 Proxy 創建的動態代理類。而且該類還實現了其所代理的一組接口,這就是爲什麼它能夠被安全地類型轉換到其所代理的某接口的根本原因。
- 被代理的一組接口有以下特點。
(1)要注意不能有重複的接口,以避免動態代理類代碼生成時的編譯錯誤。
(2)這些接口對於類裝載器必須可見,否則類裝載器將無法鏈接它們,將會導致類定義失敗。
(3)需被代理的所有非 public 的接口必須在同一個包中,否則代理類生成也會失敗。
(4)接口的數目不能超過 65535,這是 JVM 設定的限制。
六、動態代理的不足
動態代理的性能會比較差一些。理由很簡單,因爲反射地分派方法而不是採用內置的虛方法分派,可能有一些性能上的成本,但是通過動態代理可以簡化大量代碼,大大減低耦合度,如Spring中的AOP,Struts2中的攔截器就是使用動態代理,至於性能與便捷有時需要權衡使用。