反射的思想及作用
有反必有正麻裁,就像世間的陰和陽(yáng),計(jì)算機(jī)的0和1一樣。天道有輪回煎源,蒼天...(凈會(huì)在這瞎bibi)
在學(xué)習(xí)反射之前色迂,先來(lái)了解正射是什么。我們平常用的最多的 new
方式實(shí)例化對(duì)象的方式就是一種正射的體現(xiàn)手销。假如我需要實(shí)例化一個(gè)HashMap
歇僧,代碼就會(huì)是這樣子。
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);
某一天發(fā)現(xiàn)锋拖,該段程序不適合用 HashMap 存儲(chǔ)鍵值對(duì)诈悍,更傾向于用LinkedHashMap
存儲(chǔ)。重新編寫(xiě)代碼后變成下面這個(gè)樣子兽埃。
Map<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
假如又有一天侥钳,發(fā)現(xiàn)數(shù)據(jù)還是適合用 HashMap來(lái)存儲(chǔ),難道又要重新修改源碼嗎讲仰?
發(fā)現(xiàn)問(wèn)題了嗎慕趴?我們每次改變一種需求,都要去重新修改源碼鄙陡,然后對(duì)代碼進(jìn)行編譯,打包躏啰,再到 JVM 上重啟項(xiàng)目趁矾。這么些步驟下來(lái),效率非常低给僵。
對(duì)于這種需求頻繁變更但變更不大的場(chǎng)景毫捣,頻繁地更改源碼肯定是一種不允許的操作,我們可以使用一個(gè)開(kāi)關(guān)
帝际,判斷什么時(shí)候使用哪一種數(shù)據(jù)結(jié)構(gòu)蔓同。
public Map<Integer, Integer> getMap(String param) {
Map<Integer, Integer> map = null;
if (param.equals("HashMap")) {
map = new HashMap<>();
} else if (param.equals("LinkedHashMap")) {
map = new LinkedHashMap<>();
} else if (param.equals("WeakHashMap")) {
map = new WeakHashMap<>();
}
return map;
}
通過(guò)傳入?yún)?shù)param
決定使用哪一種數(shù)據(jù)結(jié)構(gòu),可以在項(xiàng)目運(yùn)行時(shí)蹲诀,通過(guò)動(dòng)態(tài)傳入?yún)?shù)決定使用哪一個(gè)數(shù)據(jù)結(jié)構(gòu)斑粱。
如果某一天還想用TreeMap
,還是避免不了修改源碼脯爪,重新編譯執(zhí)行的弊端则北。這個(gè)時(shí)候,反射就派上用場(chǎng)了痕慢。
在代碼運(yùn)行之前尚揣,我們不確定將來(lái)會(huì)使用哪一種數(shù)據(jù)結(jié)構(gòu),只有在程序運(yùn)行時(shí)才決定使用哪一個(gè)數(shù)據(jù)類(lèi)掖举,而反射
可以在程序運(yùn)行過(guò)程中動(dòng)態(tài)獲取類(lèi)信息和調(diào)用類(lèi)方法快骗。通過(guò)反射構(gòu)造類(lèi)實(shí)例,代碼會(huì)演變成下面這樣。
public Map<Integer, Integer> getMap(String className) {
Class clazz = Class.forName(className);
Consructor con = clazz.getConstructor();
return (Map<Integer, Integer>) con.newInstance();
}
無(wú)論使用什么 Map方篮,只要實(shí)現(xiàn)了Map接口
名秀,就可以使用全類(lèi)名路徑
傳入到方法中,獲得對(duì)應(yīng)的 Map 實(shí)例恭取。例如java.util.HashMap / java.util.LinkedHashMap····如果要?jiǎng)?chuàng)建其它類(lèi)例如WeakHashMap
泰偿,我也不需要修改上面這段源碼。
我們來(lái)回顧一下如何從 new
一個(gè)對(duì)象引出使用反射
的蜈垮。
- 在不使用反射時(shí)耗跛,構(gòu)造對(duì)象使用 new 方式實(shí)現(xiàn),這種方式在編譯期就可以把對(duì)象的類(lèi)型確定下來(lái)攒发。
- 如果需求發(fā)生變更调塌,需要構(gòu)造另一個(gè)對(duì)象,則需要修改源碼惠猿,非常不優(yōu)雅羔砾,所以我們通過(guò)使用
開(kāi)關(guān)
,在程序運(yùn)行時(shí)判斷需要構(gòu)造哪一個(gè)對(duì)象偶妖,在運(yùn)行時(shí)可以變更開(kāi)關(guān)來(lái)實(shí)例化不同的數(shù)據(jù)結(jié)構(gòu)姜凄。 - 如果還有其它擴(kuò)展的類(lèi)有可能被使用,就會(huì)創(chuàng)建出非常多的分支趾访,且在編碼時(shí)不知道有什么其他的類(lèi)被使用到态秧,假如日后
Map
接口下多了一個(gè)集合類(lèi)是xxxHashMap
,還得創(chuàng)建分支扼鞋,此時(shí)引出了反射:可以在運(yùn)行時(shí)
才確定使用哪一個(gè)數(shù)據(jù)類(lèi)申鱼,在切換類(lèi)時(shí),無(wú)需重新修改源碼云头、編譯程序捐友。
第一章總結(jié):
- 反射的思想:在程序運(yùn)行過(guò)程中確定和解析數(shù)據(jù)類(lèi)的類(lèi)型。
- 反射的作用:對(duì)于在
編譯期
無(wú)法確定使用哪個(gè)數(shù)據(jù)類(lèi)的場(chǎng)景溃槐,通過(guò)反射
可以在程序運(yùn)行時(shí)構(gòu)造出不同的數(shù)據(jù)類(lèi)實(shí)例匣砖。
反射的基本使用
Java 反射的主要組成部分有4個(gè):
-
Class
:任何運(yùn)行在內(nèi)存中的所有類(lèi)都是該 Class 類(lèi)的實(shí)例對(duì)象,每個(gè) Class 類(lèi)對(duì)象內(nèi)部都包含了本來(lái)的所有信息竿痰。記著一句話脆粥,通過(guò)反射干任何事,先找 Class 準(zhǔn)沒(méi)錯(cuò)影涉! -
Field
:描述一個(gè)類(lèi)的屬性变隔,內(nèi)部包含了該屬性的所有信息,例如數(shù)據(jù)類(lèi)型蟹倾,屬性名匣缘,訪問(wèn)修飾符······ -
Constructor
:描述一個(gè)類(lèi)的構(gòu)造方法猖闪,內(nèi)部包含了構(gòu)造方法的所有信息,例如參數(shù)類(lèi)型肌厨,參數(shù)名字培慌,訪問(wèn)修飾符······ -
Method
:描述一個(gè)類(lèi)的所有方法(包括抽象方法),內(nèi)部包含了該方法的所有信息柑爸,與Constructor
類(lèi)似吵护,不同之處是 Method 擁有返回值類(lèi)型信息,因?yàn)闃?gòu)造方法是沒(méi)有返回值的表鳍。
我總結(jié)了一張腦圖馅而,放在了下面,如果用到了反射譬圣,離不開(kāi)這核心的4
個(gè)類(lèi)瓮恭,只有去了解它們內(nèi)部提供了哪些信息,有什么作用厘熟,運(yùn)用它們的時(shí)候才能易如反掌屯蹦。
我們?cè)趯W(xué)習(xí)反射的基本使用時(shí),我會(huì)用一個(gè)SmallPineapple
類(lèi)作為模板進(jìn)行說(shuō)明绳姨,首先我們先來(lái)熟悉這個(gè)類(lèi)的基本組成:屬性登澜,構(gòu)造函數(shù)和方法
public class SmallPineapple {
public String name;
public int age;
private double weight; // 體重只有自己知道
public SmallPineapple() {}
public SmallPineapple(String name, int age) {
this.name = name;
this.age = age;
}
public void getInfo() {
System.out.print("["+ name + " 的年齡是:" + age + "]");
}
}
反射中的用法有非常非常多,常見(jiàn)的功能有以下這幾個(gè):
- 在運(yùn)行時(shí)獲取一個(gè)類(lèi)的 Class 對(duì)象
- 在運(yùn)行時(shí)構(gòu)造一個(gè)類(lèi)的實(shí)例化對(duì)象
- 在運(yùn)行時(shí)獲取一個(gè)類(lèi)的所有信息:變量飘庄、方法帖渠、構(gòu)造器、注解
獲取類(lèi)的 Class 對(duì)象
在 Java 中竭宰,每一個(gè)類(lèi)都會(huì)有專(zhuān)屬于自己的 Class 對(duì)象,當(dāng)我們編寫(xiě)完.java
文件后份招,使用javac
編譯后切揭,就會(huì)產(chǎn)生一個(gè)字節(jié)碼文件.class
,在字節(jié)碼文件中包含類(lèi)的所有信息锁摔,如屬性
廓旬,構(gòu)造方法
,方法
······當(dāng)字節(jié)碼文件被裝載進(jìn)虛擬機(jī)執(zhí)行時(shí)谐腰,會(huì)在內(nèi)存中生成 Class 對(duì)象孕豹,它包含了該類(lèi)內(nèi)部的所有信息,在程序運(yùn)行時(shí)可以獲取這些信息十气。
獲取 Class 對(duì)象的方法有3
種:
-
類(lèi)名.class
:這種獲取方式只有在編譯
前已經(jīng)聲明了該類(lèi)的類(lèi)型才能獲取到 Class 對(duì)象
Class clazz = SmallPineapple.class;
-
實(shí)例.getClass()
:通過(guò)實(shí)例化對(duì)象獲取該實(shí)例的 Class 對(duì)象
SmallPineapple sp = new SmallPineapple();
Class clazz = sp.getClass();
-
Class.forName(className)
:通過(guò)類(lèi)的全限定名獲取該類(lèi)的 Class 對(duì)象
Class clazz = Class.forName("com.bean.smallpineapple");
拿到 Class
對(duì)象就可以對(duì)它為所欲為了:剝開(kāi)它的皮(獲取類(lèi)信息)励背、指揮它做事(調(diào)用它的方法),看透它的一切(獲取屬性)砸西,總之它就沒(méi)有隱私了叶眉。
不過(guò)在程序中址儒,每個(gè)類(lèi)的 Class 對(duì)象只有一個(gè),也就是說(shuō)你只有這一個(gè)奴隸
衅疙。我們用上面三種方式測(cè)試莲趣,通過(guò)三種方式打印各個(gè) Class
對(duì)象都是相同的。
Class clazz1 = Class.forName("com.bean.SmallPineapple");
Class clazz2 = SmallPineapple.class;
SmallPineapple instance = new SmallPineapple();
Class clazz3 = instance.getClass();
System.out.println("Class.forName() == SmallPineapple.class:" + (clazz1 == clazz2));
System.out.println("Class.forName() == instance.getClass():" + (clazz1 == clazz3));
System.out.println("instance.getClass() == SmallPineapple.class:" + (clazz2 == clazz3));
內(nèi)存中只有一個(gè) Class 對(duì)象的原因要牽扯到
JVM 類(lèi)加載機(jī)制
的雙親委派模型
饱溢,它保證了程序運(yùn)行時(shí)喧伞,加載類(lèi)
時(shí)每個(gè)類(lèi)在內(nèi)存中僅會(huì)產(chǎn)生一個(gè)Class對(duì)象
。在這里我不打算詳細(xì)展開(kāi)說(shuō)明绩郎,可以簡(jiǎn)單地理解為 JVM 幫我們保證了一個(gè)類(lèi)在內(nèi)存中至多存在一個(gè) Class 對(duì)象潘鲫。
構(gòu)造類(lèi)的實(shí)例化對(duì)象
通過(guò)反射構(gòu)造一個(gè)類(lèi)的實(shí)例方式有2
種:
- Class 對(duì)象調(diào)用
newInstance()
方法
Class clazz = Class.forName("com.bean.SmallPineapple");
SmallPineapple smallPineapple = (SmallPineapple) clazz.newInstance();
smallPineapple.getInfo();
// [null 的年齡是:0]
即使 SmallPineapple 已經(jīng)顯式定義了構(gòu)造方法,通過(guò) newInstance() 創(chuàng)建的實(shí)例中嗽上,所有屬性值都是對(duì)應(yīng)類(lèi)型的初始值
次舌,因?yàn)?newInstance() 構(gòu)造實(shí)例會(huì)調(diào)用默認(rèn)無(wú)參構(gòu)造器。
- Constructor 構(gòu)造器調(diào)用
newInstance()
方法
Class clazz = Class.forName("com.bean.SmallPineapple");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
SmallPineapple smallPineapple2 = (SmallPineapple) constructor.newInstance("小菠蘿", 21);
smallPineapple2.getInfo();
// [小菠蘿 的年齡是:21]
通過(guò) getConstructor(Object... paramTypes) 方法指定獲取指定參數(shù)類(lèi)型的 Constructor兽愤, Constructor 調(diào)用 newInstance(Object... paramValues) 時(shí)傳入構(gòu)造方法參數(shù)的值彼念,同樣可以構(gòu)造一個(gè)實(shí)例,且內(nèi)部屬性已經(jīng)被賦值浅萧。
通過(guò)Class
對(duì)象調(diào)用 newInstance() 會(huì)走默認(rèn)無(wú)參構(gòu)造方法逐沙,如果想通過(guò)顯式構(gòu)造方法構(gòu)造實(shí)例,需要提前從Class中調(diào)用getConstructor()
方法獲取對(duì)應(yīng)的構(gòu)造器洼畅,通過(guò)構(gòu)造器去實(shí)例化對(duì)象吩案。
這些 API 是在開(kāi)發(fā)當(dāng)中最常遇到的,當(dāng)然還有非常多重載的方法帝簇,本文由于篇幅原因徘郭,且如果每個(gè)方法都一一講解,我們也記不住丧肴,所以用到的時(shí)候去類(lèi)里面查找就已經(jīng)足夠了残揉。
獲取一個(gè)類(lèi)的所有信息
Class 對(duì)象中包含了該類(lèi)的所有信息,在編譯期我們能看到的信息就是該類(lèi)的變量芋浮、方法抱环、構(gòu)造器,在運(yùn)行時(shí)最常被獲取的也是這些信息纸巷。
獲取類(lèi)中的變量(Field)
- Field[] getFields():獲取類(lèi)中所有被
public
修飾的所有變量 - Field getField(String name):根據(jù)變量名獲取類(lèi)中的一個(gè)變量镇草,該變量必須被public修飾
- Field[] getDeclaredFields():獲取類(lèi)中所有的變量,但無(wú)法獲取繼承下來(lái)的變量
- Field getDeclaredField(String name):根據(jù)姓名獲取類(lèi)中的某個(gè)變量瘤旨,無(wú)法獲取繼承下來(lái)的變量
獲取類(lèi)中的方法(Method)
Method[] getMethods():獲取類(lèi)中被
public
修飾的所有方法Method getMethod(String name, Class...<?> paramTypes):根據(jù)名字和參數(shù)類(lèi)型獲取對(duì)應(yīng)方法梯啤,該方法必須被
public
修飾Method[] getDeclaredMethods():獲取
所有
方法,但無(wú)法獲取繼承下來(lái)的方法Method getDeclaredMethod(String name, Class...<?> paramTypes):根據(jù)名字和參數(shù)類(lèi)型獲取對(duì)應(yīng)方法裆站,無(wú)法獲取繼承下來(lái)的方法
獲取類(lèi)的構(gòu)造器(Constructor)
- Constuctor[] getConstructors():獲取類(lèi)中所有被
public
修飾的構(gòu)造器 - Constructor getConstructor(Class...<?> paramTypes):根據(jù)
參數(shù)類(lèi)型
獲取類(lèi)中某個(gè)構(gòu)造器条辟,該構(gòu)造器必須被public
修飾 - Constructor[] getDeclaredConstructors():獲取類(lèi)中所有構(gòu)造器
- Constructor getDeclaredConstructor(class...<?> paramTypes):根據(jù)
參數(shù)類(lèi)型
獲取對(duì)應(yīng)的構(gòu)造器
每種功能內(nèi)部以 Declared 細(xì)分為2
類(lèi):
有
Declared
修飾的方法:可以獲取該類(lèi)內(nèi)部包含的所有變量黔夭、方法和構(gòu)造器,但是無(wú)法獲取繼承下來(lái)的信息無(wú)
Declared
修飾的方法:可以獲取該類(lèi)中public
修飾的變量羽嫡、方法和構(gòu)造器本姥,可獲取繼承下來(lái)的信息
如果想獲取類(lèi)中所有的(包括繼承)變量、方法和構(gòu)造器杭棵,則需要同時(shí)調(diào)用getXXXs()
和getDeclaredXXXs()
兩個(gè)方法婚惫,用Set
集合存儲(chǔ)它們獲得的變量、構(gòu)造器和方法魂爪,以防兩個(gè)方法獲取到相同的東西先舷。
例如:要獲取SmallPineapple獲取類(lèi)中所有的變量,代碼應(yīng)該是下面這樣寫(xiě)滓侍。
Class clazz = Class.forName("com.bean.SmallPineapple");
// 獲取 public 屬性蒋川,包括繼承
Field[] fields1 = clazz.getFields();
// 獲取所有屬性,不包括繼承
Field[] fields2 = clazz.getDeclaredFields();
// 將所有屬性匯總到 set
Set<Field> allFields = new HashSet<>();
allFields.addAll(Arrays.asList(fields1));
allFields.addAll(Arrays.asList(fields2));
不知道你有沒(méi)有發(fā)現(xiàn)一件有趣的事情撩笆,如果父類(lèi)的屬性用
protected
修飾捺球,利用反射是無(wú)法獲取到的。protected 修飾符的作用范圍:只允許
同一個(gè)包下
或者子類(lèi)
訪問(wèn)夕冲,可以繼承到子類(lèi)氮兵。getFields() 只能獲取到本類(lèi)的
public
屬性的變量值;getDeclaredFields() 只能獲取到本類(lèi)的所有屬性歹鱼,不包括繼承的泣栈;無(wú)論如何都獲取不到父類(lèi)的 protected 屬性修飾的變量,但是它的的確確存在于子類(lèi)中弥姻。
獲取注解
獲取注解單獨(dú)擰了出來(lái)南片,因?yàn)樗⒉皇菍?zhuān)屬于 Class 對(duì)象的一種信息,每個(gè)變量庭敦,方法和構(gòu)造器都可以被注解修飾铃绒,所以在反射中,F(xiàn)ield螺捐,Constructor 和 Method 類(lèi)對(duì)象都可以調(diào)用下面這些方法獲取標(biāo)注在它們之上的注解。
- Annotation[] getAnnotations():獲取該對(duì)象上的所有注解
- Annotation getAnnotation(Class annotaionClass):傳入
注解類(lèi)型
矮燎,獲取該對(duì)象上的特定一個(gè)注解 - Annotation[] getDeclaredAnnotations():獲取該對(duì)象上的顯式標(biāo)注的所有注解定血,無(wú)法獲取
繼承
下來(lái)的注解 - Annotation getDeclaredAnnotation(Class annotationClass):根據(jù)
注解類(lèi)型
,獲取該對(duì)象上的特定一個(gè)注解诞外,無(wú)法獲取繼承
下來(lái)的注解
只有注解的@Retension
標(biāo)注為RUNTIME
時(shí)澜沟,才能夠通過(guò)反射獲取到該注解,@Retension 有3
種保存策略:
-
SOURCE
:只在源文件(.java)中保存峡谊,即該注解只會(huì)保留在源文件中茫虽,編譯時(shí)編譯器會(huì)忽略該注解刊苍,例如 @Override 注解 -
CLASS
:保存在字節(jié)碼文件(.class)中,注解會(huì)隨著編譯跟隨字節(jié)碼文件中濒析,但是運(yùn)行時(shí)不會(huì)對(duì)該注解進(jìn)行解析 -
RUNTIME
:一直保存到運(yùn)行時(shí)正什,用得最多的一種保存策略,在運(yùn)行時(shí)可以獲取到該注解的所有信息
像下面這個(gè)例子号杏,SmallPineapple 類(lèi)繼承了抽象類(lèi)Pineapple
婴氮,getInfo()
方法上標(biāo)識(shí)有 @Override 注解,且在子類(lèi)中標(biāo)注了@Transient
注解盾致,在運(yùn)行時(shí)獲取子類(lèi)重寫(xiě)方法上的所有注解主经,只能獲取到@Transient
的信息。
public abstract class Pineapple {
public abstract void getInfo();
}
public class SmallPineapple extends Pineapple {
@Transient
@Override
public void getInfo() {
System.out.print("小菠蘿的身高和年齡是:" + height + "cm ; " + age + "歲");
}
}
啟動(dòng)類(lèi)Bootstrap
獲取 SmallPineapple 類(lèi)中的 getInfo() 方法上的注解信息:
public class Bootstrap {
/**
* 根據(jù)運(yùn)行時(shí)傳入的全類(lèi)名路徑判斷具體的類(lèi)對(duì)象
* @param path 類(lèi)的全類(lèi)名路徑
*/
public static void execute(String path) throws Exception {
Class obj = Class.forName(path);
Method method = obj.getMethod("getInfo");
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
public static void main(String[] args) throws Exception {
execute("com.pineapple.SmallPineapple");
}
}
// @java.beans.Transient(value=true)
通過(guò)反射調(diào)用方法
通過(guò)反射獲取到某個(gè) Method 類(lèi)對(duì)象后庭惜,可以通過(guò)調(diào)用invoke
方法執(zhí)行罩驻。
-
invoke(Oject obj, Object... args)
:參數(shù)``1指定調(diào)用該方法的**對(duì)象**,參數(shù)
2`是方法的參數(shù)列表值护赊。
如果調(diào)用的方法是靜態(tài)方法胳赌,參數(shù)1只需要傳入null
,因?yàn)殪o態(tài)方法不與某個(gè)對(duì)象有關(guān)瑟蜈,只與某個(gè)類(lèi)有關(guān)奕剃。
可以像下面這種做法,通過(guò)反射實(shí)例化一個(gè)對(duì)象器一,然后獲取Method
方法對(duì)象课锌,調(diào)用invoke()
指定SmallPineapple
的getInfo()
方法。
Class clazz = Class.forName("com.bean.SmallPineapple");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
SmallPineapple sp = (SmallPineapple) constructor.newInstance("小菠蘿", 21);
Method method = clazz.getMethod("getInfo");
if (method != null) {
method.invoke(sp, null);
}
// [小菠蘿的年齡是:21]
反射的應(yīng)用場(chǎng)景
反射常見(jiàn)的應(yīng)用場(chǎng)景這里介紹3
個(gè):
- Spring 實(shí)例化對(duì)象:當(dāng)程序啟動(dòng)時(shí)祈秕,Spring 會(huì)讀取配置文件
applicationContext.xml
并解析出里面所有的 標(biāo)簽實(shí)例化到IOC
容器中渺贤。 - 反射 + 工廠模式:通過(guò)
反射
消除工廠中的多個(gè)分支,如果需要生產(chǎn)新的類(lèi)请毛,無(wú)需關(guān)注工廠類(lèi)志鞍,工廠類(lèi)可以應(yīng)對(duì)各種新增的類(lèi),反射
可以使得程序更加健壯方仿。 - JDBC連接數(shù)據(jù)庫(kù):使用JDBC連接數(shù)據(jù)庫(kù)時(shí)固棚,指定連接數(shù)據(jù)庫(kù)的
驅(qū)動(dòng)類(lèi)
時(shí)用到反射加載驅(qū)動(dòng)類(lèi)
Spring 的 IOC 容器
在 Spring 中,經(jīng)常會(huì)編寫(xiě)一個(gè)上下文配置文件applicationContext.xml
仙蚜,里面就是關(guān)于bean
的配置此洲,程序啟動(dòng)時(shí)會(huì)讀取該 xml 文件,解析出所有的 <bean>
標(biāo)簽委粉,并實(shí)例化對(duì)象放入IOC
容器中呜师。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="smallpineapple" class="com.bean.SmallPineapple">
<constructor-arg type="java.lang.String" value="小菠蘿"/>
<constructor-arg type="int" value="21"/>
</bean>
</beans>
在定義好上面的文件后,通過(guò)ClassPathXmlApplicationContext
加載該配置文件贾节,程序啟動(dòng)時(shí)汁汗,Spring 會(huì)將該配置文件中的所有bean
都實(shí)例化衷畦,放入 IOC 容器中,IOC 容器本質(zhì)上就是一個(gè)工廠知牌,通過(guò)該工廠傳入 \ 標(biāo)簽的id
屬性獲取到對(duì)應(yīng)的實(shí)例祈争。
public class Main {
public static void main(String[] args) {
ApplicationContext ac =
new ClassPathXmlApplicationContext("applicationContext.xml");
SmallPineapple smallPineapple = (SmallPineapple) ac.getBean("smallpineapple");
smallPineapple.getInfo(); // [小菠蘿的年齡是:21]
}
}
Spring 在實(shí)例化對(duì)象的過(guò)程經(jīng)過(guò)簡(jiǎn)化之后,可以理解為反射實(shí)例化對(duì)象的步驟:
- 獲取Class對(duì)象的構(gòu)造器
- 通過(guò)構(gòu)造器調(diào)用 newInstance() 實(shí)例化對(duì)象
當(dāng)然 Spring 在實(shí)例化對(duì)象時(shí)送爸,做了非常多額外的操作铛嘱,才能夠讓現(xiàn)在的開(kāi)發(fā)足夠的便捷且穩(wěn)定。
在之后的文章中會(huì)專(zhuān)門(mén)寫(xiě)一篇文章講解如何利用反射實(shí)現(xiàn)一個(gè)
簡(jiǎn)易版
的IOC
容器袭厂,IOC容器原理很簡(jiǎn)單墨吓,只要掌握了反射的思想,了解反射的常用 API 就可以實(shí)現(xiàn)纹磺,我可以提供一個(gè)簡(jiǎn)單的思路:利用 HashMap 存儲(chǔ)所有實(shí)例帖烘,key 代表 \ 標(biāo)簽的id
,value 存儲(chǔ)對(duì)應(yīng)的實(shí)例橄杨,這對(duì)應(yīng)了 Spring IOC容器管理的對(duì)象默認(rèn)是單例的秘症。
反射 + 抽象工廠模式
傳統(tǒng)的工廠模式,如果需要生產(chǎn)新的子類(lèi)式矫,需要修改工廠類(lèi)乡摹,在工廠類(lèi)中增加新的分支;
public class MapFactory {
public Map<Object, object> produceMap(String name) {
if ("HashMap".equals(name)) {
return new HashMap<>();
} else if ("TreeMap".equals(name)) {
return new TreeMap<>();
} // ···
}
}
利用反射和工廠模式相結(jié)合采转,在產(chǎn)生新的子類(lèi)時(shí)聪廉,工廠類(lèi)不用修改任何東西,可以專(zhuān)注于子類(lèi)的實(shí)現(xiàn)故慈,當(dāng)子類(lèi)確定下來(lái)時(shí)板熊,工廠也就可以生產(chǎn)該子類(lèi)了。
反射 + 抽象工廠的核心思想是:
- 在運(yùn)行時(shí)通過(guò)參數(shù)傳入不同子類(lèi)的全限定名獲取到不同的 Class 對(duì)象察绷,調(diào)用 newInstance() 方法返回不同的子類(lèi)干签。細(xì)心的讀者會(huì)發(fā)現(xiàn)提到了子類(lèi)這個(gè)概念,所以反射 + 抽象工廠模式拆撼,一般會(huì)用于有繼承或者接口實(shí)現(xiàn)關(guān)系容劳。
例如,在運(yùn)行時(shí)才確定使用哪一種 Map
結(jié)構(gòu)闸度,我們可以利用反射傳入某個(gè)具體 Map 的全限定名鸭蛙,實(shí)例化一個(gè)特定的子類(lèi)。
public class MapFactory {
/**
* @param className 類(lèi)的全限定名
*/
public Map<Object, Object> produceMap(String className) {
Class clazz = Class.forName(className);
Map<Object, Object> map = clazz.newInstance();
return map;
}
}
className
可以指定為 java.util.HashMap筋岛,或者 java.util.TreeMap 等等,根據(jù)業(yè)務(wù)場(chǎng)景來(lái)定晒哄。
JDBC 加載數(shù)據(jù)庫(kù)驅(qū)動(dòng)類(lèi)
在導(dǎo)入第三方庫(kù)時(shí)睁宰,JVM不會(huì)主動(dòng)去加載外部導(dǎo)入的類(lèi)肪获,而是等到真正使用時(shí),才去加載需要的類(lèi)柒傻,正是如此孝赫,我們可以在獲取數(shù)據(jù)庫(kù)連接時(shí)傳入驅(qū)動(dòng)類(lèi)的全限定名,交給 JVM 加載該類(lèi)红符。
public class DBConnectionUtil {
/** 指定數(shù)據(jù)庫(kù)的驅(qū)動(dòng)類(lèi) */
private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";
public static Connection getConnection() {
Connection conn = null;
// 加載驅(qū)動(dòng)類(lèi)
Class.forName(DRIVER_CLASS_NAME);
// 獲取數(shù)據(jù)庫(kù)連接對(duì)象
conn = DriverManager.getConnection("jdbc:mysql://···", "root", "root");
return conn;
}
}
在我們開(kāi)發(fā) SpringBoot 項(xiàng)目時(shí)青柄,會(huì)經(jīng)常遇到這個(gè)類(lèi),但是可能習(xí)慣成自然了预侯,就沒(méi)多大在乎致开,我在這里給你們看看常見(jiàn)的application.yml
中的數(shù)據(jù)庫(kù)配置,我想你應(yīng)該會(huì)恍然大悟吧萎馅。
這里的 driver-class-name双戳,和我們一開(kāi)始加載的類(lèi)是不是覺(jué)得很相似,這是因?yàn)?strong>MySQL版本不同引起的驅(qū)動(dòng)類(lèi)不同糜芳,這體現(xiàn)使用反射的好處:不需要修改源碼飒货,僅加載配置文件就可以完成驅(qū)動(dòng)類(lèi)的替換。
在之后的文章中會(huì)專(zhuān)門(mén)寫(xiě)一篇文章詳細(xì)地介紹反射的應(yīng)用場(chǎng)景峭竣,實(shí)現(xiàn)簡(jiǎn)單的
IOC
容器以及通過(guò)反射實(shí)現(xiàn)工廠模式的好處塘辅。在這里,你只需要掌握反射的基本用法和它的思想皆撩,了解它的主要使用場(chǎng)景扣墩。
反射的優(yōu)勢(shì)及缺陷
反射的優(yōu)點(diǎn):
- 增加程序的靈活性:面對(duì)需求變更時(shí),可以靈活地實(shí)例化不同對(duì)象
但是毅访,有得必有失沮榜,一項(xiàng)技術(shù)不可能只有優(yōu)點(diǎn)沒(méi)有缺點(diǎn),反射也有兩個(gè)比較隱晦的缺點(diǎn):
- 破壞類(lèi)的封裝性:可以強(qiáng)制訪問(wèn) private 修飾的信息
- 性能損耗:反射相比直接實(shí)例化對(duì)象喻粹、調(diào)用方法蟆融、訪問(wèn)變量,中間需要非常多的檢查步驟和解析步驟守呜,JVM無(wú)法對(duì)它們優(yōu)化型酥。
增加程序的靈活性
這里不再用 SmallPineapple 舉例了,我們來(lái)看一個(gè)更加貼近開(kāi)發(fā)
的例子:
- 利用反射連接數(shù)據(jù)庫(kù)查乒,涉及到數(shù)據(jù)庫(kù)的數(shù)據(jù)源弥喉。在 SpringBoot 中一切約定大于配置,想要定制配置時(shí)玛迄,使用
application.properties
配置文件指定數(shù)據(jù)源
角色1 - Java的設(shè)計(jì)者:我們?cè)O(shè)計(jì)好DataSource
接口由境,你們其它數(shù)據(jù)庫(kù)廠商想要開(kāi)發(fā)者用你們的數(shù)據(jù)源
監(jiān)控?cái)?shù)據(jù)庫(kù),就得實(shí)現(xiàn)我的這個(gè)接口
!
角色2 - 數(shù)據(jù)庫(kù)廠商:
- MySQL 數(shù)據(jù)庫(kù)廠商:我們提供了 com.mysql.cj.jdbc.MysqlDataSource 數(shù)據(jù)源虏杰,開(kāi)發(fā)者可以使用它連接 MySQL讥蟆。
- 阿里巴巴廠商:我們提供了 com.alibaba.druid.pool.DruidDataSource 數(shù)據(jù)源,我這個(gè)數(shù)據(jù)源更牛逼纺阔,具有頁(yè)面監(jiān)控瘸彤,慢SQL日志記錄等功能,開(kāi)發(fā)者快來(lái)用它監(jiān)控 MySQL吧笛钝!
- SQLServer 廠商:我們提供了 com.microsoft.sqlserver.jdbc.SQLServerDataSource 數(shù)據(jù)源质况,如果你想實(shí)用SQL Server 作為數(shù)據(jù)庫(kù),那就使用我們的這個(gè)數(shù)據(jù)源連接吧
角色3 - 開(kāi)發(fā)者:我們可以用配置文件
指定使用DruidDataSource
數(shù)據(jù)源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
需求變更:某一天玻靡,老板來(lái)跟我們說(shuō)结榄,Druid 數(shù)據(jù)源不太符合我們現(xiàn)在的項(xiàng)目了,我們使用 MysqlDataSource 吧啃奴,然后程序猿就會(huì)修改配置文件潭陪,重新加載配置文件,并重啟項(xiàng)目最蕾,完成數(shù)據(jù)源的切換依溯。
spring.datasource.type=com.mysql.cj.jdbc.MysqlDataSource
在改變連接數(shù)據(jù)庫(kù)的數(shù)據(jù)源時(shí),只需要改變配置文件即可瘟则,無(wú)需改變?nèi)魏未a黎炉,原因是:
- Spring Boot 底層封裝好了連接數(shù)據(jù)庫(kù)的數(shù)據(jù)源配置,利用反射醋拧,適配各個(gè)數(shù)據(jù)源慷嗜。
下面來(lái)簡(jiǎn)略的進(jìn)行源碼分析。我們用ctrl+左鍵
點(diǎn)擊spring.datasource.type
進(jìn)入 DataSourceProperties 類(lèi)中丹壕,發(fā)現(xiàn)使用setType() 將全類(lèi)名轉(zhuǎn)化為 Class 對(duì)象注入到type
成員變量當(dāng)中庆械。在連接并監(jiān)控?cái)?shù)據(jù)庫(kù)時(shí),就會(huì)使用指定的數(shù)據(jù)源操作菌赖。
private Class<? extends DataSource> type;
public void setType(Class<? extends DataSource> type) {
this.type = type;
}
Class
對(duì)象指定了泛型上界DataSource
缭乘,我們?nèi)タ匆幌赂鞔髷?shù)據(jù)源的類(lèi)圖結(jié)構(gòu)
。
上圖展示了一部分?jǐn)?shù)據(jù)源琉用,當(dāng)然不止這些堕绩,但是我們可以看到,無(wú)論指定使用哪一種數(shù)據(jù)源邑时,我們都只需要與配置文件打交道奴紧,而無(wú)需更改源碼,這就是反射的靈活性晶丘!
破壞類(lèi)的封裝性
很明顯的一個(gè)特點(diǎn)黍氮,反射可以獲取類(lèi)中被private
修飾的變量、方法和構(gòu)造器,這違反了面向?qū)ο蟮姆庋b特性沫浆,因?yàn)楸?private 修飾意味著不想對(duì)外暴露觉壶,只允許本類(lèi)訪問(wèn),而setAccessable(true)
可以無(wú)視訪問(wèn)修飾符的限制件缸,外界可以強(qiáng)制訪問(wèn)。
還記得單例模式
一文嗎叔遂?里面講到反射破壞餓漢式和懶漢式單例模式他炊,所以之后用了枚舉
避免被反射KO。
回到最初的起點(diǎn)已艰,SmallPineapple 里有一個(gè) weight 屬性被 private 修飾符修飾痊末,目的在于自己的體重并不想給外界知道。
public class SmallPineapple {
public String name;
public int age;
private double weight; // 體重只有自己知道
public SmallPineapple(String name, int age, double weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
雖然 weight 屬性理論上只有自己知道哩掺,但是如果經(jīng)過(guò)反射凿叠,這個(gè)類(lèi)就像在裸奔一樣,在反射面前變得一覽無(wú)遺
嚼吞。
SmallPineapple sp = new SmallPineapple("小菠蘿", 21, "54.5");
Clazz clazz = Class.forName(sp.getClass());
Field weight = clazz.getDeclaredField("weight");
weight.setAccessable(true);
System.out.println("窺覷到小菠蘿的體重是:" + weight.get(sp));
// 窺覷到小菠蘿的體重是:54.5 kg
性能損耗
在直接 new 對(duì)象并調(diào)用對(duì)象方法和訪問(wèn)屬性時(shí)盒件,編譯器會(huì)在編譯期提前檢查可訪問(wèn)性,如果嘗試進(jìn)行不正確的訪問(wèn)舱禽,IDE會(huì)提前提示錯(cuò)誤炒刁,例如參數(shù)傳遞類(lèi)型不匹配,非法訪問(wèn) private 屬性和方法誊稚。
而在利用反射操作對(duì)象時(shí)翔始,編譯器無(wú)法提前得知對(duì)象的類(lèi)型,訪問(wèn)是否合法里伯,參數(shù)傳遞類(lèi)型是否匹配城瞎。只有在程序運(yùn)行時(shí)調(diào)用反射的代碼時(shí)才會(huì)從頭開(kāi)始檢查、調(diào)用疾瓮、返回結(jié)果脖镀,JVM也無(wú)法對(duì)反射的代碼進(jìn)行優(yōu)化。
雖然反射具有性能損耗的特點(diǎn)爷贫,但是我們不能一概而論认然,產(chǎn)生了使用反射就會(huì)性能下降的思想,反射的慢漫萄,需要同時(shí)調(diào)用上100W
次才可能體現(xiàn)出來(lái)卷员,在幾次、幾十次的調(diào)用腾务,并不能體現(xiàn)反射的性能低下毕骡。所以不要一味地戴有色眼鏡看反射,在單次調(diào)用反射的過(guò)程中,性能損耗可以忽略不計(jì)未巫。如果程序的性能要求很高窿撬,那么盡量不要使用反射。
反射基礎(chǔ)篇文末總結(jié)
- 反射的思想:反射就像是一面鏡子一樣叙凡,在運(yùn)行時(shí)才看到自己是誰(shuí)劈伴,可獲取到自己的信息,甚至實(shí)例化對(duì)象握爷。
- 反射的作用:在運(yùn)行時(shí)才確定實(shí)例化對(duì)象跛璧,使程序更加健壯,面對(duì)需求變更時(shí)新啼,可以最大程度地做到不修改程序源碼應(yīng)對(duì)不同的場(chǎng)景追城,實(shí)例化不同類(lèi)型的對(duì)象。
- 反射的應(yīng)用場(chǎng)景常見(jiàn)的有
3
個(gè):Spring的 IOC 容器燥撞,反射+工廠模式 使工廠類(lèi)更穩(wěn)定座柱,JDBC連接數(shù)據(jù)庫(kù)時(shí)加載驅(qū)動(dòng)類(lèi) - 反射的
3
個(gè)特點(diǎn):增加程序的靈活性、破壞類(lèi)的封裝性以及性能損耗
作者:程序員cxuan
鏈接:https://juejin.im/post/6864324335654404104
來(lái)源:掘金