使用過Spring框架進行web開發的應該都知道,Spring的兩大核心技術是IOC和AOP。而其中IOC又是AOP的支撐。IOC要求由容器來幫我們自動創建Bean實例並完成依賴注入。IOC容器的代碼在實現時肯定不知道要創建哪些Bean,並且這些Bean之間的依賴關系是怎樣的(如果寫死在里面,這框架還能用嗎?)。所以其必須在運行期通過掃描配置文件或注解的方式來確定需要為哪些類創建實例。通俗的說,必須在運行時為編譯期還不能確定的類創建實例。再直白一點,必須提供一種new Object()之外的創建對象的方法。依賴注入存在類似的問題,容器必須能夠在運行時發現所有標注有@Autowired或@Resource的字段或方法,並且能夠在不知道對象的任何類型信息的情況下調用其setter方法完成依賴的注入(默認bean的字段都會實現setter方法)。總結一下IOC容器在實現時必須做到的三件看起來“不太可能的事”。
- 1.提供new之外的創建對象的方法,這個對象的類型在編譯期不能確定。
- 2.能夠在運行期知道類的結構信息,包括:方法,字段以及其上的注解信息等。
- 3.能夠在運行期對編譯期不能確定任何類型或接口信息的對象進行方法調用。
而這些,在java的反射技術下成為了可能。應該說反射技術並不僅僅在IOC容器中被使用,它是整個Spring框架的底層核心技術之一,是Spring實現通用型和擴展性的基石。
2. 反射技術初探
2.1 什么是反射技術
下面這段是官方的定義
Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.
總結下官方的定義,我們可以知道反射技術的核心就兩點:
- 1.反射使程序能夠在運行時探知類的結構信息:構造器,方法,字段等。
- 2.並且依賴這些結構信息完成相應的操作,比如創建對象,方法調用,字段賦值等。
2.2 類結構信息和java對象的映射
回顧下類加載的過程:當JVM需要使用某個類,但內存中不存在時,會將該類的字節碼文件加載進內存的方法區中,並在堆區創建一個Class對象。Class對象相當於存儲於方法區的字節碼信息的映射。我們可以通過該Class對象獲得關於該類的所有描述信息:類名,訪問權限,類注解,構造方法,字段,方法等等,盡管真實的類信息並不存在於該對象中。但通過它我們能獲得想要的東西,並進行相關的操作,某種程度可以認為它們邏輯上等價。對類的結構進一步細分,類主要由構造方法,方法,字段構成。所以也必須存在和它們建立邏輯關系的映射。java在反射包下定義了Constructor類,Method類和Field類來建立和構造方法,方法,字段的映射。Constructor對象映射構造器方法,Method對象映射靜態或實例方法,Field對象映射類的字段,而Class對象映射整個字節碼文件(從字節碼文件中抽取的方法區的運行時數據結構),通過Class對象又可以獲得Method,Constructor,Field對象。它們之間的關系如下圖所示。
通過這個圖像我們對反射可以建立更加直觀的認識。堆中的對象就像一面鏡子,反射出類全部或某一部分的面貌。通過這些對象,我們可以在運行時獲取類的全部信息;並且同樣通過這些對象,可以完成創建類的實例,方法調用,字段賦值等操作。
3 Class對象的獲取及需要注意的地方
我們知道Class對象是進行反射操作的入口,所以首先必須獲得Class對象。除了通過實例獲取外,Class對象主要由以下幾種方法獲得:
- 1.通過類加載器加載class文件
Class<?> clazz = Thread.currentThread().getContextClassLoader().
loadClass("com.takumiCX.reflect.ClassTest");
- 2.通過靜態方法Class.forName()獲取,需要傳入類的全限定名字符串作參數
Class<?> clazz = Class.forName("com.takumiCX.reflect.ClassTest");
- 3.通過類.class獲得類的Class對象
Class<ClassTest> clazz = ClassTest.class;
除了獲得的Class對象的泛型類型信息不一樣外,還有一個不同點值得注意。只有2在獲得class對象的同時會引起類的初始化,而1和3都不會。還記得獲得jdbc連接前注冊驅動的操作嗎?這就是完成驅動注冊的代碼
Class.forName("com.mysql.jdbc.Driver");
該方法引起了com.mysql.jdbc.Driver類被加載進內存,同時引起了類的初始化,而注冊驅動的邏輯就是在Driver類中的靜態代碼塊中完成的,
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
而通過類.class或classLoad.loadClass()雖然會引起類加載進內存,但不會引起類的初始化。通過下面的例子可以清楚的看到它們之間的區別:
/**
* @author: takumiCX
* @create: 2018-07-27
**/
public class ClassInitializeTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<InitialTest> clazz = InitialTest.class;
System.out.println("InitialTest.class:如果之前打印了初始化語句,說明該操作引起了類的初始化!");
Thread.currentThread().getContextClassLoader().
loadClass("com.takumiCX.reflect.InitialTest");
System.out.println("classLoader.loadClass:如果之前打印了初始化語句,說明該操作引起了類的初始化!");
Class.forName("com.takumiCX.reflect.InitialTest");
System.out.println("Class.forName:如果之前打印了初始化語句,說明該操作引起了類的初始化!");
}
}
class InitialTest{
static {
System.out.println("ClassTest 初始化!");
}
}
測試結果如下
4. 運行時反射獲取類的結構信息
Class類里的方法比較多,如要是圍繞如何獲得Method對象,Field對象,Constructor對象,Annotation對象的方法及其重載方法,當然也可以獲得類的父類,類實現的接口等信息。
- 代碼
/**
* @author: takumiCX
* @create: 2018-07-27
**/
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException {
Class<User> clazz = User.class;
//根據構造參數類型獲得指定的Constructor對象(包括非公有構造方法)
Constructor<User> constructor = clazz.getDeclaredConstructor(String.class);
System.out.println("獲得帶String參數的Constructor:"+constructor);
//獲得指定字段名的Field對象(包括非公有字段)
Field name = clazz.getDeclaredField("name");
System.out.println("獲得字段名為name的Field:"+name);
//根據方法名和方法參數類型獲得指定的Method對象(包括非公有方法)
Method method = clazz.getDeclaredMethod("setName", String.class);
System.out.println("獲得帶String類型參數且方法名為setName的Method:"+method);
//獲得類上指定的注解
MyAnnotation myAnnotation = clazz.getAnnotation(MyAnnotation.class);
System.out.println("獲得類上MyAnnotation類型的注解:"+myAnnotation);
//獲得類的所有實現接口
Class<?>[] interfaces = clazz.getInterfaces();
System.out.println("獲得類實現的所有接口:"+interfaces);
//獲得包對象
Package apackage = clazz.getPackage();
System.out.println("獲得類所在的包:"+apackage);
}
@MyAnnotation
public static class User implements Iuser{
private String name;
public User() {
}
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
static @interface MyAnnotation{
}
static interface Iuser {
}
}
- 測試結果
5. 運行時反射獲取泛型的真實類型
除了獲得類的常規信息外,類的參數類型(泛型)信息也可以通過反射在運行時獲得。泛型類不能用Class來表示,必須借助於反射包下關於類型概念的其他抽象結構。反射包下對類型這個復雜的概念進行了不同層次的抽象,我們有必要知道這種抽象的層次結構以及不同的抽象對應着什么樣的類型信息。
5.1 反射包下對類型概念的抽象層次結構
反射包下對類型這個概念進行了不同層級的抽象,它們之間的關系可以用下面這張圖表示
- Class:可以表示類,枚舉,注解,接口,數組等,但是其不能帶泛型參數。
- GenericArrayType:表示帶泛型參數的數組類型,如T[ ].
- ParameterizedType:表示帶泛型參數的類型,如List< String > ,java.lang.Comparable<? super T>.
- TypeVariable:表示類型變量,比如T,T entends Serializable
- WildcardType:表示通配符類型表達式,比如?,? extends Number等
- Type:關於類型概念的頂級抽象,可以用Type表示所有類型。
5.2 運行時獲取帶泛型的類,字段,方法參數,方法返回值的真實類型信息
- 代碼
/**
* @author: takumiCX
* @create: 2018-07-27
**/
abstract class GenericType<T> {
}
public class TestGenericType extends GenericType<String> {
private Map<String, Integer> map;
public Map<String,Integer> getMap(){
return map;
}
public void setMap(Map<String, Integer> map) {
this.map = map;
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
//獲取Class對象
Class<TestGenericType> clazz = TestGenericType.class;
System.out.println("獲取類的參數化類型信息:");
//1.獲取類的參數化類型信息
Type type = clazz.getGenericSuperclass();//獲取帶泛型的父類類型
if (type instanceof ParameterizedType) { //判斷是否參數化類型
Type[] types = ((ParameterizedType) type).getActualTypeArguments(); //獲得參數的實際類型
for (Type type1 : types) {
System.out.println(type1);
}
}
System.out.println("--------------------------");
System.out.println("獲取字段上的參數化類型信息:");
//獲取字段上的參數化類型信息
Field field = clazz.getDeclaredField("map");
Type type1 = field.getGenericType();
Type[] types = ((ParameterizedType) type1).getActualTypeArguments();
for(Type type2:types){
System.out.println(type2);
}
System.out.println("--------------------------");
System.out.println("獲取方法參數的參數化類型信息:");
//獲取方法參數的參數化類型信息
Method method = clazz.getDeclaredMethod("setMap",Map.class);
Type[] types1 = method.getGenericParameterTypes();
for(Type type2:types1){
if(type2 instanceof ParameterizedType){
Type[] typeArguments = ((ParameterizedType) type2).getActualTypeArguments();
for(Type type3:typeArguments){
System.out.println(type3);
}
}
}
System.out.println("--------------------------");
System.out.println("獲取方法返回值的參數化類型信息:");
//獲取方法返回值得參數化類型信息
Method method1 = clazz.getDeclaredMethod("getMap");
Type returnType = method1.getGenericReturnType();
if(returnType instanceof ParameterizedType){
Type[] arguments = ((ParameterizedType) returnType).getActualTypeArguments();
for(Type type2:arguments){
System.out.println(type2);
}
}
}
}
- 結果
5.3 運行時泛型父類獲取子類的真實類型信息
- 代碼
/**
* @author: takumiCX
* @create: 2018-07-27
**/
abstract class GenericType2<T> {
protected Class<T> tClass;
public GenericType2() {
Class<? extends GenericType2> aClass = this.getClass();
Type superclass = aClass.getGenericSuperclass();
if(superclass instanceof ParameterizedType){
Type[] typeArguments = ((ParameterizedType) superclass).getActualTypeArguments();
tClass=(Class<T>) typeArguments[0];
}
}
}
public class TestGenericType2 extends GenericType2<String>{
public static void main(String[] args) {
TestGenericType2 type2 = new TestGenericType2();
System.out.println(type2.tClass);
}
}
- 結果
5.4 泛型的類型信息不是編譯期間就擦除了嗎
java里的的泛型只在源碼階段存在,編譯的時候就會被擦除,聲明中的泛型類型信息會變成Object或泛型上界的類型,而使用時都用Object替換,如果要返回泛型類型,則通過強轉的方式完成。我們可以寫一個泛型類,將其編譯成字節碼文件后再反編譯看看發生了什么。
- 源碼
/**
* @author: takumiCX
* @create: 2018-07-27
**/
abstract class GenericType<T> {
}
public class TestGenericType extends GenericType<String> {
private Map<String, Integer> map;
public Map<String,Integer> getMap(){
return map;
}
public void setMap(Map<String, Integer> map) {
this.map = map;
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
}
}
- 反編譯后的源碼
package com.takumiCX.reflect;
import java.util.ArrayList;
import java.util.Map;
// Referenced classes of package com.takumiCX.reflect:
// GenericType
public class TestGenericType extends GenericType
{
public TestGenericType()
{
}
public Map getMap()
{
return map;
}
public void setMap(Map map)
{
this.map = map;
}
public static void main(String args[])
{
ArrayList list = new ArrayList();
}
private Map map;
}
反編譯后的源碼泛型信息全部消失了。說明編譯器在編譯源代碼的時候已經把泛型的類型信息擦除。理論上來說,源碼中指定的具體的泛型類型,在運行時是無法知道的。但是5.2和5.3的例子里我們確實通過反射在運行時得到了類,字段,方法參數以及方法返回值的泛型類型信息。那么問題出在哪里?關於這個問題我也是納悶了好久,在網上找了很多資料才得出比較靠譜的答案。泛型如果被用來進行聲明,比如說類上,字段上,方法參數和方法返回值上,這些屬於類的結構信息其實是會被編譯進Class文件中的;而泛型如果被用來使用,常見的方法體中帶泛型的局部變量,其類型信息不會被編譯進Class文件中。前者因為存在於Class文件中,所以運行時通過反射還是能夠獲得其類型信息的;而后者因為在Class文件中根本不存在,反射也就無能為力了。
6. 反射創建實例,方法調用,修改字段
- 代碼
/**
* @author: takumiCX
* @create: 2018-07-27
**/
public class ReflectOpration {
private String name;
public ReflectOpration(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "ReflectOpration{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class<ReflectOpration> clazz = ReflectOpration.class;
//獲取帶參構造器
Constructor<ReflectOpration> constructor = clazz.getConstructor(String.class);
//反射創建實例,傳入構造器參數takumiCX
ReflectOpration instance = constructor.newInstance("takumiCX");
System.out.println(instance);
//根據方法名獲取指定方法
Method getName = clazz.getMethod("getName");
//通過反射進行方法調用,傳入進行調用的對象作參數,后面可跟上方法參數
String res = (String) getName.invoke(instance);
System.out.println(res);
//獲取Field對象
Field field = clazz.getDeclaredField("name");
//修改訪問權限
field.setAccessible(true);
//反射修改字段,將名字改為全大寫
field.set(instance,"TAKUMICX");
System.out.println(instance);
}
}
- 運行結果
7. 反射的缺點
反射功能強大,使用它我們幾乎可以做到java語言層面支持的任何事情。但要注意到這種強大是有代價的。過多的使用反射可能會帶來嚴重的性能問題。曾今作支付平台的系統改造時就碰到過前人濫用反射留下的坑,因為模型對象在web,業務和持久層是不同,但其屬性基本一樣,所以原來的開發人員為了偷懶大量使用反射來進行這種對象屬性的拷貝操作。開發時間是節省了,但給系統性能帶來嚴重的負擔,支付接口的調用時間太長,甚至會超時。后來我們將其改回了手動調用setter賦值的方式,雖然工作量不少,但是最后上線的系統性能有了很大的提高,接口調用的響應時間比原來少了近30%。這個例子說明了對反射合理使用的重要性:框架中大量使用反射是因為要提供一套通用的處理流程來減少開發者的工作量,且大部分都在准備或者說容器啟動階段,反射的使用雖然增加了容器啟動時間,但因為提高了開發效率,所以是可以接受的;而在對性能有要求的業務代碼層面,使用反射會降低業務處理的速度,拖慢接口的響應時間,很多時候是不可接受的。反射一定要在權衡了開發效率和執行性能后,視場景和性能要求謹慎使用。