Shell 流程控制
956
2025-04-01
古時(shí)的風(fēng)箏原創(chuàng)文章
第一次在程序的世界中聽(tīng)到反射這個(gè)概念,我有些疑惑,不知道它和光的反射有什么異曲同工之處。后來(lái),等我真正了解它的時(shí)候,才發(fā)現(xiàn),好像并沒(méi)有什么關(guān)系。可能就是翻譯的有問(wèn)題而已。
那么問(wèn)題來(lái)了,你了解反射到底是個(gè)什么嗎,靈魂三問(wèn)。
1、反射的作用,為什么要用反射?
2、反射在常用框架中的應(yīng)用,Spring 中哪些地方使用了反射你知道嗎?
3、反射為什么性能比較差?
遙想當(dāng)年,我初識(shí)反射
遙想剛畢業(yè)那年,水到不行,第一次真正見(jiàn)識(shí)反射還是在某個(gè)項(xiàng)目上。當(dāng)時(shí)我還在做 .NET ,有一個(gè)為某國(guó)企開(kāi)發(fā) Portal 系統(tǒng)的項(xiàng)目,其中有個(gè)「待辦任務(wù)」模塊,任務(wù)來(lái)自其他幾個(gè)系統(tǒng),話(huà)說(shuō)國(guó)企就是國(guó)企,系統(tǒng)真是多,這些待辦任務(wù)來(lái)自 5 個(gè)不同的系統(tǒng),據(jù)說(shuō)這還不是全部。我表示無(wú)話(huà)可說(shuō)。
當(dāng)時(shí)也根本不用消息隊(duì)列,自然就想到兩種方案,要么我們做接口定時(shí)去另外 5 個(gè)系統(tǒng)拉數(shù)據(jù),要么那 5 個(gè)系統(tǒng)一產(chǎn)生待辦就直接推給我們。定時(shí)去其他系統(tǒng)拉數(shù)據(jù)會(huì)有一個(gè)延時(shí)的問(wèn)題,而且據(jù)說(shuō)有兩個(gè)系統(tǒng)用的服務(wù)器很古老,配置很低,別談什么并發(fā)了,請(qǐng)求頻繁點(diǎn)兒都不行,沒(méi)想到國(guó)企也不是很有錢(qián)(呵呵)。所以最終決定我們寫(xiě)接口,其他 5 個(gè)系統(tǒng)實(shí)時(shí)請(qǐng)求我們的接口推過(guò)來(lái)數(shù)據(jù)。
然后我們就開(kāi)始動(dòng)手寫(xiě)接口文檔,提供了接口地址、請(qǐng)求參數(shù)、數(shù)據(jù)格式等等,經(jīng)過(guò)一場(chǎng)友好的會(huì)議討論后,有 3 系統(tǒng)接口人表示數(shù)據(jù)格式不能按照我們的來(lái),說(shuō)是他們的待辦實(shí)體已經(jīng)確定了,改動(dòng)太大,只能按照他們內(nèi)部的格式轉(zhuǎn)換成字符串傳過(guò)來(lái)。好吧,誰(shuí)讓人家是內(nèi)部人開(kāi)發(fā)的呢,字符串就字符串吧。
說(shuō)了這么多跟反射有個(gè)啥子關(guān)系,來(lái)了,重點(diǎn)來(lái)了。
當(dāng)時(shí),我作為一個(gè)菜鳥(niǎo),當(dāng)時(shí)我一下子想到兩種方案。
第一種:為 5 個(gè)系統(tǒng)各開(kāi)一個(gè)接口,對(duì)應(yīng)的自然就可以用不同的邏輯解析主體信息了。
但是我有覺(jué)得,這種寫(xiě)法雖然清晰,但會(huì)不會(huì)太傻了一些,于是,我想到了第二種方式,根據(jù)傳過(guò)來(lái)的系統(tǒng)來(lái)源參數(shù)(有一個(gè)參數(shù)表示來(lái)自那個(gè)系統(tǒng),用一個(gè)字符串表示)判斷,幾個(gè) if 區(qū)分,當(dāng)時(shí)我甚至想到了如果方法過(guò)長(zhǎng),要單獨(dú)提取出去變成幾個(gè)私有方法,以便可以?xún)?nèi)聯(lián)(心想,我竟然如此牛X)。
于是我把這個(gè)想法愉快的告訴了我的組長(zhǎng),聽(tīng)罷,他默默點(diǎn)上了一支煙,徑自走到了窗前,剛抽了兩口反應(yīng)過(guò)來(lái)不能抽煙,趕緊掐滅,又走了回來(lái),從始至終,一言未發(fā)。我只好回到座位,一定是組長(zhǎng)驚嘆于我剛剛畢業(yè),竟有如此才華,我不禁心里暗暗得意。
半個(gè)小時(shí)之后,組長(zhǎng)發(fā)過(guò)來(lái)消息:
先回家!當(dāng)然,我還是先回家了。第二天到公司,第一件事兒,獲取最新代碼,多了個(gè) interface、一個(gè)配置文件和 5 個(gè)普通類(lèi)文件,一臉懵的我選擇先看看那個(gè)說(shuō)明文檔。
怎么可能,竟然沒(méi)用我昨天說(shuō)的方案。等等,這是什么方法,能行嗎?大致思路是這樣的:
首先 5 個(gè)普通類(lèi)都實(shí)現(xiàn)自那個(gè) interface,里面都只有一個(gè)方法,用來(lái)處理請(qǐng)求主體的。然后讀配置文件,配置文件就是系統(tǒng)來(lái)源那個(gè)參數(shù)作為 key,另外那 5 個(gè)普通類(lèi)的完全類(lèi)名作為 value。然后用了 reflect 庫(kù)下什么方法加載了那 5 個(gè)類(lèi),然后再調(diào)用里面的方法。
那時(shí)才知道,有一種方式叫做反射,竟然比 if 大法還好用。
什么是反射
反射這一概念最早由編程開(kāi)發(fā)人員Smith在1982年提出,主要指應(yīng)用程序訪問(wèn)、檢測(cè)、修改自身狀態(tài)與行為的能力。幾乎所有的面向?qū)ο蟮拈_(kāi)發(fā)語(yǔ)言都提供了反射機(jī)制。
Java 中的反射是指在程序運(yùn)行時(shí)動(dòng)態(tài)獲取和操作當(dāng)前程序中類(lèi)型,比如獲取類(lèi)(class)的名稱(chēng)、實(shí)例化一個(gè)類(lèi)實(shí)體、操作屬性、調(diào)用方法等。
Java 是編譯型語(yǔ)言,絕大多數(shù)對(duì)象在編譯期就確定了類(lèi)型。而反射為 Java 提供了動(dòng)態(tài)編譯的實(shí)現(xiàn)方式,也就是在 JVM 已經(jīng)運(yùn)行的情況下動(dòng)態(tài)的加載并操作類(lèi)型。
比如下面這個(gè)初始化語(yǔ)句,在編譯之后就已經(jīng)確定了 user 對(duì)象為 User 類(lèi)型,在 JVM 啟動(dòng)之后就被加載到 JVM 中了。
kite.lab.reflect.User user = new kite.lab.reflect.User();
而下面這個(gè)利用反射的操作,在編譯和 JVM 啟動(dòng)的時(shí)候并沒(méi)有確定類(lèi)型,而是當(dāng)程序執(zhí)行到這兩行代碼的時(shí)候才加載 kite.lab.reflect.User 類(lèi),并在調(diào)用 newInstance() 方法時(shí)才實(shí)例化 User 對(duì)象。
Class clazz = Class.forName("kite.lab.reflect.User"); Object userInstance = clazz.newInstance();
反射的基本用法
反射雖然聽(tīng)上去高深,但用起來(lái)還是很簡(jiǎn)單的,它就是 JDK 提供給我們的一套簡(jiǎn)單易用的 API,在 java.lang.reflect這個(gè) package 下,再加上一個(gè) java.lang.Class。
java.lang.Class提供了一系列操作類(lèi)型的方法,常用的就是獲取類(lèi)的全名、獲取屬性集合、根據(jù)名稱(chēng)獲取屬性、獲取方法集合、根據(jù)方法名稱(chēng)獲取方法、實(shí)例化一個(gè)對(duì)象、獲取屬性值、修改屬性值、調(diào)用方法等。
下面是一個(gè)簡(jiǎn)單的例子,演示了反射的基本用法。
public class User { static String country; private String name; public int age; private Result
result; public void say(String world){ System.out.println("我說(shuō):" + world); } private void writeNote(){ System.out.println("寫(xiě)日記"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Result
getResult() { return result; } public void setResult(Result
result) { this.result = result; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class ReflectTest { public static void main(String[] args) throws Exception{ reflectDemo(); } public static void reflectDemo() throws Exception{ Class clazz = Class.forName("kite.lab.reflect.User"); Field[] declaredFields = clazz.getDeclaredFields(); for(Field declaredField:declaredFields){ System.out.println(declaredField.getName()); } Method[] methods = clazz.getDeclaredMethods(); for(Method method:methods){ System.out.println(method.getName()); } Object userInstance = clazz.newInstance(); Method sayMethod = clazz.getDeclaredMethod("say", String.class); sayMethod.invoke(userInstance,"你好"); Method writeNoteMethod = clazz.getDeclaredMethod("writeNote"); writeNoteMethod.setAccessible(true); writeNoteMethod.invoke(userInstance); } }
上面例子演示了獲取 User 類(lèi)的所有聲明的屬性和方法,并調(diào)用了 public 的 say() 方法和 private 的 writeNote() 方法。
反射的使用場(chǎng)景
在能確定類(lèi)型的情況下能不用反射就不用反射,因?yàn)榉瓷涞男阅鼙戎苯诱{(diào)用的性能略差。大多數(shù)在無(wú)法事先確定類(lèi)型的時(shí)候才會(huì)用到反射。
一般在設(shè)計(jì)通用型框架的時(shí)候會(huì)用到反射,所謂通用型框架,指的是框架搭好了,你可以拿去用,但是里面有很多的細(xì)節(jié)需要結(jié)合你的具體需求來(lái)實(shí)現(xiàn)。
舉個(gè)例子,其實(shí)和前面初識(shí)反射的經(jīng)歷中所用到的方案是同一個(gè)意思。比方說(shuō)我要實(shí)現(xiàn)一個(gè)日志采集分析框架,框架要實(shí)現(xiàn)的就是收集日志,然后分析出警告信息、異常信息的條數(shù)、占比以及對(duì)高級(jí)別異常做特殊標(biāo)記等。
那如果我這個(gè)框架只是給使用了 SLF4J 的 Java 項(xiàng)目使用就簡(jiǎn)單了,可現(xiàn)在要做的是不限制日志來(lái)自哪兒,可以是 SLF4J,也可以是其他日志框架,甚至可以來(lái)自 Nginx、Redis 或者你自己定義的日志格式。 假設(shè)框架有諸多細(xì)節(jié),包括數(shù)據(jù)怎么流轉(zhuǎn)、如何分析等,這些都不提,僅僅說(shuō)數(shù)據(jù)收集這塊,這塊兒是整個(gè)框架中存在不確定性的地方,因?yàn)椴恢滥愕娜罩緛?lái)源是哪里。
基于以上原因,框架規(guī)定好了最后需要的日志格式,比如"類(lèi)型(exception|warn|info):日志內(nèi)容:時(shí)間戳"這種格式。框架給你開(kāi)放一個(gè)接口出來(lái),你實(shí)現(xiàn)接口,按照這種格式返回就好了。
public interface ILogHandler { String collect(); }
然后在配置文件中配置上你自定義的接口實(shí)現(xiàn)類(lèi)。
public class LogCollectHander implements ILogHandler{ @Override public String collect(){ // 獲取你的日志 并返回固定格式 return "類(lèi)型(exception|warn|info):日志內(nèi)容:時(shí)間戳"; } }
然后在系統(tǒng)中增加配置,比如這樣配置:
kite: log: analysis: handler: org.my.project.LogCollectHander
那像 org.my.project.LogCollectHander這個(gè)實(shí)現(xiàn)類(lèi)就是框架之外你自定義的,每個(gè)使用框架的開(kāi)發(fā)者都會(huì)定義不同的實(shí)現(xiàn)類(lèi),所以,框架在編譯的時(shí)候就事先不知道具體類(lèi)型,只有當(dāng)程序運(yùn)行到這里的時(shí)候,通過(guò)配置文件獲取實(shí)現(xiàn)類(lèi)的全名,然后根據(jù)反射獲取 class,然后調(diào)用 collect() 方法,實(shí)現(xiàn)邏輯差不多是這樣:
Class clazz = Class.forName("org.my.project.LogCollectHander"); ILogHandler logHandler = (ILogHandler)clazz.newInstance(); logHandler.collect();
這整個(gè)過(guò)程其實(shí)用到了一個(gè)設(shè)計(jì)模式-「工廠模式」。完善一下代碼如下:
public class MyFactory { private static class SingletonHolder { private static final MyFactory INSTANCE = new MyFactory(); } private MyFactory (){} public static final MyFactory create() { return SingletonHolder.INSTANCE; } public ILogHandler build() throws Exception{ // 來(lái)自于配置文件 String className = "org.my.project.LogCollectHander"; Class clazz = Class.forName(className); ILogHandler logHandler = (ILogHandler)clazz.newInstance(); return logHandler; } } //調(diào)用 ILogHandler myLogCollectHander = MyFactory.create().build(); myLogCollectHander.collect();
這樣一來(lái),利用反射,輕松把不通用的地方整合到了通用框架中。
概括說(shuō)來(lái),反射可以兼容通用框架中不通用(個(gè)性化)的部分。
Spring 控制反轉(zhuǎn)/依賴(lài)注入
類(lèi)似的通用型框架有很多,比如 Spring 中就有很多地方用到了反射,Spring 核心科技「控制反轉(zhuǎn)(IoC)-依賴(lài)注入(DI)」就用到了反射。控制反轉(zhuǎn)的意思就是將控制權(quán)由開(kāi)發(fā)者轉(zhuǎn)交給 Spring 框架,我們用 Spring MVC 的時(shí)候,經(jīng)常會(huì)將 bean 寫(xiě)到 xml 配置文件中,比如這樣:
簡(jiǎn)單來(lái)說(shuō),Spring 框架在啟動(dòng)的時(shí)候會(huì)加載這些 bean 所指定的 class,注冊(cè)到一個(gè) map 中,之后,用到的時(shí)候直接在 map 中取就可以了。那這個(gè)加載的過(guò)程就要用到反射。
JDK 數(shù)據(jù)庫(kù)操作部分
JDK 中關(guān)于數(shù)據(jù)庫(kù)驅(qū)動(dòng)的部分,也用到反射,不管你是用 mysql 還是 oracle,只要你配置好對(duì)應(yīng)的驅(qū)動(dòng)配置信息并添加好驅(qū)動(dòng)依賴(lài)包,JDK 就會(huì)利用反射動(dòng)態(tài)的加載對(duì)應(yīng)驅(qū)動(dòng)類(lèi),然后執(zhí)行驅(qū)動(dòng)類(lèi)中具體的方法。
Dubbo SPI
Dubbo 框架中的 SPI 技術(shù)也用到了反射。
這么說(shuō)吧,當(dāng)你閱讀開(kāi)源代碼時(shí),碰到配置文件中配置了具體類(lèi)的完全名稱(chēng)的地方,那幾乎都會(huì)用到反射。
動(dòng)態(tài)代理,比如AOP
動(dòng)態(tài)代理技術(shù)也會(huì)用到反射,要在生成的代理類(lèi)中動(dòng)態(tài)的調(diào)用原始被代理的方法,比如 AOP。
實(shí)體類(lèi)拷貝
還有我們經(jīng)常會(huì)遇到的兩個(gè) bean 實(shí)體的屬性拷貝,例如 Spring 中的 BeanUtils.copyProperties() 方法。
IDE 和調(diào)試器
另外,還有我們每天開(kāi)發(fā)都會(huì)用到的編輯器中和調(diào)試工具,你在編輯器中敲下"."之后,編輯器會(huì)智能給你相關(guān)方法和屬性的列表,這就是通過(guò)反射實(shí)現(xiàn)的。調(diào)式過(guò)程中,監(jiān)視屬性值等也都是通過(guò)反射實(shí)現(xiàn)。
反射為什么性能比較差
說(shuō)起反射,大家可能都知道性能差,每一本講 Java 的書(shū)籍提到反射的部分都會(huì)說(shuō)反射性能比較差,能不用反射的地方盡量不要用。那反射的性能為什么差呢?
根本的原因就是因?yàn)榉瓷涫莿?dòng)態(tài)加載,所以 jit 對(duì)其所做的優(yōu)化極其有限。
jit - 即時(shí)編譯器,是 JVM 優(yōu)化性能的殺手級(jí)利器,它會(huì)對(duì)熱點(diǎn)代碼進(jìn)行一系列優(yōu)化,比如非常重要的優(yōu)化手段-方法內(nèi)聯(lián)。而反射的代碼則享受不到這種待遇。
反射中性能最差的部分在于獲取方法和屬性的部分,比如 getMethod() 方法,是因?yàn)楂@取這個(gè)方法需要遍歷所有的方法列表,包括父類(lèi)。而如果不是反射的話(huà),那這個(gè)方法地址都是提前確定的。
還有在 method#invoke() 方法執(zhí)行的過(guò)程中需要執(zhí)行要對(duì)參數(shù)做封裝和解封操作,invoke 方法的參數(shù)是 Object[] 類(lèi)型,所以傳入的參數(shù)要轉(zhuǎn)換為 Object 并封裝成數(shù)組,而到了真正執(zhí)行方法的時(shí)候,還要把 Object 數(shù)組解封。這樣一來(lái)一回就浪費(fèi)了不少時(shí)間。
另外還需要需要檢查方法可見(jiàn)性和參數(shù)的校驗(yàn),這樣做是為了保證調(diào)用安全,檢查的過(guò)程也要耗時(shí)。
總結(jié)
那其實(shí)除非發(fā)生大量的反射調(diào)用,正常使用的情況下,性能只是略差而已,這樣的性能損耗比起反射帶來(lái)的靈活性來(lái)講可以忽略不計(jì),比如 Spring 框架要靠反射來(lái)支撐最核心的技術(shù),Spring 給我們?nèi)粘i_(kāi)發(fā)帶來(lái)的好處和它采用反射技術(shù)對(duì)性能的影響而言,那自然不值一提。
另外,如果真的是會(huì)頻繁調(diào)用反射方法,采用緩存的方案可以很大程度上優(yōu)化性能。比如在第一次調(diào)用某個(gè)方法的時(shí)候?qū)⑺彺嫫饋?lái),下次再調(diào)用直接從緩存拿就可以了。
網(wǎng)絡(luò)安全 Java 緩存 Spring 數(shù)據(jù)庫(kù)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。