制深度解析:從Class對(duì)象到框架實(shí)戰(zhàn))
反射這兩個(gè)字在 Java 圈子里幾乎是繞不過(guò)去的。不管是面試還是看框架源碼它都像個(gè)幽靈一樣無(wú)處不在。你去翻開(kāi) Spring 的 IoC 容器、MyBatis 的 Mapper 映射、Hibernate 的實(shí)體關(guān)聯(lián)隨便扒一層都能看見(jiàn)反射的影子。有些初學(xué)者一聽(tīng)到反射就頭皮發(fā)麻覺(jué)得它玄乎其玄其實(shí)真正理解了 Class 對(duì)象和類(lèi)加載機(jī)制之后你會(huì)發(fā)現(xiàn)它就是一個(gè)“運(yùn)行時(shí)的類(lèi)型操作工具”只不過(guò)這把工具的鑰匙藏在 JVM 的底層。這篇文章我想從一個(gè)一線開(kāi)發(fā)者的視角把反射從原理、核心 API、實(shí)戰(zhàn)場(chǎng)景到常見(jiàn)坑位一條線講清楚順帶聊一聊我平時(shí)是怎么用、怎么避坑的。適合剛學(xué)完 Java 基礎(chǔ)想進(jìn)階的朋友也適合準(zhǔn)備面試想要系統(tǒng)梳理的開(kāi)發(fā)者。1. 反射到底是什么理解它的立足之本1.1 一個(gè)最簡(jiǎn)單的場(chǎng)景作為引子先說(shuō)個(gè)最簡(jiǎn)單的例子如果你寫(xiě)代碼的時(shí)候知道一個(gè)類(lèi)的名字比如String你在代碼里直接new String()就行這是“編譯期已知類(lèi)型”。但假如你寫(xiě)代碼的時(shí)候完全不知道這個(gè)類(lèi)是啥用戶只給你一個(gè)字符串java.lang.String你得在運(yùn)行時(shí)通過(guò)這個(gè)字符串把它變成真正的類(lèi)然后創(chuàng)建對(duì)象、調(diào)方法。這就是反射最原始的場(chǎng)景把“名字”變成“類(lèi)型”把“類(lèi)型”變成“對(duì)象”把“對(duì)象”當(dāng)成“財(cái)產(chǎn)”來(lái)翻查。正常編程像是拿著圖紙施工從地基到窗戶都是定死的反射則像是施工到一半工程隊(duì)隊(duì)長(zhǎng)掏出一本“已建成的建筑手冊(cè)”說(shuō)我現(xiàn)在不看了我可以根據(jù)手冊(cè)實(shí)時(shí)修改房間格局、臨時(shí)查看和調(diào)用任何房間里已有的設(shè)施。這個(gè)比喻雖然粗糙但核心意思對(duì)反射是程序在運(yùn)行過(guò)程中動(dòng)態(tài)地去查看和操作自身結(jié)構(gòu)的能力。1.2 反射解決的根本矛盾為什么框架作者天天跟反射打交道因?yàn)榭蚣苡幸粋€(gè)天然的矛盾寫(xiě)框架的時(shí)候它壓根不知道未來(lái)要接手什么類(lèi)。我寫(xiě)個(gè)通用 JSON 序列化庫(kù)不能提前知道你會(huì)拿什么 User、Order、Product 類(lèi)來(lái)調(diào)用我寫(xiě)個(gè) ORM 框架同樣不知道你要映射哪張表、哪個(gè)實(shí)體。編譯器幫不上忙因?yàn)樾畔⒃诰幾g期根本就不存在。反射就是在這種“運(yùn)行時(shí)才見(jiàn)分曉”的場(chǎng)景下JVM 給開(kāi)發(fā)者打開(kāi)的一扇門(mén)——讓我在代碼運(yùn)行的那一瞬間去讀取類(lèi)的字段、方法、構(gòu)造器然后動(dòng)態(tài)地干活。拿 Spring 的 IoC 容器舉例你把一個(gè)配置類(lèi)路徑丟給它它用Class.forName()加載類(lèi)掃描類(lèi)的注解和字段發(fā)現(xiàn)有Autowired注解的字段就通過(guò)反射賦值。這一套流程下來(lái)容器在編譯期對(duì)你寫(xiě)的每個(gè)類(lèi)毫無(wú)感知但運(yùn)行期卻能把它們串成完整協(xié)作的網(wǎng)絡(luò)。這就是反射的立足之本面向未來(lái)未知類(lèi)型的動(dòng)態(tài)調(diào)度能力。2. Class 對(duì)象反射機(jī)制的絕對(duì)核心2.1 三類(lèi)加載一個(gè)入口要玩轉(zhuǎn)反射第一步是拿到某個(gè)類(lèi)的Class對(duì)象。這個(gè)對(duì)象是 JVM 在類(lèi)加載階段自動(dòng)創(chuàng)建的一個(gè)“類(lèi)型檔案”里面裝了這個(gè)類(lèi)的全部元數(shù)據(jù)類(lèi)名、父類(lèi)、接口、字段列表、方法列表、注解列表等等。獲取它的方式有三種很多人一開(kāi)始總是混Class.forName(com.example.User)最典型的全限定名方式適合你手里只有字符串的場(chǎng)景同時(shí)它會(huì)執(zhí)行類(lèi)的靜態(tài)初始化塊。User.class編譯期已知類(lèi)型時(shí)推薦這種方式不會(huì)觸發(fā)靜態(tài)初始化拿到 Class 對(duì)象是“最輕量”的。user.getClass()手里已經(jīng)有實(shí)例對(duì)象想回頭查它的類(lèi)型信息這是最簡(jiǎn)單粗暴的方式。三種方式各有各的用武之地??蚣苤写蠖鄶?shù)用Class.forName因?yàn)榕渲美锞褪亲址?dāng)我們想主動(dòng)檢查某個(gè)對(duì)象的實(shí)際類(lèi)時(shí)用getClass()。有個(gè)細(xì)節(jié)值得注意Class.forName會(huì)觸發(fā)初始化如果你的類(lèi)在靜態(tài)塊里做了重活比如加載外部配置、啟動(dòng)線程那調(diào)用這個(gè) API 的瞬間可能會(huì)有明顯卡頓。用.class的方式則完全不會(huì)它只是把已有的類(lèi)元數(shù)據(jù)取出來(lái)。2.2 類(lèi)加載與 Class 對(duì)象的關(guān)系很多人問(wèn)過(guò)一個(gè)問(wèn)題Class 對(duì)象是 JVM 什么時(shí)候創(chuàng)建的答案是類(lèi)加載過(guò)程會(huì)把.class文件的二進(jìn)制字節(jié)流讀進(jìn)來(lái)在“加載”階段 JVM 就生成了代表這個(gè)類(lèi)的 Class 對(duì)象并且把它放到方法區(qū)Java 8 之后是元空間維護(hù)。之后“連接”階段做校驗(yàn)、準(zhǔn)備、解析“初始化”階段執(zhí)行靜態(tài)變量賦值和靜態(tài)塊。所以反射能拿到的信息其實(shí)是 JVM 內(nèi)部已經(jīng)完整解析過(guò)的類(lèi)型結(jié)構(gòu)而不是你自己讀.class文件再去解析一遍。這也是為什么反射的代價(jià)并不完全沒(méi)有——信息的來(lái)源已經(jīng)準(zhǔn)備好了但操作的動(dòng)態(tài)性產(chǎn)出了額外開(kāi)銷(xiāo)。2.3 Class 對(duì)象能干什么拿到 Class 對(duì)象之后你幾乎可以做任何事創(chuàng)建實(shí)例getDeclaredConstructor().newInstance()、讀取/修改字段值getDeclaredField()、setAccessible()、調(diào)用方法getDeclaredMethod()、invoke()、動(dòng)態(tài)創(chuàng)建數(shù)組Array.newInstance()、獲取注解、判斷繼承關(guān)系等等。有一句話總結(jié)得很好反射本質(zhì)上是“元級(jí)編程”它操作的是一級(jí)數(shù)據(jù)——類(lèi)型而不只是對(duì)象。3. 反射核心 API 實(shí)戰(zhàn)拆解3.1 創(chuàng)建對(duì)象的幾種姿勢(shì)反射創(chuàng)建實(shí)例最常見(jiàn)的方法是Class.forName(...).getDeclaredConstructor().newInstance()。注意這里已經(jīng)不是舊版的newInstance()了那個(gè)方法在 Java 9 后標(biāo)記為廢棄因?yàn)樗荒軈^(qū)分你調(diào)的是構(gòu)造器還是類(lèi)本身的靜態(tài)方法。你可能會(huì)說(shuō)這有多大區(qū)別其實(shí)區(qū)別很實(shí)際舊方法用一個(gè)看似創(chuàng)建對(duì)象的 API底層卻走了另一條路編譯器無(wú)法保證行為一致新方式強(qiáng)制你先拿到 Constructor再通過(guò) Constructor 的newInstance(Object... initArgs)創(chuàng)建語(yǔ)義更清晰可讀性也更好。如果目標(biāo)類(lèi)沒(méi)有無(wú)參構(gòu)造器或者構(gòu)造器有參數(shù)那就需要提前把參數(shù)類(lèi)型數(shù)組傳給getDeclaredConstructor(Class?... parameterTypes)。這里又是一個(gè)經(jīng)典坑位你傳入的參數(shù)類(lèi)型必須是精確匹配的反射不會(huì)幫你做隱式轉(zhuǎn)型。比如構(gòu)造器接收Integer你傳int.class是匹配不上的。我見(jiàn)過(guò)不少人在這里排查半天最后發(fā)現(xiàn)只是類(lèi)型不匹配。Class? clazz Class.forName(com.example.User); Constructor? constructor clazz.getDeclaredConstructor(String.class, int.class); Object obj constructor.newInstance(張三, 18);假如構(gòu)造器是私有的比如單例模式或者工具類(lèi)你直接調(diào)用會(huì)拋IllegalAccessException。這時(shí)候先調(diào)constructor.setAccessible(true)這是反射世界里最常見(jiàn)的一個(gè)“違規(guī)操作”入口后面我會(huì)專(zhuān)門(mén)說(shuō)它的原理和代價(jià)。3.2 操作字段讀值與改值字段操作使用的 API 有兩組很多新手會(huì)弄混getField(String name)只返回 public 字段且包含繼承下來(lái)的字段。getDeclaredField(String name)返回當(dāng)前類(lèi)聲明的所有訪問(wèn)權(quán)限字段不包含繼承。這兩者的區(qū)別是很多NoSuchFieldException的根源。比如你想在子類(lèi)里反射操作父類(lèi)的 private 字段用getDeclaredField會(huì)直接找不到這時(shí)候需要沿著父類(lèi)鏈?zhǔn)止ね险?。我?xiě)框架的時(shí)候如果有需要掃描類(lèi)的字段通常會(huì)寫(xiě)一個(gè) while 循環(huán)從當(dāng)前類(lèi)一路遍歷到 Object。Class? clazz obj.getClass(); Field field clazz.getDeclaredField(name); // 報(bào)錯(cuò)先想想這個(gè)字段是不是父類(lèi)的拿到 Field 之后讀取對(duì)象字段值用field.get(obj)修改用field.set(obj, value)。注意這兩個(gè)方法的第一個(gè)參數(shù)傳的是“目標(biāo)對(duì)象實(shí)例”而不是 Class因?yàn)樽侄问谴嬖趯?duì)象實(shí)例上的。靜態(tài)字段傳null即可這一點(diǎn)很多人第一反應(yīng)會(huì)傳 Class 對(duì)象進(jìn)去然后就拋IllegalArgumentException。3.3 調(diào)用方法invoke 的細(xì)節(jié)方法調(diào)用更靈活但坑也更多。getMethod只能拿 public 且包含繼承的方法getDeclaredMethod拿當(dāng)前類(lèi)所有方法但忽略繼承。調(diào)用用method.invoke(target, args...)實(shí)例方法target 傳對(duì)象實(shí)例。靜態(tài)方法target 傳 null。參數(shù)匹配同樣需要精確匹配參數(shù)類(lèi)型int.class和Integer.class不能混。有個(gè)非常隱蔽的問(wèn)題當(dāng)你在反射調(diào)用一個(gè)簽名是(Integer, Integer)的方法時(shí)參數(shù)傳1和2看起來(lái)沒(méi)問(wèn)題但 Java 會(huì)自動(dòng)裝包底層在反射參數(shù)匹配時(shí)也可能產(chǎn)生歧義。穩(wěn)妥的做法是構(gòu)造 Object[] 的時(shí)候把數(shù)值先顯式轉(zhuǎn)型成Integer避免某些極端場(chǎng)景下匹配錯(cuò)亂。Method method clazz.getDeclaredMethod(calculate, Integer.class, Integer.class); Object result method.invoke(obj, Integer.valueOf(1), Integer.valueOf(2));3.4 注解掃描反射的最常見(jiàn)工業(yè)場(chǎng)景反射和注解幾乎是綁定的一對(duì)。Spring 的Autowired、ComponentMyBatis 的Mapper都是基于“掃描注解反射操作”的套路。核心 API 就幾個(gè)clazz.isAnnotationPresent(MyAnnotation.class)判斷類(lèi)上有沒(méi)有這個(gè)注解。clazz.getAnnotation(MyAnnotation.class)拿注解實(shí)例。clazz.getDeclaredFields()遍歷字段去看field.isAnnotationPresent(...)或者遍歷方法看method.getAnnotation(...)。這個(gè)模式是最實(shí)用的框架入門(mén)路徑注解定義配置規(guī)則反射讀取規(guī)則并執(zhí)行動(dòng)作。平時(shí)自己寫(xiě)個(gè)小工具比如要做字段級(jí)權(quán)限控制定義了Permission(level admin)反射遍歷字段去校驗(yàn)當(dāng)前用戶角色夠不夠權(quán)限幾行代碼就搞定這比在業(yè)務(wù)里寫(xiě)一堆 if-else 干凈太多了。4. 實(shí)戰(zhàn)案例手寫(xiě)一個(gè)迷你依賴注入容器4.1 需求與設(shè)計(jì)思路紙上談兵那么多我們直接動(dòng)手做一個(gè) 200 行以內(nèi)的迷你 IoC 容器。需求很簡(jiǎn)單約定一個(gè)包路徑下的類(lèi)凡是標(biāo)注了MyComponent注解的類(lèi)自動(dòng)注冊(cè)類(lèi)中字段標(biāo)注了MyInject注解的自動(dòng)注入依賴實(shí)例。核心流程就是三個(gè)角色掃描器找類(lèi)、注冊(cè)器建工廠、注入器組裝實(shí)例。設(shè)計(jì)思路是這樣的掃描器接收一個(gè)包名用Class.forName把它變成 Class 對(duì)象注冊(cè)器把 Class 對(duì)象存到一個(gè) Map 里key 是類(lèi)名value 是 Class注入器在創(chuàng)建實(shí)例的時(shí)候先查 Map 里有沒(méi)有這個(gè)類(lèi)有就直接返回緩存實(shí)例沒(méi)有就遞歸創(chuàng)建它依賴的字段最后用反射 set 進(jìn)去。4.2 掃描與注冊(cè)掃描包路徑這個(gè)環(huán)節(jié)如果用純反射會(huì)比較繁瑣因?yàn)镃lassLoader并不支持直接“列出某個(gè)包下所有類(lèi)”。常規(guī)做法是用文件系統(tǒng)掃描 classpath 目錄把package換成路徑字符串然后用Files.walk找到.class文件再挨個(gè)Class.forName。這部分代碼偏向 IO 操作不展開(kāi)寫(xiě)細(xì)節(jié)核心過(guò)度點(diǎn)是Class.forName。實(shí)際生產(chǎn)中的 Spring 會(huì)有更復(fù)雜的掃描機(jī)制但原理一樣。public class MyContainer { private MapString, Object beanMap new ConcurrentHashMap(); private MapString, Class? classMap new ConcurrentHashMap(); public void scan(String basePackage) throws Exception { String path basePackage.replace(., /); URL resource Thread.currentThread().getContextClassLoader().getResource(path); // 遍歷 resource 目錄下的 .class 文件逐個(gè) Class.forName // 判斷是否標(biāo)注了 MyComponent有就放進(jìn) classMap } }注冊(cè)階段有個(gè)細(xì)節(jié)值得提掃描到的類(lèi)不一定都可以立即實(shí)例化因?yàn)閷?shí)例化和依賴注入是有順序的。我習(xí)慣把實(shí)例化包裝成一個(gè)方法createBean(Class? clazz)內(nèi)部先查緩存沒(méi)有再遞歸實(shí)例化依賴字段。4.3 依賴注入的遞歸實(shí)現(xiàn)依賴注入最核心的一段代碼是創(chuàng)建實(shí)例后遍歷所有字段。每個(gè)字段如果帶了MyInject注解就先根據(jù)字段類(lèi)型找到對(duì)應(yīng)的 Class 對(duì)象然后遞歸調(diào)用createBean(Field.getType())拿到依賴實(shí)例之后用反射 set。private Object createBean(Class? clazz) throws Exception { String beanName clazz.getName(); if (beanMap.containsKey(beanName)) { return beanMap.get(beanName); } Object instance clazz.getDeclaredConstructor().newInstance(); beanMap.put(beanName, instance); // 先放緩存避免循環(huán)依賴時(shí)無(wú)限遞歸 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(MyInject.class)) { field.setAccessible(true); Class? fieldType field.getType(); Object value createBean(fieldType); // 遞歸創(chuàng)建依賴 field.set(instance, value); } } return instance; }這里有一個(gè)很關(guān)鍵的經(jīng)驗(yàn)點(diǎn)創(chuàng)建完實(shí)例要先放進(jìn)緩存再處理依賴注入否則兩個(gè)類(lèi)相互依賴時(shí)A 依賴 BB 依賴 A會(huì)無(wú)限遞歸導(dǎo)致棧溢出。先放緩存在語(yǔ)義上相當(dāng)于給 Spring 的“提前暴露對(duì)象引用”做了個(gè)簡(jiǎn)化版——雖然嚴(yán)格說(shuō) Spring 解決的是三級(jí)緩存下的循環(huán)依賴問(wèn)題但先存引用至少避免遞歸卡死。真實(shí)項(xiàng)目中設(shè)計(jì)依賴關(guān)系時(shí)最好還是別搞成循環(huán)能解耦就解耦別拿框架特性當(dāng)設(shè)計(jì)欠賬的底氣。4.4 必要的異常處理與邊界反射代碼的異常類(lèi)型非常多ClassNotFoundException、NoSuchMethodException、IllegalAccessException、InvocationTargetException、InstantiationException。其中最陰險(xiǎn)的是InvocationTargetException因?yàn)樗前b異常真正的問(wèn)題藏在e.getCause()里面。我用反射寫(xiě)工具時(shí)習(xí)慣先統(tǒng)一捕獲Exception再打印printStackTrace()調(diào)試時(shí)優(yōu)先看 cause不然很容易被表面反射異常誤導(dǎo)。5. 反射的底層原理、性能損耗與安全限制5.1 setAccessible 背后做了什么先解決一個(gè)大問(wèn)題setAccessible(true)為什么會(huì)讓人有“打破封裝”的不安全感其實(shí)它做的不是“解密”而是“請(qǐng)求 JVM 在本次調(diào)用時(shí)跳過(guò) Java 語(yǔ)言層面的訪問(wèn)控制檢查”。Java 語(yǔ)言本身不允許訪問(wèn)私有成員但反射 API 在授權(quán)檢查前會(huì)檢查這個(gè)標(biāo)記位一旦設(shè)置為 true底層就少了很多權(quán)限判斷。這帶來(lái)的好處是暴力直接壞處是如果你在代碼里瘋狂調(diào)用私有方法且每次都不緩存那每次調(diào)用都要重復(fù)做這些檢查性能完全壓不住。Java 9 模塊化之后setAccessible也不是萬(wàn)能鑰匙了。如果你通過(guò)--add-opens或是模塊描述符沒(méi)有做相應(yīng)開(kāi)放非法訪問(wèn)時(shí)已經(jīng)不靜默了而是直接拋InaccessibleObjectException。所以有個(gè)心法很重要反射能改私有字段不代表你應(yīng)該到處改訪問(wèn)控制是設(shè)計(jì)上給你的邊界越過(guò)之前先問(wèn)問(wèn)自己是否有必要。5.2 為什么反射慢以及如何優(yōu)化反射為什么慢拋開(kāi) JIT 的影響直接說(shuō)三次主要開(kāi)銷(xiāo)類(lèi)型查找與安全檢查每次拿到 Method 之后調(diào)用 invokeJVM 都要做一大串訪問(wèn)驗(yàn)證而且如果不能內(nèi)聯(lián)這些開(kāi)銷(xiāo)每次都得付。參數(shù)裝箱與 Object[] 構(gòu)造invoke 的方法簽名是Object... args你傳基本類(lèi)型它會(huì)自動(dòng)裝箱反射層還要把這些對(duì)象展開(kāi)、轉(zhuǎn)成正確的調(diào)用約定這比直接調(diào)用多了一大坨間接成本。動(dòng)態(tài)分派反射調(diào)用本身是動(dòng)態(tài)分派JIT 無(wú)法像普通方法一樣直接內(nèi)聯(lián)或做逃逸分析某些情況下優(yōu)化跟不上普通調(diào)用路徑。既然知道了慢從哪里來(lái)優(yōu)化方向也就清晰了緩存反射對(duì)象把Method和Field對(duì)象緩存到靜態(tài) Map 中避免每次調(diào)用都getDeclaredMethod猛查一遍。這是最直接也是收益最大的優(yōu)化很多框架就是這么干的。減少訪問(wèn)控制檢查盡量在創(chuàng)建反射對(duì)象時(shí)setAccessible(true)一次設(shè)置后續(xù)所有調(diào)用都不再檢查訪問(wèn)權(quán)限。批量調(diào)用而不是循環(huán)調(diào)用循環(huán)里反復(fù)真反射調(diào)用會(huì)很受傷。盡量把邏輯上移到靜態(tài)塊或初始化階段把反射結(jié)果提取到普通字段里再用??紤] MethodHandlejava.lang.invoke包下的MethodHandle是比傳統(tǒng)反射更接近 JIT 的優(yōu)化點(diǎn)能在某些場(chǎng)景下達(dá)到近乎直接調(diào)用的效果。如果性能敏感可以把它當(dāng)作更高級(jí)的替代方案。有沒(méi)有可能用接口替換反射這在架構(gòu)設(shè)計(jì)上更管用。能定義接口的地方盡量定義接口調(diào)用側(cè)走接口編譯只有框架側(cè)才用反射裝配兩邊都干凈利落。5.3 模塊系統(tǒng)對(duì)反射的限制提到 Java 9 模塊化就必須講一講它對(duì)反射的影響。如果你在一個(gè) module 內(nèi)部寫(xiě)得挺爽反射去訪問(wèn)另一個(gè) module 的類(lèi)而且對(duì)方?jīng)]有exports和opens給自己那反射會(huì)直接報(bào)IllegalAccessException。exports控制的是編譯期可見(jiàn)性opens控制的是反射期訪問(wèn)。所以如果你的公司在升級(jí) Java 17 或 21 時(shí)發(fā)現(xiàn)某些舊框架反射調(diào)用掛了多半是模塊邊界問(wèn)題而非代碼邏輯問(wèn)題解決辦法一般是給啟動(dòng)參數(shù)配置--add-opens java.base/java.langALL-UNNAMED這類(lèi)白名單授信。這是我實(shí)際升級(jí)時(shí)踩過(guò)的一個(gè)坑先別急著懷疑框架拿 cause chain 追蹤到底是哪個(gè)模塊沒(méi)開(kāi)放。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 高頻異常對(duì)照表異常類(lèi)型出現(xiàn)場(chǎng)景排查方向ClassNotFoundExceptionClass.forName(com.xxx.Clazz)找不到類(lèi)類(lèi)名是否寫(xiě)錯(cuò)依賴 jar 是否引入classpath 是否正確NoSuchMethodException要調(diào)的方法本來(lái)存在但沒(méi)找到方法名拼錯(cuò)參數(shù)類(lèi)型是否精確匹配int vs Integer用的是getMethod還是getDeclaredMethodNoSuchFieldException字段反射失敗字段名拼錯(cuò)private 字段用了getField而不是getDeclaredField字段在父類(lèi)里IllegalAccessException權(quán)限不足調(diào)私有成員前是否setAccessible(true)Java 9 是否有模塊 opens 限制InvocationTargetExceptioninvoke包裝異常看e.getCause()真正異常在里面IllegalArgumentException參數(shù)類(lèi)型不匹配檢查參數(shù)個(gè)數(shù)、類(lèi)型、靜態(tài)方法是否傳了 nullClassCastException反射返回 Object 強(qiáng)轉(zhuǎn)出錯(cuò)先打印obj.getClass()看真實(shí)類(lèi)型6.2 泛型擦除與反射的“靈魂拷問(wèn)”泛型和反射之間有個(gè)天然的坑Java 泛型在運(yùn)行時(shí)會(huì)被擦除。你定義一個(gè)ListString的字段運(yùn)行時(shí)這個(gè)字段的類(lèi)型只會(huì)是List.class具體是 String 還是 Integer反射默認(rèn)看不到。但是 Java 設(shè)計(jì)者也沒(méi)把路堵死在字段的Field對(duì)象上提供了getGenericType()方法的返回值也可以用getGenericReturnType()返回的是ParameterizedType。這類(lèi)接口可以把泛型信息解析出來(lái)Field field clazz.getDeclaredField(nameList); Type genericType field.getGenericType(); if (genericType instanceof ParameterizedType pt) { Type actualType pt.getActualTypeArguments()[0]; System.out.println(actualType.getTypeName()); // 打印出 java.lang.String }這個(gè)技術(shù)在寫(xiě) JSON 反序列化器時(shí)非常有用比如 Jackson 里TypeReferenceListString的類(lèi)型捕獲本質(zhì)上就是通過(guò)反射抓泛型信息。記住一個(gè)規(guī)律實(shí)例對(duì)象上沒(méi)有泛型信息但字段聲明和方法簽名上還有因?yàn)樗鼈兪菍?xiě)在字節(jié)碼里的。6.3 內(nèi)部類(lèi)與靜態(tài)內(nèi)部類(lèi)的特別之處反射實(shí)例化內(nèi)部類(lèi)是個(gè)冷門(mén)但真實(shí)會(huì)遇到的坑。非靜態(tài)內(nèi)部類(lèi)會(huì)隱式持有外部類(lèi)的引用它的構(gòu)造器首參其實(shí)是外部類(lèi)實(shí)例所以用反射創(chuàng)建內(nèi)部類(lèi)實(shí)例時(shí)除了找對(duì)應(yīng)構(gòu)造器還得額外傳入外部類(lèi)對(duì)象。而靜態(tài)內(nèi)部類(lèi)則沒(méi)有這種問(wèn)題它和普通類(lèi)一致。寫(xiě)框架掃描類(lèi)并嘗試實(shí)例化時(shí)如果不斷遇到InstantiationException或構(gòu)造器參數(shù)對(duì)不上建議先檢查這個(gè)類(lèi)是不是一個(gè)非靜態(tài)內(nèi)部類(lèi)。這個(gè)坑在 Kotlin 或者 Scala 的伴生對(duì)象場(chǎng)景里更常見(jiàn)Java 里相對(duì)少見(jiàn)但一旦遇到確實(shí)夠折騰。6.4 利用反射定位問(wèn)題的實(shí)用技巧反射代碼出錯(cuò)時(shí)很常見(jiàn)的問(wèn)題是“我明明看到源碼里有這個(gè)方法為什么反射說(shuō)沒(méi)有”。最快定位手段不是復(fù)查源碼而是寫(xiě)一段臨時(shí)代碼把所有可達(dá)方法打出來(lái)for (Method m : clazz.getDeclaredMethods()) { System.out.println(m.getName() - Arrays.toString(m.getParameterTypes())); }這種方式在調(diào)試第三方 jar 時(shí)尤其有用因?yàn)閯e人打包后的字節(jié)碼可能跟你手里的源碼版本不一致方法名被混淆掉、方法簽名變化這些都是常見(jiàn)情況。我遇到過(guò)幾次看似“反射調(diào)不通”的問(wèn)題最后都是版本不匹配導(dǎo)致的方法簽名變化。先打印出真實(shí)類(lèi)型結(jié)構(gòu)再?zèng)Q定怎么調(diào)比盲試強(qiáng)太多。7. 寫(xiě)在最后的一點(diǎn)經(jīng)驗(yàn)玩反射玩了這些年我最大的體會(huì)就是反射是框架層級(jí)的利器也是業(yè)務(wù)代碼層的危險(xiǎn)品。寫(xiě)框架、寫(xiě)通用工具、做測(cè)試輔助反射幾乎不可或缺但在業(yè)務(wù)代碼里到處用反射帶來(lái)的往往不是優(yōu)雅而是可讀性惡化、性能走樣和類(lèi)型安全崩塌。一個(gè)方法本來(lái)直接調(diào)就好了非得繞一大圈去反射后期排查問(wèn)題的時(shí)候后悔都來(lái)不及。能編譯期解決的問(wèn)題就不要拖到運(yùn)行時(shí)去“靈活”真正的靈活應(yīng)該是留給那些動(dòng)態(tài)性無(wú)法回避的場(chǎng)景。如果你打算深入學(xué)習(xí)建議按這個(gè)路徑走先吃透 Class 對(duì)象和類(lèi)加載機(jī)制再手寫(xiě)一個(gè)小型注解處理器或依賴注入 Demo最后再去讀一遍 Spring 和 MyBatis 里反射相關(guān)的源碼片段。把這三步走完反射這關(guān)算是真正過(guò)關(guān)了。