Java反射指南

前言

Java反射機制可讓咱們在編譯期(Compile Time)以外的運行期(Runtime)檢查類,接口,變量以及方法的信息。反射還可讓咱們在運行期實例化對象,調用方法,經過調用get/set方法獲取變量的值。html

Java反射機制功能強大並且很是實用。舉個例子,你能夠用反射機制把Java對象映射到數據庫表,就像Butterfly Persistence所作的那樣,或者把腳本中的一段語句在運行期映射到相應的對象調用方法上,就像 Butterfly Container在解析它的配置腳本時所作的那樣。java

目前在互聯網上已經有不勝枚舉的Java反射指南,然而大多數的指南包括Sun公司所發佈的反射指南中都僅僅只是介紹了一些反射的表面內容以及它的潛能。web

在這個系列的文章中,咱們會比其餘指南更深刻的去理解Java反射機制,它會闡述Java反射機制的基本原理包括如何去使用數組,註解,泛型以及動態代理還有類的動態加載以及類的重載的實現。同時也會向你展現如何實現一些比較有特性的功能,好比從一個類中讀取全部的get/set方法,或者訪問一個類的私有變量以及私有方法。在這個系列的指南中同時也會說明一些非反射相關的可是使人困惑的問題,好比哪些泛型信息在運行時是有效的,一些人聲稱全部的泛型信息在運行期都會消失,其實這是不對的。數據庫

改系列文章中所描述介紹的是Java 6版本的反射機制。api

Java反射的例子數組

下面是一個Java反射的簡單例子:app

Method[] methods = MyObject.class.getMethods();

for(Method method : methods){
    System.out.println("method = " + method.getName());
}

在這個例子中經過調用MyObject類的class屬性獲取對應的Class類的對象,經過這個Class類的對象獲取MyObject類中的方法集合。迭代這個方法的集合而且打印每一個方法的名字。框架

Class對象

在本節中咱們會簡短的涉及上述所說起的信息,上述的一些主題咱們會使用單獨的章節進行更詳細的描述,好比這段內容會描述如何獲取一個類的全部方法或者指定方法,可是在單獨的章節中則會向你展現如何調用反射得到的方法(Method Object),如何在多個同名方法中經過給定的參數集合匹配到指定的方法,在一個方法經過反射機制調用的時候會拋出那些異常?如何準確的獲取getter/setter方法等等。本節的內容主要是介紹Class類以及你能從Class類中獲取哪些信息。ide

在你想檢查一個類的信息以前,你首先須要獲取類的Class對象。Java中的全部類型包括基本類型(int, long, float等等),即便是數組都有與之關聯的Class類的對象。若是你在編譯期知道一個類的名字的話,那麼你可使用以下的方式獲取一個類的Class對象。工具

Class myObjectClass = MyObject.class;

若是你在編譯期不知道類的名字,可是你能夠在運行期得到到類名的字符串,那麼你則能夠這麼作來獲取Class對象:

String className = ... ;//在運行期獲取的類名字符串
   Class class = Class.forName(className);

在使用Class.forName()方法時,你必須提供一個類的全名,這個全名包括類所在的包的名字。例如MyObject類位於com.jenkov.myapp包,那麼他的全名就是com.jenkov.myapp.MyObject。
若是在調用Class.forName()方法時,沒有在編譯路徑下(classpath)找到對應的類,那麼將會拋出ClassNotFoundException。

類名

你能夠從Class對象中獲取兩個版本的類名。

經過getName() 方法返回類的全限定類名(包含包名):

Class aClass = ... //獲取Class對象,具體方式可見Class對象小節
    String className = aClass.getName();

若是你僅僅只是想獲取類的名字(不包含包名),那麼你可使用getSimpleName()方法:

Class aClass = ... //獲取Class對象,具體方式可見Class對象小節
    String simpleClassName = aClass.getSimpleName();

修飾符

能夠經過Class對象來訪問一個類的修飾符,即public,private,static等等的關鍵字,你可使用以下方法來獲取類的修飾符:

Class  aClass = ... //獲取Class對象,具體方式可見Class對象小節
    int modifiers = aClass.getModifiers();

修飾符都被包裝成一個int類型的數字,這樣每一個修飾符都是一個位標識(flag bit),這個位標識能夠設置和清除修飾符的類型。
可使用java.lang.reflect.Modifier類中的方法來檢查修飾符的類型:

Modifier.isAbstract(int modifiers);
    Modifier.isFinal(int modifiers);
    Modifier.isInterface(int modifiers);
    Modifier.isNative(int modifiers);
    Modifier.isPrivate(int modifiers);
    Modifier.isProtected(int modifiers);
    Modifier.isPublic(int modifiers);
    Modifier.isStatic(int modifiers);
    Modifier.isStrict(int modifiers);
    Modifier.isSynchronized(int modifiers);
    Modifier.isTransient(int modifiers);
    Modifier.isVolatile(int modifiers);

包信息

可使用Class對象經過以下的方式獲取包信息:

Class  aClass = ... //獲取Class對象,具體方式可見Class對象小節
    Package package = aClass.getPackage();

經過Package對象你能夠獲取包的相關信息,好比包名,你也能夠經過Manifest文件訪問位於編譯路徑下jar包的指定信息,好比你能夠在Manifest文件中指定包的版本編號。更多的Package類信息能夠閱讀java.lang.Package

父類

經過Class對象你能夠訪問類的父類,以下例:

Class superclass = aClass.getSuperclass();

能夠看到superclass對象其實就是一個Class類的實例,因此你能夠繼續在這個對象上進行反射操做。

實現的接口

能夠經過以下方式獲取指定類所實現的接口集合:

Class  aClass = ... //獲取Class對象,具體方式可見Class對象小節
   Class[] interfaces = aClass.getInterfaces();

因爲一個類能夠實現多個接口,所以getInterfaces();方法返回一個Class數組,在Java中接口一樣有對應的Class對象。
注意:getInterfaces()方法僅僅只返回當前類所實現的接口。當前類的父類若是實現了接口,這些接口是不會在返回的Class集合中的,儘管實際上當前類其實已經實現了父類接口。

構造器

你能夠經過以下方式訪問一個類的構造方法:

Constructor[] constructors = aClass.getConstructors();

更多有關Constructor的信息能夠訪問Constructors

方法

你能夠經過以下方式訪問一個類的全部方法:

Method[] method = aClass.getMethods();

更多有關Method的信息能夠訪問Methods

變量

你能夠經過以下方式訪問一個類的成員變量:

Field[] method = aClass.getFields();

更多有關Field的信息能夠訪問Fields

註解

你能夠經過以下方式訪問一個類的註解:

Annotation[] annotations = aClass.getAnnotations();

更多有關Annotation的信息能夠訪問Annotations

獲取Constructor對象

利用Java的反射機制你能夠檢查一個類的構造方法,而且能夠在運行期建立一個對象。這些功能都是經過java.lang.reflect.Constructor這個類實現的。本節將深刻的闡述Java Constructor對象。

咱們能夠經過Class對象來獲取Constructor類的實例:

Class aClass = ...//獲取Class對象
  Constructor[] constructors = aClass.getConstructors();

返回的Constructor數組包含每個聲明爲公有的(Public)構造方法。
若是你知道你要訪問的構造方法的方法參數類型,你能夠用下面的方法獲取指定的構造方法,這例子返回的構造方法的方法參數爲String類型:

Class aClass = ...//獲取Class對象
  Constructor constructor =
        aClass.getConstructor(new Class[]{String.class});

若是沒有指定的構造方法能知足匹配的方法參數則會拋出:NoSuchMethodException。

構造方法參數

你能夠經過以下方式獲取指定構造方法的方法參數信息:

Constructor constructor = ... //獲取Constructor對象
  Class[] parameterTypes = constructor.getParameterTypes();

利用Constructor對象實例化一個類

你能夠經過以下方法實例化一個類:

Constructor constructor = MyObject.class.getConstructor(String.class);
  MyObject myObject = (MyObject)
         constructor.newInstance("constructor-arg1");

constructor.newInstance()方法的方法參數是一個可變參數列表,可是當你調用構造方法的時候你必須提供精確的參數,即形參與實參必須一一對應。在這個例子中構造方法須要一個String類型的參數,那咱們在調用newInstance方法的時候就必須傳入一個String類型的參數。

獲取Field對象

使用Java反射機制你能夠運行期檢查一個類的變量信息(成員變量)或者獲取或者設置變量的值。經過使用java.lang.reflect.Field類就能夠實現上述功能。在本節會帶你深刻了解Field對象的信息。

能夠經過Class對象獲取Field對象,以下例:

Class aClass = ...//獲取Class對象
  Field[] methods = aClass.getFields();

返回的Field對象數組包含了指定類中聲明爲公有的(public)的全部變量集合。
若是你知道你要訪問的變量名稱,你能夠經過以下的方式獲取指定的變量:

Class  aClass = MyObject.class
  Field field = aClass.getField("someField");

上面的例子返回的Field類的實例對應的就是在MyObject類中聲明的名爲someField的成員變量,就是這樣:

public class MyObject{
    public String someField = null;
  }

在調用getField()方法時,若是根據給定的方法參數沒有找到對應的變量,那麼就會拋出NoSuchFieldException。

變量名稱

一旦你獲取了Field實例,你能夠經過調用Field.getName()方法獲取他的變量名稱,以下例:

Field field = ... //獲取Field對象
  String fieldName = field.getName();

變量類型

你能夠經過調用Field.getType()方法來獲取一個變量的類型(如String, int等等)

Field field = aClass.getField("someField");
  Object fieldType = field.getType();

獲取或設置(get/set)變量值

一旦你得到了一個Field的引用,你就能夠經過調用Field.get()或Field.set()方法,獲取或者設置變量的值,以下例:

Class  aClass = MyObject.class
  Field field = aClass.getField("someField");

  MyObject objectInstance = new MyObject();

  Object value = field.get(objectInstance);

  field.set(objetInstance, value);

傳入Field.get()/Field.set()方法的參數objetInstance應該是擁有指定變量的類的實例。在上述的例子中傳入的參數是MyObject類的實例,是由於someField是MyObject類的實例。
若是變量是靜態變量的話(public static)那麼在調用Field.get()/Field.set()方法的時候傳入null作爲參數而不用傳遞擁有該變量的類的實例。(譯者注:你若是傳入擁有該變量的類的實例也能夠獲得相同的結果)

獲取Method對象

使用Java反射你能夠在運行期檢查一個方法的信息以及在運行期調用這個方法,經過使用java.lang.reflect.Method類就能夠實現上述功能。在本節會帶你深刻了解Method對象的信息。

能夠經過Class對象獲取Method對象,以下例:

Class aClass = ...//獲取Class對象
   Method[] methods = aClass.getMethods();

返回的Method對象數組包含了指定類中聲明爲公有的(public)的全部變量集合。
若是你知道你要調用方法的具體參數類型,你就能夠直接經過參數類型來獲取指定的方法,下面這個例子中返回方法對象名稱是「doSomething」,他的方法參數是String類型:

Class  aClass = ...//獲取Class對象
   Method method = aClass.getMethod("doSomething", new Class[]{String.class});

若是根據給定的方法名稱以及參數類型沒法匹配到相應的方法,則會拋出NoSuchMethodException。
若是你想要獲取的方法沒有參數,那麼在調用getMethod()方法時第二個參數傳入null便可,就像這樣:

Class  aClass = ...//獲取Class對象
   Method method = aClass.getMethod("doSomething", null);

方法參數以及返回類型

你能夠獲取指定方法的方法參數是哪些:

Method method = ... //獲取Class對象
   Class[] parameterTypes = method.getParameterTypes();

你能夠獲取指定方法的返回類型:

Method method = ... //獲取Class對象
   Class returnType = method.getReturnType();

經過Method對象調用方法

你能夠經過以下方式來調用一個方法:

//獲取一個方法名爲doSomesthing,參數類型爲String的方法
   Method method = MyObject.class.getMethod("doSomething", String.class);
   Object returnValue = method.invoke(null, "parameter-value1");

傳入的null參數是你要調用方法的對象,若是是一個靜態方法調用的話則能夠用null代替指定對象做爲invoke()的參數,在上面這個例子中,若是doSomething不是靜態方法的話,你就要傳入有效的MyObject實例而不是null。
Method.invoke(Object target, Object … parameters)方法的第二個參數是一個可變參數列表,可是你必需要傳入與你要調用方法的形參一一對應的實參。就像上個例子那樣,方法須要String類型的參數,那咱們必需要傳入一個字符串。

Getter,Setter方法

使用Java反射你能夠在運行期檢查一個方法的信息以及在運行期調用這個方法,使用這個功能一樣能夠獲取指定類的getters和setters,你不能直接尋找getters和setters,你須要檢查一個類全部的方法來判斷哪一個方法是getters和setters。

首先讓咱們來規定一下getters和setters的特性:

Getter

Getter方法的名字以get開頭,沒有方法參數,返回一個值。

Setter

Setter方法的名字以set開頭,有一個方法參數。

setters方法有可能會有返回值也有可能沒有,一些Setter方法返回void,一些用來設置值,有一些對象的setter方法在方法鏈中被調用(譯者注:這類的setter方法必需要有返回值),所以你不該該妄自假設setter方法的返回值,一切應該視狀況而定。

下面是一個獲取getter方法和setter方法的例子:

public static void printGettersSetters(Class aClass){
  Method[] methods = aClass.getMethods();

  for(Method method : methods){
    if(isGetter(method)) System.out.println("getter: " + method);
    if(isSetter(method)) System.out.println("setter: " + method);
  }
}

public static boolean isGetter(Method method){
  if(!method.getName().startsWith("get"))      return false;
  if(method.getParameterTypes().length != 0)   return false;
  if(void.class.equals(method.getReturnType()) return false;
  return true;
}

public static boolean isSetter(Method method){
  if(!method.getName().startsWith("set")) return false;
  if(method.getParameterTypes().length != 1) return false;
  return true;
}

訪問私有變量

在一般的觀點中從對象的外部訪問私有變量以及方法是不容許的,可是Java反射機制能夠作到這一點。使用這個功能並不困難,在進行單元測試時這個功能很是有效。本節會向你展現如何使用這個功能。

注意:這個功能只有在代碼運行在單機Java應用(standalone Java application)中才會有效,就像你作單元測試或者一些常規的應用程序同樣。若是你在Java Applet中使用這個功能,那麼你就要想辦法去應付SecurityManager對你限制了。可是通常狀況下咱們是不會這麼作的,因此在本節裏面咱們不會探討這個問題。

要想獲取私有變量你能夠調用Class.getDeclaredField(String name)方法或者Class.getDeclaredFields()方法。Class.getField(String name)和Class.getFields()只會返回公有的變量,沒法獲取私有變量。下面例子定義了一個包含私有變量的類,在它下面是如何經過反射獲取私有變量的例子:

public class PrivateObject {

  private String privateString = null;

  public PrivateObject(String privateString) {
    this.privateString = privateString;
  }
}
PrivateObject privateObject = new PrivateObject("The Private Value");

Field privateStringField = PrivateObject.class.getDeclaredField("privateString");

privateStringField.setAccessible(true);

String fieldValue = (String) privateStringField.get(privateObject);
System.out.println("fieldValue = " + fieldValue);

這個例子會輸出」fieldValue = The Private Value」,The Private Value是PrivateObject實例的privateString私有變量的值,注意調用PrivateObject.class.getDeclaredField(「privateString」)方法會返回一個私有變量,這個方法返回的變量是定義在PrivateObject類中的而不是在它的父類中定義的變量。
注意privateStringField.setAccessible(true)這行代碼,經過調用setAccessible()方法會關閉指定類Field實例的反射訪問檢查,這行代碼執行以後不管是私有的、受保護的以及包訪問的做用域,你均可以在任何地方訪問,即便你不在他的訪問權限做用域以內。可是你若是你用通常代碼來訪問這些不在你權限做用域以內的代碼依然是不能夠的,在編譯的時候就會報錯。

訪問私有方法

訪問一個私有方法你須要調用 Class.getDeclaredMethod(String name, Class[] parameterTypes)或者Class.getDeclaredMethods() 方法。 Class.getMethod(String name, Class[] parameterTypes)和Class.getMethods()方法,只會返回公有的方法,沒法獲取私有方法。下面例子定義了一個包含私有方法的類,在它下面是如何經過反射獲取私有方法的例子:

public class PrivateObject {

  private String privateString = null;

  public PrivateObject(String privateString) {
    this.privateString = privateString;
  }

  private String getPrivateString(){
    return this.privateString;
  }
}
PrivateObject privateObject = new PrivateObject("The Private Value");

Method privateStringMethod = PrivateObject.class.
        getDeclaredMethod("getPrivateString", null);

privateStringMethod.setAccessible(true);

String returnValue = (String)
        privateStringMethod.invoke(privateObject, null);

System.out.println("returnValue = " + returnValue);

這個例子會輸出」returnValue = The Private Value」,The Private Value是PrivateObject實例的getPrivateString()方法的返回值。
PrivateObject.class.getDeclaredMethod(「privateString」)方法會返回一個私有方法,這個方法是定義在PrivateObject類中的而不是在它的父類中定義的。
一樣的,注意Method.setAcessible(true)這行代碼,經過調用setAccessible()方法會關閉指定類的Method實例的反射訪問檢查,這行代碼執行以後不管是私有的、受保護的以及包訪問的做用域,你均可以在任何地方訪問,即便你不在他的訪問權限做用域以內。可是你若是你用通常代碼來訪問這些不在你權限做用域以內的代碼依然是不能夠的,在編譯的時候就會報錯。

註解

利用Java反射機制能夠在運行期獲取Java類的註解信息。

什麼是註解

註解是Java 5的一個新特性。註解是插入你代碼中的一種註釋或者說是一種元數據(meta data)。這些註解信息能夠在編譯期使用預編譯工具進行處理(pre-compiler tools),也能夠在運行期使用Java反射機制進行處理。下面是一個類註解的例子:

@MyAnnotation(name="someName",  value = "Hello World")
   public class TheClass {
   }

在TheClass類定義的上面有一個@MyAnnotation的註解。註解的定義與接口的定義類似,下面是MyAnnotation註解的定義:

@Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.TYPE)

  public @interface MyAnnotation {
    public String name();
    public String value();
  }

在interface前面的@符號表名這是一個註解,一旦你定義了一個註解以後你就能夠將其應用到你的代碼中,就像以前咱們的那個例子那樣。
在註解定義中的兩個指示@Retention(RetentionPolicy.RUNTIME)和@Target(ElementType.TYPE),說明了這個註解該如何使用。
@Retention(RetentionPolicy.RUNTIME)表示這個註解能夠在運行期經過反射訪問。若是你沒有在註解定義的時候使用這個指示那麼這個註解的信息不會保留到運行期,這樣反射就沒法獲取它的信息。
@Target(ElementType.TYPE) 表示這個註解只能用在類型上面(好比類跟接口)。你一樣能夠把Type改成Field或者Method,或者你能夠不用這個指示,這樣的話你的註解在類,方法和變量上就均可以使用了。
關於Java註解更詳細的講解能夠訪問Java Annotations tutorial

類註解

你能夠在運行期訪問類,方法或者變量的註解信息,下是一個訪問類註解的例子:

Class aClass = TheClass.class;
Annotation[] annotations = aClass.getAnnotations();

for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("name: " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
}

你還能夠像下面這樣指定訪問一個類的註解:

Class aClass = TheClass.class;
Annotation annotation = aClass.getAnnotation(MyAnnotation.class);

if(annotation instanceof MyAnnotation){
    MyAnnotation myAnnotation = (MyAnnotation) annotation;
    System.out.println("name: " + myAnnotation.name());
    System.out.println("value: " + myAnnotation.value());
}

方法註解

下面是一個方法註解的例子:

public class TheClass {
  @MyAnnotation(name="someName",  value = "Hello World")
  public void doSomething(){}
}

你能夠像這樣訪問方法註解:

Method method = ... //獲取方法對象
Annotation[] annotations = method.getDeclaredAnnotations();

for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("name: " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
}

你能夠像這樣訪問指定的方法註解:

Method method = ... // 獲取方法對象
Annotation annotation = method.getAnnotation(MyAnnotation.class);

if(annotation instanceof MyAnnotation){
    MyAnnotation myAnnotation = (MyAnnotation) annotation;
    System.out.println("name: " + myAnnotation.name());
    System.out.println("value: " + myAnnotation.value());
}

參數註解

方法參數也能夠添加註解,就像下面這樣:

public class TheClass {
  public static void doSomethingElse(
        @MyAnnotation(name="aName", value="aValue") String parameter){
  }
}

你能夠經過Method對象來訪問方法參數註解:

Method method = ... //獲取方法對象
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();

int i=0;
for(Annotation[] annotations : parameterAnnotations){
  Class parameterType = parameterTypes[i++];

  for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("param: " + parameterType.getName());
        System.out.println("name : " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
  }
}

須要注意的是Method.getParameterAnnotations()方法返回一個註解類型的二維數組,每個方法的參數包含一個註解數組。

變量註解

下面是一個變量註解的例子:

public class TheClass {

  @MyAnnotation(name="someName",  value = "Hello World")
  public String myField = null;
}

你能夠像這樣來訪問變量的註解:

Field field = ... //獲取方法對象</pre>
<pre>Annotation[] annotations = field.getDeclaredAnnotations();

for(Annotation annotation : annotations){
 if(annotation instanceof MyAnnotation){
 MyAnnotation myAnnotation = (MyAnnotation) annotation;
 System.out.println("name: " + myAnnotation.name());
 System.out.println("value: " + myAnnotation.value());
 }
}

你能夠像這樣訪問指定的變量註解:

Field field = ...//獲取方法對象
Annotation annotation = field.getAnnotation(MyAnnotation.class);

if(annotation instanceof MyAnnotation){
 MyAnnotation myAnnotation = (MyAnnotation) annotation;
 System.out.println("name: " + myAnnotation.name());
 System.out.println("value: " + myAnnotation.value());
}

泛型反射

我經常在一些文章以及論壇中讀到說Java泛型信息在編譯期被擦除(erased)因此你沒法在運行期得到有關泛型的信息。其實這種說法並不徹底正確的,在一些狀況下是能夠在運行期獲取到泛型的信息。這些狀況其實覆蓋了一些咱們須要泛型信息的需求。在本節中咱們會演示一下這些狀況。

運用泛型反射的經驗法則

下面是兩個典型的使用泛型的場景:
一、聲明一個須要被參數化(parameterizable)的類/接口。
二、使用一個參數化類。

當你聲明一個類或者接口的時候你能夠指明這個類或接口能夠被參數化,java.util.List接口就是典型的例子。你能夠運用泛型機制建立一個標明存儲的是String類型list,這樣比你建立一個Object的list要更好。

當你想在運行期參數化類型自己,好比你想檢查java.util.List類的參數化類型,你是沒有辦法能知道他具體的參數化類型是什麼。這樣一來這個類型就能夠是一個應用中全部的類型。可是,當你檢查一個使用了被參數化的類型的變量或者方法,你能夠得到這個被參數化類型的具體參數。總之:

你不能在運行期獲知一個被參數化的類型的具體參數類型是什麼,可是你能夠在用到這個被參數化類型的方法以及變量中找到他們,換句話說就是獲知他們具體的參數化類型。
在下面的段落中會向你演示這類狀況。

泛型方法返回類型

若是你得到了java.lang.reflect.Method對象,那麼你就能夠獲取到這個方法的泛型返回類型信息。若是方法是在一個被參數化類型之中(譯者注:如T fun())那麼你沒法獲取他的具體類型,可是若是方法返回一個泛型類(譯者注:如List fun())那麼你就能夠得到這個泛型類的具體參數化類型。你能夠在「Java Reflection: Methods」中閱讀到有關如何獲取Method對象的相關內容。下面這個例子定義了一個類這個類中的方法返回類型是一個泛型類型:

public class MyClass {

  protected List<String> stringList = ...;

  public List<String> getStringList(){
    return this.stringList;
  }
}

咱們能夠獲取getStringList()方法的泛型返回類型,換句話說,咱們能夠檢測到getStringList()方法返回的是List而不只僅只是一個List。以下例:

Method method = MyClass.class.getMethod("getStringList", null);

Type returnType = method.getGenericReturnType();

if(returnType instanceof ParameterizedType){
    ParameterizedType type = (ParameterizedType) returnType;
    Type[] typeArguments = type.getActualTypeArguments();
    for(Type typeArgument : typeArguments){
        Class typeArgClass = (Class) typeArgument;
        System.out.println("typeArgClass = " + typeArgClass);
    }
}

這段代碼會打印出 「typeArgClass = java.lang.String」,Type[]數組typeArguments只有一個結果 – 一個表明java.lang.String的Class類的實例。Class類實現了Type接口。

泛型方法參數類型

你一樣能夠經過反射來獲取方法參數的泛型類型,下面這個例子定義了一個類,這個類中的方法的參數是一個被參數化的List:

public class MyClass {
  protected List<String> stringList = ...;

  public void setStringList(List<String> list){
    this.stringList = list;
  }
}

你能夠像這樣來獲取方法的泛型參數:

method = Myclass.class.getMethod("setStringList", List.class);

Type[] genericParameterTypes = method.getGenericParameterTypes();

for(Type genericParameterType : genericParameterTypes){
    if(genericParameterType instanceof ParameterizedType){
        ParameterizedType aType = (ParameterizedType) genericParameterType;
        Type[] parameterArgTypes = aType.getActualTypeArguments();
        for(Type parameterArgType : parameterArgTypes){
            Class parameterArgClass = (Class) parameterArgType;
            System.out.println("parameterArgClass = " + parameterArgClass);
        }
    }
}

這段代碼會打印出」parameterArgType = java.lang.String」。Type[]數組parameterArgTypes只有一個結果 – 一個表明java.lang.String的Class類的實例。Class類實現了Type接口。

泛型變量類型

一樣能夠經過反射來訪問公有(Public)變量的泛型類型,不管這個變量是一個類的靜態成員變量或是實例成員變量。你能夠在「Java Reflection: Fields」中閱讀到有關如何獲取Field對象的相關內容。這是以前的一個例子,一個定義了一個名爲stringList的成員變量的類。

public class MyClass {
  public List<String> stringList = ...;
}
Field field = MyClass.class.getField("stringList");

Type genericFieldType = field.getGenericType();

if(genericFieldType instanceof ParameterizedType){
    ParameterizedType aType = (ParameterizedType) genericFieldType;
    Type[] fieldArgTypes = aType.getActualTypeArguments();
    for(Type fieldArgType : fieldArgTypes){
        Class fieldArgClass = (Class) fieldArgType;
        System.out.println("fieldArgClass = " + fieldArgClass);
    }
}

這段代碼會打印出」fieldArgClass = java.lang.String」。Type[]數組fieldArgClass只有一個結果 – 一個表明java.lang.String的Class類的實例。Class類實現了Type接口。

數組

利用反射機制來處理數組會有點棘手。尤爲是當你想要得到一個數組的Class對象,好比int[]等等。本節會討論經過反射機制建立數組和如何獲取數組的Class對象。

注意:在閱讀Eyal Lupu的博客文章「Two Side Notes About Arrays and Reflection」以後對本文的內容作了更新。目前這個版本參考了這篇博文裏面的內容。

java.lang.reflect.Array

Java反射機制經過java.lang.reflect.Array這個類來處理數組。不要把這個類與Java集合套件(Collections suite)中的java.util.Arrays混淆,java.util.Arrays是一個提供了遍歷數組,將數組轉化爲集合等工具方法的類。

建立一個數組

Java反射機制經過java.lang.reflect.Array類來建立數組。下面是一個如何建立數組的例子:

int[] intArray = (int[]) Array.newInstance(int.class, 3);

這個例子建立一個int類型的數組。Array.newInstance()方法的第一個參數表示了咱們要建立一個什麼類型的數組。第二個參數表示了這個數組的空間是多大。

訪問一個數組

經過Java反射機制一樣能夠訪問數組中的元素。具體可使用Array.get(…)和Array.set(…)方法來訪問。下面是一個例子:

int[] intArray = (int[]) Array.newInstance(int.class, 3);

Array.set(intArray, 0, 123);
Array.set(intArray, 1, 456);
Array.set(intArray, 2, 789);

System.out.println("intArray[0] = " + Array.get(intArray, 0));
System.out.println("intArray[1] = " + Array.get(intArray, 1));
System.out.println("intArray[2] = " + Array.get(intArray, 2));

這個例子會輸出:

intArray[0] = 123
intArray[1] = 456
intArray[2] = 789

獲取數組的Class對象

在我編寫Butterfly DI Container的腳本語言時,當我想經過反射獲取數組的Class對象時遇到了一點麻煩。若是不經過反射的話你能夠這樣來獲取數組的Class對象:

Class stringArrayClass = String[].class;

若是使用Class.forName()方法來獲取Class對象則不是那麼簡單。好比你能夠像這樣來得到一個原生數據類型(primitive)int數組的Class對象:

Class intArray = Class.forName("[I");

在JVM中字母I表明int類型,左邊的‘[’表明我想要的是一個int類型的數組,這個規則一樣適用於其餘的原生數據類型。
對於普通對象類型的數組有一點細微的不一樣:

Class stringArrayClass = Class.forName("[Ljava.lang.String;");

注意‘[L’的右邊是類名,類名的右邊是一個‘;’符號。這個的含義是一個指定類型的數組。
須要注意的是,你不能經過Class.forName()方法獲取一個原生數據類型的Class對象。下面這兩個例子都會報ClassNotFoundException:

Class intClass1 = Class.forName("I");
Class intClass2 = Class.forName("int");

我一般會用下面這個方法來獲取普通對象以及原生對象的Class對象:

public Class getClass(String className){
  if("int" .equals(className)) return int .class;
  if("long".equals(className)) return long.class;
  ...
  return Class.forName(className);
}

一旦你獲取了類型的Class對象,你就有辦法輕鬆的獲取到它的數組的Class對象,你能夠經過指定的類型建立一個空的數組,而後經過這個空的數組來獲取數組的Class對象。這樣作有點討巧,不過頗有效。以下例:

Class theClass = getClass(theClassName);
Class stringArrayClass = Array.newInstance(theClass, 0).getClass();

這是一個特別的方式來獲取指定類型的指定數組的Class對象。無需使用類名或其餘方式來獲取這個Class對象。
爲了確保Class對象是否是表明一個數組,你可使用Class.isArray()方法來進行校驗:

Class stringArrayClass = Array.newInstance(String.class, 0).getClass();
System.out.println("is array: " + stringArrayClass.isArray());

獲取數組的成員類型

一旦你獲取了一個數組的Class對象,你就能夠經過Class.getComponentType()方法獲取這個數組的成員類型。成員類型就是數組存儲的數據類型。例如,數組int[]的成員類型就是一個Class對象int.class。String[]的成員類型就是java.lang.String類的Class對象。
下面是一個訪問數組成員類型的例子:

String[] strings = new String[3];
Class stringArrayClass = strings.getClass();
Class stringArrayComponentType = stringArrayClass.getComponentType();
System.out.println(stringArrayComponentType);

下面這個例子會打印「java.lang.String」表明這個數組的成員類型是字符串。

動態代理

利用Java反射機制你能夠在運行期動態的建立接口的實現。java.lang.reflect.Proxy類就能夠實現這一功能。這個類的名字(譯者注:Proxy意思爲代理)就是爲何把動態接口實現叫作動態代理。動態的代理的用途十分普遍,好比數據庫鏈接和事物管理(transaction management)還有單元測試時用到的動態mock對象以及AOP中的方法攔截功能等等都使用到了動態代理。

建立代理

你能夠經過使用Proxy.newProxyInstance()方法建立動態代理。newProxyInstance()方法有三個參數:
一、類加載器(ClassLoader)用來加載動態代理類。
二、一個要實現的接口的數組。
三、一個InvocationHandler把全部方法的調用都轉到代理上。
以下例:

InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
                            MyInterface.class.getClassLoader(),
                            new Class[] { MyInterface.class },
                            handler);

在執行完這段代碼以後,變量proxy包含一個MyInterface接口的的動態實現。全部對proxy的調用都被轉向到實現了InvocationHandler接口的handler上。有關InvocationHandler的內容會在下一段介紹。

InvocationHandler接口

在前面提到了當你調用Proxy.newProxyInstance()方法時,你必需要傳入一個InvocationHandler接口的實現。全部對動態代理對象的方法調用都會被轉向到InvocationHandler接口的實現上,下面是InvocationHandler接口的定義:

public interface InvocationHandler{
  Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

下面是它的實現類的定義:

public class MyInvocationHandler implements InvocationHandler{

  public Object invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    //do something "dynamic"
  }
}

傳入invoke()方法中的proxy參數是實現要代理接口的動態代理對象。一般你是不須要他的。

invoke()方法中的Method對象參數表明了被動態代理的接口中要調用的方法,從這個method對象中你能夠獲取到這個方法名字,方法的參數,參數類型等等信息。關於這部份內容能夠查閱以前有關Method的文章。

Object數組參數包含了被動態代理的方法須要的方法參數。注意:原生數據類型(如int,long等等)方法參數傳入等價的包裝對象(如Integer, Long等等)。

常見用例

動態代理常被應用到如下幾種狀況中

  • 數據庫鏈接以及事物管理
  • 單元測試中的動態Mock對象
  • 自定義工廠與依賴注入(DI)容器之間的適配器
  • 相似AOP的方法攔截器

數據庫鏈接以及事物管理

Spring框架中有一個事物代理可讓你提交/回滾一個事物。它的具體原理在 Advanced Connection and Transaction Demarcation and Propagation一文中有詳細描述,因此在這裏我就簡短的描述一下,方法調用序列以下:

web controller --> proxy.execute(...);
  proxy --> connection.setAutoCommit(false);
  proxy --> realAction.execute();
    realAction does database work
  proxy --> connection.commit();

單元測試中的動態Mock對象

Butterfly Testing工具經過動態代理來動態實現樁(stub),mock和代理類來進行單元測試。在測試類A的時候若是用到了接口B,你能夠傳給A一個實現了B接口的mock來代替實際的B接口實現。全部對接口B的方法調用都會被記錄,你能夠本身來設置B的mock中方法的返回值。
並且Butterfly Testing工具可讓你在B的mock中包裝真實的B接口實現,這樣全部調用mock的方法都會被記錄,而後把調用轉向到真實的B接口實現。這樣你就能夠檢查B中方法真實功能的調用狀況。例如:你在測試DAO時你能夠把真實的數據庫鏈接包裝到mock中。這樣的話就與真實的狀況同樣,DAO能夠在數據庫中讀寫數據,mock會把對數據庫的讀寫操做指令都傳給數據庫,你能夠經過mock來檢查DAO是否是以正確的方式來使用數據庫鏈接,好比你能夠檢查是否調用了connection.close()方法。這種狀況是不能簡單的依靠調用DAO方法的返回值來判斷的。

自定義工廠與依賴注入(DI)容器之間的適配器

依賴注入容器Butterfly Container有一個很是強大的特性可讓你把整個容器注入到這個容器生成的bean中。可是,若是你不想依賴這個容器的接口,這個容器能夠適配你本身定義的工廠接口。你僅僅須要這個接口而不是接口的實現,這樣這個工廠接口和你的類看起來就像這樣:

public interface IMyFactory {
  Bean   bean1();
  Person person();
  ...
}
public class MyAction{

  protected IMyFactory myFactory= null;

  public MyAction(IMyFactory factory){
    this.myFactory = factory;
  }

  public void execute(){
    Bean bean = this.myFactory.bean();
    Person person = this.myFactory.person();
  }

}

當MyAction類調用經過容器注入到構造方法中的IMyFactory實例的方法時,這個方法調用實際先調用了IContainer.instance()方法,這個方法可讓你從容器中獲取實例。這樣這個對象能夠把Butterfly Container容器在運行期當成一個工廠使用,比起在建立這個類的時候進行注入,這種方式顯然更好。並且這種方法沒有依賴到Butterfly Container中的任何接口。

相似AOP的方法攔截器

Spring框架能夠攔截指定bean的方法調用,你只需提供這個bean繼承的接口。Spring使用動態代理來包裝bean。全部對bean中方法的調用都會被代理攔截。代理能夠判斷在調用實際方法以前是否須要調用其餘方法或者調用其餘對象的方法,還能夠在bean的方法調用完畢以後再調用其餘的代理方法。

動態類加載重載
Java容許你在運行期動態加載和重載類,可是這個功能並無像人們但願的那麼簡單直接。這篇文章將闡述在Java中如何加載以及重載類。
你可能會質疑爲何Java動態類加載特性是Java反射機制的一部分而不是Java核心平臺的一部分。無論怎樣,這篇文章被放到了Java反射系列裏面並且也沒有更好的系列來包含它了。

類加載器

全部Java應用中的類都是被java.lang.ClassLoader類的一系列子類加載的。所以要想動態加載類的話也必須使用java.lang.ClassLoader的子類。

一個類一旦被加載時,這個類引用的全部類也同時會被加載。類加載過程是一個遞歸的模式,全部相關的類都會被加載。但並不必定是一個應用裏面全部類都會被加載,與這個被加載類的引用鏈無關的類是不會被加載的,直到有引用關係的時候它們纔會被加載。

類加載體系

在Java中類加載是一個有序的體系。當你新建立一個標準的Java類加載器時你必須提供它的父加載器。當一個類加載器被調用來加載一個類的時候,首先會調用這個加載器的父加載器來加載。若是父加載器沒法找到這個類,這時候這個加載器纔會嘗試去加載這個類。

類加載

類加載器加載類的順序以下:
一、檢查這個類是否已經被加載。
二、若是沒有被加載,則首先調用父加載器加載。
三、若是父加載器不能加載這個類,則嘗試加載這個類。

當你實現一個有重載類功能的類加載器,它的順序與上述會有些不一樣。類重載不會請求的他的父加載器來進行加載。在後面的段落會進行講解。

動態類加載

動態加載一個類十分簡單。你要作的就是獲取一個類加載器而後調用它的loadClass()方法。下面是個例子:

public class MainClass {

  public static void main(String[] args){

    ClassLoader classLoader = MainClass.class.getClassLoader();

    try {
        Class aClass = classLoader.loadClass("com.jenkov.MyClass");
        System.out.println("aClass.getName() = " + aClass.getName());
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
  }
}

動態類重載

動態類重載有一點複雜。Java內置的類加載器在加載一個類以前會檢查它是否已經被加載。所以重載一個類是沒法使用Java內置的類加載器的,若是想要重載一個類你須要手動繼承ClassLoader。

在你定製ClassLoader的子類以後,你還有一些事須要作。全部被加載的類都須要被連接。這個過程是經過ClassLoader.resolve()方法來完成的。因爲這是一個final方法,所以這個方法在ClassLoader的子類中是沒法被重寫的。resolve()方法是不會容許給定的ClassLoader實例連接一個類兩次。因此每當你想要重載一個類的時候你都須要使用一個新的ClassLoader的子類。你在設計類重載功能的時候這是必要的條件。

自定義類重載

在前面已經說過你不能使用已經加載過類的類加載器來重載一個類。所以你須要其餘的ClassLoader實例來重載這個類。可是這又帶來了一些新的挑戰。

全部被加載到Java應用中的類都以類的全名(包名 + 類名)做爲一個惟一標識來讓ClassLoader實例來加載。這意味着,類MyObject被類加載器A加載,若是類加載器B又加載了MyObject類,那麼兩個加載器加載出來的類是不一樣的。看看下面的代碼:

MyObject object = (MyObject)
    myClassReloadingFactory.newInstance("com.jenkov.MyObject");

MyObject類在上面那段代碼中被引用,它的變量名是object。這就致使了MyObject這個類會被這段代碼所在類的類加載器所加載。

若是myClassReloadingFactory工廠對象使用不一樣的類加載器重載MyObject類,你不能把重載的MyObject類的實例轉換(cast)到類型爲MyObject的對象變量。一旦MyObject類分別被兩個類加載器加載,那麼它就會被認爲是兩個不一樣的類,儘管它們的類的全名是徹底同樣的。你若是嘗試把這兩個類的實例進行轉換就會報ClassCastException。
你能夠解決這個限制,不過你須要從如下兩個方面修改你的代碼:
一、標記這個變量類型爲一個接口,而後只重載這個接口的實現類。
二、標記這個變量類型爲一個超類,而後只重載這個超類的子類。

請看下面這兩個例子:

MyObjectInterface object = (MyObjectInterface)
    myClassReloadingFactory.newInstance("com.jenkov.MyObject");

 

MyObjectSuperclass object = (MyObjectSuperclass)
    myClassReloadingFactory.newInstance("com.jenkov.MyObject");

只要保證變量的類型是超類或者接口,這兩個方法就能夠正常運行,當它們的子類或是實現類被重載的時候超類跟接口是不會被重載的。

爲了保證這種方式能夠運行你須要手動實現類加載器而後使得這些接口或超類能夠被它的父加載器加載。當你的類加載器加載MyObject類時,超類MyObjectSuperclass或者接口MyObjectSuperclass也會被加載,由於它們是MyObject的依賴。你的類加載器必需要代理這些類的加載到同一個類加載器,這個類加載器加載這個包括接口或者超類的類。

類加載/重載示例

光說不練假把式。讓咱們看看一個簡單的例子。下面這個例子是一個類加載器的子類。注意在這個類不想被重載的狀況下它是如何把對一個類的加載代理到它的父加載器上的。若是一個類被它的父加載器加載,這個類之後將不能被重載。記住,一個類只能被同一個ClassLoader實例加載一次。
就像我以前說的那樣,這僅僅是一個簡單的例子,經過這個例子會向你展現類加載器的基本行爲。這並非一個可讓你直接用於設計你項目中類加載器的模板。你本身設計的類加載器應該不只僅只有一個,若是你想用來重載類的話你可能會設計不少加載器。而且你也不會像下面這樣將須要加載的類的路徑硬編碼(hardcore)到你的代碼中。

public class MyClassLoader extends ClassLoader{

    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }

    public Class loadClass(String name) throws ClassNotFoundException {
        if(!"reflection.MyObject".equals(name))
                return super.loadClass(name);

        try {
            String url = "file:C:/data/projects/tutorials/web/WEB-INF/" +
                            "classes/reflection/MyObject.class";
            URL myUrl = new URL(url);
            URLConnection connection = myUrl.openConnection();
            InputStream input = connection.getInputStream();
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int data = input.read();

            while(data != -1){
                buffer.write(data);
                data = input.read();
            }
            input.close();

            byte[] classData = buffer.toByteArray();

            return defineClass("reflection.MyObject",classData, 0, classData.length);

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

下面是使用MyClassLoader的例子:

public static void main(String[] args) throws
    ClassNotFoundException,
    IllegalAccessException,
    InstantiationException {

    ClassLoader parentClassLoader = MyClassLoader.class.getClassLoader();
    MyClassLoader classLoader = new MyClassLoader(parentClassLoader);
    Class myObjectClass = classLoader.loadClass("reflection.MyObject");

    AnInterface2 object1 = (AnInterface2) myObjectClass.newInstance();

    MyObjectSuperClass object2 = (MyObjectSuperClass) myObjectClass.newInstance();

    //create new class loader so classes can be reloaded.
    classLoader = new MyClassLoader(parentClassLoader);
    myObjectClass = classLoader.loadClass("reflection.MyObject");

    object1 = (AnInterface2) myObjectClass.newInstance();
    object2 = (MyObjectSuperClass) myObjectClass.newInstance();
}

下面這個就是被加載的reflection.MyObject類。注意它既繼承了一個超類而且也實現了一個接口。這樣作僅僅是爲了經過例子演示這個特性。在你自定義的狀況下你可能僅會實現一個類或者繼承一兩個接口。

public class MyObject extends MyObjectSuperClass implements AnInterface2{
    //... body of class ... override superclass methods
    //    or implement interface methods
}
相關文章
相關標籤/搜索