Java反射與注解的詳細講解(含代碼)
前言
初學(xué) Java SE 時,我們習(xí)慣手動 new 對象、調(diào)用方法,一切直白可控。
但進入 Spring、MyBatis 等框架后,@Autowired 自動裝配依賴,@Transactional 即可生效事務(wù)……看似神奇,其實底層依然是 反射與注解的默默支撐。
一、 為什么有反射?
在真正接觸框架之前,很多開發(fā)者會覺得反射又慢又麻煩,直接 new 對象不好嗎?
要回答這個問題,我們需要從代碼的演進思維說起。
1.1 硬編碼
日常業(yè)務(wù)開發(fā)中,我們絕大部分代碼都是硬編碼。所謂硬編碼,就是在寫代碼的時候(編譯期),就已經(jīng)完全確定了要使用哪個具體的類。
代碼演示:
// 定義一個接口
public interface UserService {
void serve();
}
// 具體的實現(xiàn)類 A
public class UserServiceImpl implements UserService {
@Override
public void serve() { System.out.println("執(zhí)行舊版業(yè)務(wù)邏輯..."); }
}
// 我們的主程序
public class MyApp {
public static void main(String[] args) {
// 【硬編碼痛點】:主程序強依賴了 UserServiceImpl 這個具體的實現(xiàn)類
UserService service = new UserServiceImpl();
service.serve();
}
}
分析:
編譯器清清楚楚地知道你要實例化 UserServiceImpl。這種寫法的優(yōu)點是運行速度極快、類型絕對安全。
但它的缺點是極度僵化(高耦合)。假設(shè)隨著業(yè)務(wù)發(fā)展,你寫了一個更好的實現(xiàn)類 UserServiceNewImpl,你必須打開 MyApp.java 的源代碼,把 new UserServiceImpl() 改成 new UserServiceNewImpl(),然后重新編譯整個項目,打包,停機發(fā)布。這在大型工程中是不可接受的。
1.2 軟編碼
為了解決硬編碼帶來的耦合問題,前人想出了一個聰明的辦法:把“到底用哪個類”的決定權(quán),從源代碼里抽離出來,放到外部的配置文件里。
代碼演示:
假設(shè)我們有一個外部配置文件 application.properties:
# 在不修改源代碼的情況下,只需修改這里的字符串即可切換實現(xiàn)類 service.class=com.example.UserServiceNewImpl
我們的主程序變成這樣:
public class MyApp {
public static void main(String[] args) throws Exception {
// 1. 讀取外部配置文件,拿到類名的字符串(這里用硬編碼字符串模擬讀取過程)
String className = "com.example.UserServiceNewImpl";
// 2. 利用反射,把一個“字符串”變成活生生的“對象”
Class<?> clazz = Class.forName(className);
UserService service = (UserService) clazz.getDeclaredConstructor().newInstance();
service.serve();
}
}
分析:
這時候,MyApp.java 在編譯階段根本不知道自己最終要創(chuàng)建什么對象,它只依賴 UserService 這個抽象的接口。
只有在程序運行那一刻,讀取到了配置文件的字符串,才知道要加載誰。
這就是軟編碼。而反射機制,就是跨越“字符串”和“運行時對象”之間那道鴻溝的唯一橋梁。
1.3 框架的核心訴求:對象工廠模式
Spring 這樣的 IoC(控制反轉(zhuǎn))框架,本質(zhì)上就是一個超級、通用的對象工廠(Object Factory)。
框架的作者(比如 Spring 的創(chuàng)始人 Rod Johnson)在寫 Spring 的時候,完全不可能預(yù)知你(使用者)未來會寫出什么奇怪的類(比如 OrderController、UserMapper)。
框架為了能幫你統(tǒng)一管理這些未知的對象,只能采用這樣的設(shè)計:
代碼演示(極簡版 Spring 容器雛形):
import java.util.HashMap;
import java.util.Map;
// 這是一個通用的對象工廠(模擬 Spring 的 ApplicationContext)
public class SimpleBeanFactory {
// 容器內(nèi)部維護一個 Map,存放:<Bean的名字, 類的全限定名>
private Map<String, String> beanDefinitionMap = new HashMap<>();
public SimpleBeanFactory() {
// 框架在啟動時,會去掃描你的 XML 配置文件或 @Component 注解
// 解析出你需要管理的類,并放進 Map 中
beanDefinitionMap.put("userService", "com.example.UserServiceNewImpl");
beanDefinitionMap.put("orderService", "com.example.OrderServiceImpl");
}
// 核心的工廠方法:你給我一個 Bean 的名字,我給你一個可用的對象
public Object getBean(String beanName) throws Exception {
String className = beanDefinitionMap.get(beanName);
if (className == null) {
throw new RuntimeException("容器中找不到對應(yīng)的 Bean 定義");
}
// 框架利用反射,動態(tài)地替你把對象造出來
return Class.forName(className).getDeclaredConstructor().newInstance();
}
}
// 開發(fā)者使用框架時的樣子:
public class DeveloperApp {
public static void main(String[] args) throws Exception {
SimpleBeanFactory factory = new SimpleBeanFactory();
// 開發(fā)者再也不需要自己去 new 對象了,全權(quán)交給框架的反射去創(chuàng)建
UserService userService = (UserService) factory.getBean("userService");
userService.serve();
}
}
總結(jié):
從這三個步驟的演進,你就能深刻體會到:沒有反射,就沒有軟編碼;沒有軟編碼,就寫不出任何具備擴展性的現(xiàn)代 Java 框架。
反射就是為了解決在“運行時處理未知類”這一核心訴求而誕生的。

二、 什么是反射?
2.1 萬物皆對象
在 Java 中,“一切皆對象”這條箴言需要更精確的表述:不僅類的實例是對象,類本身也是一個對象。
在常規(guī)的面向?qū)ο缶幊讨?,我們的認知是這樣的:
- 類(Class) 是圖紙、是模具。
- 對象(Object) 是根據(jù)圖紙造出來的汽車、是模具刻出來的餅干。
圖紙就是圖紙,汽車就是汽車,兩者截然不同。但是,在 JVM 的世界里,為了能讓程序在運行時去讀取和修改圖紙,JVM 把“圖紙”本身也封裝成了一個“產(chǎn)品”(對象)。
2.1.1 概念理解
假設(shè)你現(xiàn)在就是詹姆斯·高斯林(Java 之父),你需要設(shè)計一種數(shù)據(jù)結(jié)構(gòu),用來在內(nèi)存中保存程序員寫出來的 User、Order、Product 等各種各樣的類的信息。你會怎么設(shè)計?
你肯定會專門寫一個類,用來描述“類”這種抽象事物。這個用來描述“類”的類,就叫 java.lang.Class。
它的底層邏輯(偽代碼)大概長這樣:
// 這是 JDK 源碼里真實存在的 java.lang.Class 類的縮影
public final class Class<T> {
private String name; // 記錄類的全限定名(如 com.example.User)
private Field[] fields; // 記錄類里面有哪些屬性
private Method[] methods; // 記錄類里面有哪些方法
private Constructor[] cons; // 記錄類里面有哪些構(gòu)造器
private Class superclass; // 記錄它的父類是誰
// ... 各種獲取這些信息的方法
}
在 Java 中,實例化(創(chuàng)建對象)不僅發(fā)生在業(yè)務(wù)層面,也發(fā)生在底層系統(tǒng)層面。這其實是一個三層結(jié)構(gòu):
第一層:萬物之母
java.lang.Class。這是 JDK 源碼里自帶的一個類。你可以把它理解為一家「專門生產(chǎn) 3D 打印機的工業(yè)母機」。
它的工作只有一個:定義“3D打印機(圖紙)”應(yīng)該長什么樣(必須有類名、有字段數(shù)組、有方法數(shù)組)。
第二層:圖紙對象
User.class(中間態(tài))。當 JVM 啟動,加載到你寫的
User代碼時,這臺“工業(yè)母機”就自動啟動了,它為你生產(chǎn)出了一臺「印著 User 標簽的專屬 3D 打印機」**。 **這臺印著 User 標簽的 3D 打印機,本身就是一個實實在在的 產(chǎn)品(對象)!它是
java.lang.Class生產(chǎn)出來的實例。
第三層:業(yè)務(wù)對象
new User()(最終態(tài))。現(xiàn)在,你在代碼里寫下了
new User()(或者用反射newInstance),就相當于啟動了這臺專屬的 User 3D 打印機。最后打印出了一個個具體的「塑料小人(user 業(yè)務(wù)對象)」。
代碼證明如下:
User userObj = new User(); // 這是一個 User 對象 // 證明 1:userObj 是誰的實例? System.out.println(userObj instanceof User); // true (塑料小人是 3D 打印機印出來的) Class<?> userClassObj = User.class; // 這是一個 Class 對象 // 證明 2:User.class 是誰的實例? System.out.println(userClassObj instanceof java.lang.Class); // true (3D 打印機是工業(yè)母機造出來的)

2.1.2 底層分析
當你的程序啟動,執(zhí)行到 User user = new User(); 時,背后發(fā)生了兩件事:
- 印戶口本(僅一次):JVM 的類加載器讀取了
User.class字節(jié)碼文件,并在堆內(nèi)存中實例化了一個java.lang.Class對象。這個對象里的name屬性被填上了"com.example.User",fields數(shù)組被填上了你寫的屬性。這面“鏡子”就此誕生。 - 造汽車(可無數(shù)次):JVM 照著這面鏡子(圖紙),在堆內(nèi)存的另一塊區(qū)域,為你開辟空間,真正造出了那個包含數(shù)據(jù)的
user實例對象。

核心總結(jié):User 類的實例是 user 對象;而 User 類本身,則是 java.lang.Class 這個類的實例對象!這就是所謂的“萬物皆對象”。
2.2 Class 對象
2.2.1 概念理解
Class 是 Java 中用來描述「類本身」的最終類(java.lang.Class),無法被繼承。
每一個被 JVM 加載的類 / 類型,在內(nèi)存中都會生成唯一的 Class 對象,這個對象封裝了該類的全部元數(shù)據(jù)信息(類名、父類、接口、字段、方法、構(gòu)造器等),是 Java 反射、動態(tài)代理、多態(tài)實現(xiàn)的核心基石。
通俗類比:
| 概念 | 生活類比 | 核心作用 |
|---|---|---|
| 類(Class 代碼) | 建筑圖紙 | 定義「結(jié)構(gòu)、能力」的模板 |
| Class 對象(類對象) | 圖紙的官方唯一備案檔案 | 記錄圖紙的全部信息,全局唯一 |
| 實例對象(Instance) | 按圖紙建成的房子 | 按模板生成的具體個體,有獨立屬性 |
三個核心概念的精準區(qū)分
| 概念 | 代碼示例 | 核心本質(zhì) | 生命周期 |
|---|---|---|---|
| 類(普通類) | public class Dog {} | 編譯期生成的.class字節(jié)碼文件,是代碼模板 | 編譯期生成,運行期被 JVM 加載 |
| Class 對象(類對象) | Dog.class | 類加載后 JVM 在內(nèi)存中生成的唯一對象,代表類本身 | 和類的生命周期綁定,類卸載時回收 |
| 實例對象 | new Dog() | 基于類模板創(chuàng)建的具體實例,堆內(nèi)存中的對象 | 隨 new 創(chuàng)建,無引用時被 GC 回收 |
Class 對象的核心特性(必記)
- 絕對單例性(有邊界):同一個類,在同一個 JVM 實例 + 同一個類加載器下,只會生成一個 Class 對象,
==比對永遠返回 true。 - 全類型覆蓋:所有 Java 類型都有對應(yīng)的 Class 對象,包括:
- 只讀性:Class 對象封裝的類元數(shù)據(jù),運行期常規(guī)手段無法修改(僅可通過字節(jié)碼注入等特殊技術(shù)修改)。
- JVM 私有創(chuàng)建:Class 類的構(gòu)造方法是 private,只有 JVM 能創(chuàng)建 Class 對象,開發(fā)者無法通過
new Class()手動生成。
2.2.2 使用操作
關(guān)于泛型 Class<?>:
平時代碼中??吹?Class<?>。
這里的 ? 是通配符,表示未知的類型。因為我們在編寫反射通用代碼時,通常不知道要處理的具體是哪個類,使用 Class<?> 可以避免編譯器的未經(jīng)檢查警告(Unchecked Warning),保持代碼的泛型安全。
有三種常見方式,應(yīng)用場景各不相同:
// 1. Class.forName("全限定名"):最常用,適用于完全不知道類,只有類名字符串的情況(如加載數(shù)據(jù)庫驅(qū)動)
Class<?> clazz1 = Class.forName("com.example.User");
// 2. 類名.class:適用于在編譯前就已經(jīng)明確知道并且引入了該類的情況(常用于鎖對象、參數(shù)類型傳遞)
Class<?> clazz2 = User.class;
// 3. 對象.getClass():適用于已經(jīng)有了該類的實例對象,想反推它屬于哪個類
User user = new User();
Class<?> clazz3 = user.getClass();
一個類在同一個類加載器中,只有一個唯一的 Class 對象。無論通過哪種方式獲取,得到的都是同一個實例:
Class<User> c1 = User.class;
Class<?> c2 = Class.forName("com.example.User");
Class<?> c3 = new User().getClass();
System.out.println(c1 == c2); // true
System.out.println(c2 == c3); // true
這個唯一性保障了同步鎖(User.class 可作為同步監(jiān)視器)、類型比較(instanceof 底層用到 Class 對象)等基礎(chǔ)機制的正確運行。
2.3 反射的定義
反射(Reflection) 是 Java 語言提供的一種機制。它允許程序在運行時(Runtime):
動態(tài)探索:獲取任意一個類的所有內(nèi)部結(jié)構(gòu)(包含私有屬性、方法、構(gòu)造器、注解等)。
動態(tài)操作:動態(tài)調(diào)用任意一個對象的方法,修改其屬性。
理解編譯期和運行期,可以看我的另一篇博客:java的運行機制:編譯期、運行期和半編譯半解釋性
反射的本質(zhì):
先拿到目標類的 Class 對象(反射的入口)
通過 Class 對象獲取它的 “全息檔案”(動態(tài)探索)
通過檔案里的信息,去操作具體的實例對象(動態(tài)操作)

2.4 反射的核心 API 詳解
java.lang.reflect 包下提供了反射的核心組件:
Constructor:用來「造對象」,核心是newInstance,私有構(gòu)造器記得setAccessible(true)。Field:用來「改屬性」,核心是get和set,static 字段傳null。Method:用來「調(diào)方法」,核心是invoke,異常記得用getCause()解包。
它們就是三個類,和 String、ArrayList 一樣:
java.lang.reflect.Constructor<T> // 代表一個構(gòu)造方法 java.lang.reflect.Field // 代表一個成員變量(字段) java.lang.reflect.Method // 代表一個成員方法
當你調(diào)用 clazz.getConstructor() 時,JVM 并不是返回了一團神秘的東西,而是老老實實地 new 了一個 Constructor 對象給你。
同理,getDeclaredField() 返回一個 Field 實例,getMethod() 返回一個 Method 實例。
Java 虛擬機(JVM)在加載一個類時,不僅會把類本身封裝成一個 Class 對象,還會把這個類里面的每一個成員(構(gòu)造器、字段、方法)都解析并封裝成對應(yīng)的 Constructor、Field 和 Method 對象。
真正的構(gòu)造器、字段、方法的定義,在 JVM 底層是以 C++ 數(shù)據(jù)結(jié)構(gòu)的形式,存放在元空間(Metaspace) 的。
這些原始數(shù)據(jù)對 Java 程序是不可見的。
Constructor、Field、Method 的作用,就是把那些黑盒的底層元數(shù)據(jù),包裝成 Java 對象,讓你能通過調(diào)用這些對象的方法,去間接操縱底層的實際內(nèi)容。
在常規(guī)開發(fā)中,我們是這樣想的:“我要調(diào)用 user 對象的 getName 方法”。
在反射開發(fā)中,思維模型要倒轉(zhuǎn)過來:“我拿到了一個名為 getName 的 Method 對象,我要讓這個 Method 對象去作用于 user 實例”。
2.4.1Constructor(構(gòu)造器對象)
用于動態(tài)創(chuàng)建實例。
getConstructor(Class<?>... parameterTypes):獲取 public 構(gòu)造器。getDeclaredConstructor(...):獲取所有聲明的構(gòu)造器(包含 private)。newInstance(Object... initargs):執(zhí)行構(gòu)造器創(chuàng)建對象。setAccessible(true): 突破 private 限制
**注意:**當我們使用單例模式或某些隱藏了實例創(chuàng)建邏輯的框架時,類的構(gòu)造器往往是 private 的。此時,必須配合 setAccessible(true) 使用,否則會拋出 IllegalAccessException。
public class User {
private String name;
// 私有構(gòu)造器
private User(String name) {
this.name = name;
}
}
// 反射調(diào)用測試
Class<?> clazz = User.class;
// 1. 獲取私有構(gòu)造器 (注意:這里用 getDeclaredConstructor)
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
// 2. 打破封裝,允許訪問私有成員
constructor.setAccessible(true);
// 3. 動態(tài)實例化對象
Object userInstance = constructor.newInstance("ReflectUser");
2.4.2Field(字段對象)
用于動態(tài)讀取和修改屬性。
getDeclaredField(String name):獲取當前類聲明的指定名稱的字段(無論什么訪問修飾符)。setAccessible(true):反射的破壁者。操作 private 字段前必須調(diào)用此方法打破封裝。get(Object obj):獲取指定對象obj中該字段的值。set(Object obj, Object value):將指定對象obj中該字段的值修改為value。
注意:
- 靜態(tài)(static)字段操作:由于靜態(tài)字段屬于類級別而不是對象級別,所以在調(diào)用
get()或set()時,傳入的對象實例可以直接寫null。 - 類型安全檢查:如果
set時傳入的數(shù)據(jù)類型與字段實際類型不匹配,會引發(fā)IllegalArgumentException。
// 假設(shè)前面的 userInstance 已經(jīng)創(chuàng)建
Field nameField = clazz.getDeclaredField("name");
// 打破私有屬性的封裝
nameField.setAccessible(true);
// 讀取屬性值
String currentName = (String) nameField.get(userInstance);
System.out.println("修改前: " + currentName); // 輸出: ReflectUser
// 修改屬性值
nameField.set(userInstance, "NewName");
System.out.println("修改后: " + nameField.get(userInstance)); // 輸出: NewName
2.4.3Method(方法對象)
用于動態(tài)執(zhí)行方法。
getDeclaredMethod(String name, Class<?>... parameterTypes):根據(jù)方法名和參數(shù)列表獲取當前類聲明的方法。invoke(Object obj, Object... args):核心執(zhí)行方法。obj:要執(zhí)行該方法的具體對象實例。args:方法運行所需的實際參數(shù)。
注意:
- 靜態(tài)方法調(diào)用:如果反射調(diào)用的方法是
static的,invoke的第一個參數(shù)obj傳null即可(因為靜態(tài)方法不依賴于具體的對象實例)。 - 異常解包機制:當被反射調(diào)用的目標方法內(nèi)部拋出異常時,反射機制會將其包裝成一個
InvocationTargetException拋出。要獲取目標方法真實拋出的業(yè)務(wù)異常,必須調(diào)用exception.getCause()進行解包。
代碼:
public class Calculator {
private int add(int a, int b) {
return a + b;
}
public static void staticMethod() {
System.out.println("靜態(tài)方法被調(diào)用");
}
}
Class<?> calcClass = Calculator.class;
Object calcInstance = calcClass.getDeclaredConstructor().newInstance();
// 1. 調(diào)用私有實例方法
Method addMethod = calcClass.getDeclaredMethod("add", int.class, int.class);
addMethod.setAccessible(true);
// obj 傳實例對象,后面?zhèn)鲗崊?
Object result = addMethod.invoke(calcInstance, 10, 20);
System.out.println("10 + 20 = " + result);
// 2. 調(diào)用靜態(tài)方法
Method staticMethod = calcClass.getDeclaredMethod("staticMethod");
// 靜態(tài)方法不需要實例,obj 傳 null
staticMethod.invoke(null);
三、注解
如果說反射是底層執(zhí)行的引擎,那么注解就是指揮引擎運行的“路標”。
3.1 為什么要使用注解?
在早期 Spring 或 Struts,軟編碼大量依賴龐大且冗長的 XML 配置文件。
這被稱為“XML 地獄”。開發(fā)人員不得不在 Java 代碼和 XML 文件之間反復(fù)橫跳。
案例:聲明一個 UserService,并給它注入一個 Dao 對象。
Java 代碼本身極其干凈(但毫無線索):
public class UserService {
private UserDao userDao;
// 必須寫又長又啰嗦的 setter 方法供 XML 調(diào)用
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
對應(yīng)的 XML 配置文件(災(zāi)難的開始):
<!-- applicationContext.xml 里面可能堆積了成百上千個這樣的標簽 -->
<bean id="userDao" class="com.example.dao.UserDaoImpl" />
<bean id="userService" class="com.example.service.UserService">
<!-- 靠全限定類名和字符串來匹配,極易寫錯 -->
<property name="userDao" ref="userDao" />
</bean>
痛點解析:
- 反復(fù)橫跳:開發(fā)者看代碼時,不知道這個類被誰用、怎么配的;看 XML 時,又不知道這個類的具體邏輯。必須在
.java和.xml文件之間瘋狂切換。 - 重構(gòu)災(zāi)難:一旦修改了類名或包名,如果在 XML 里忘了同步修改(由于是純字符串,編譯器不會報錯),程序在運行時就會直接崩潰。
- 極度臃腫:大型項目的 XML 文件動輒幾千行,甚至需要拆分成幾十個 XML 互相引入,維護成本極高。
注解的出現(xiàn)解決了這個問題:它允許將元數(shù)據(jù)(Metadata)直接標記在代碼本身上(類、方法、字段上),做到了配置與代碼的高內(nèi)聚,大大提升了可讀性和開發(fā)效率。
// @Service 路標:告訴 Spring 引擎,這是一個業(yè)務(wù)邏輯類,請幫我實例化
@Service
public class UserService {
// @Autowired 路標:告訴 Spring 引擎,請把我需要的 UserDao 自動拿過來給我
@Autowired
private UserDao userDao;
// 連 setter 方法都不需要寫了!
}
一個極其容易陷入誤區(qū)的點:很多人認為 @Autowired 能自動注入對象,是因為這個注解里寫了注入的代碼。
其實不是,因為:“注解本身不包含任何邏輯,它沒有任何行為能力。”
注意:注解在本質(zhì)上只是一個標記、一個標簽。就好比你在超市商品上貼了一個“打八折”的紅色貼紙(注解)。貼紙本身不能改變商品的價格,真正改變價格的,是收銀員(反射機制)看到了這個貼紙,然后按照收銀機里的邏輯給你減錢。
3.2 注解是什么
3.2.1 基本概念
注解(Annotation),也叫元數(shù)據(jù)(Metadata)。簡單來說,它就是一種代碼級別的說明和標簽。
它是 JDK 1.5 引入的特性,與類、接口、枚舉屬于同一個層次。它可以像“便利貼”一樣,貼在包、類、字段、方法、局部變量、方法參數(shù)等的前面。
那么,貼上這些“便利貼”到底有什么用呢?總體來說,注解的作用可以分為三大類:
- 編譯檢查:通過注解讓編譯器實現(xiàn)基本的編譯檢查(比如告訴編譯器這個方法是重寫的,幫我盯緊點)。
- 編寫文檔:配合工具(如 javadoc),通過代碼里標識的注解生成 API 文檔。
- 代碼分析與運行(核心):在運行期,框架可以通過反射機制讀取這些注解,進而動態(tài)地改變程序的運行邏輯。
JDK 提供的最常用的三個基礎(chǔ)注解,它們主要負責編譯階段的檢查與提示。
@Override: 標記在成員方法上,用于標識當前方法是重寫父類(父接口)方法,編譯器在對該方法進行編譯時會檢查是否符合重寫規(guī)則,如果不符合,編譯報錯。
@Deprecated: 用于標記當前類、成員變量、成員方法或者構(gòu)造方法過時如果開發(fā)者調(diào)用了被標記為過時的方法,編譯器在編譯期進行警告。
@SuppressWarnings: 壓制警告注解,可放置在類和方法上,該注解的作用是阻止編譯器發(fā)出某些警告信息。
1. JDK自帶三種注解
@Override:這個注解只能標記在方法上,用來告訴編譯器:“我這個方法是重寫父類(或接口)的,請幫我檢查一下方法名和參數(shù)對不對”。public class Animal { public void speak() { System.out.println("動物發(fā)聲"); } } public class Dog extends Animal { // 加上 @Override 后,如果下面方法名錯寫成 spaek(),編譯器會直接標紅報錯! // 如果不加注解,寫成 spaek() 編譯器只會認為你寫了一個新方法,導(dǎo)致潛藏 bug。 @Override public void speak() { System.out.println("汪汪汪"); } }@Deprecated:隨著軟件迭代,有些老方法或老類存在缺陷或性能問題,不推薦大家繼續(xù)使用了,但為了向前兼容又不能直接刪除。這時就可以用它。public class StringUtils { // 標記為已過時。如果你在其他地方調(diào)用這個方法,IDEA 會給它劃上一條刪除線 @Deprecated public static void oldFormat() { System.out.println("這個方法太老了,不安全,別用了!"); } public static void newFormat() { System.out.println("請使用全新的格式化方法"); } }@SuppressWarnings:有時候我們的代碼會產(chǎn)生一些黃色的警告(比如未使用的變量、未經(jīng)檢查的泛型轉(zhuǎn)換等)。如果你確信代碼沒問題,可以用它來讓編譯器“閉嘴”。public class Test { // 壓制“未使用”和“未檢查的強轉(zhuǎn)”警告,讓代碼看起來干干凈凈 @SuppressWarnings({"unused", "unchecked"}) public void doSomething() { int a = 10; // 雖然沒被使用,但加了注解后,IDEA 不會再報黃色的警告 List list = new ArrayList(); list.add("Hello"); // 泛型強轉(zhuǎn)警告也會被壓制 List<String> strList = (List<String>) list; } }
2. 現(xiàn)代 Web 框架中的注解
JDK 內(nèi)置注解只是牛刀小試。
注解真正的威力,在于結(jié)合反射機制,徹底改變了現(xiàn)代 Java 后端(如 Spring MVC)的開發(fā)模式,改進了老版本的了讓人頭疼的“XML 配置文件”。
在現(xiàn)代開發(fā)中,注解就像是指揮框架運行的路標。
// @RestController:告訴 Spring,這是一個處理 Web 請求的類,幫我把它變成一個 Bean 放到容器里。
@RestController
// @RequestMapping:定義這個類處理所有以 "/api/users" 開頭的請求。
@RequestMapping("/api/users")
public class UserController {
// @Autowired:自動裝配。告訴 Spring,請幫我在內(nèi)存里找一個 UserService 對象,強行塞到這個字段里,不用我自己 new 了!
@Autowired
private UserService userService;
// @GetMapping:處理 HTTP 的 GET 請求。如果瀏覽器訪問 "/api/users/1",就會進到這里。
// @PathVariable:告訴框架,把 URL 里的 "{id}" 摳出來,賦值給入?yún)?userId。
@GetMapping("/{id}")
public User getUserById(@PathVariable("id") Long userId) {
return userService.findById(userId);
}
}
在這個例子中,代碼極其簡潔,沒有任何多余的配置代碼。
這全都歸功于注解:開發(fā)者負責貼標簽,Spring 框架在底層負責用反射讀取標簽并執(zhí)行對應(yīng)的操作。
3.3 注解的本質(zhì)
注解并不是什么憑空出現(xiàn)的全新數(shù)據(jù)結(jié)構(gòu),它的本質(zhì),就是一個普普通通的 Java 接口(Interface)。
我們可以通過代碼反編譯,來拆穿編譯器的這層“語法糖”。
假設(shè)我們自定義了一個極其簡單的注解:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
// 【步驟一:定義注解】
@Retention(RetentionPolicy.RUNTIME) // 關(guān)鍵:告訴JVM在運行時保留這個注解
public @interface MyAnnotation {
String value() default "Hello";
int age();
}
// 【步驟二:使用注解(貼標簽)】
@MyAnnotation(value = "張三", age = 18) // 貼在類上
public class Student {
@MyAnnotation(age = 20) // 貼在方法上(value有默認值,可以省略)
public void study() {
System.out.println("好好學(xué)習(xí)!");
}
}
// 【步驟三:解析注解(讀標簽)】
public class Main {
public static void main(String[] args) throws Exception {
// 1. 獲取類上的注解數(shù)據(jù)
MyAnnotation classAnno = Student.class.getAnnotation(MyAnnotation.class);
if (classAnno != null) {
System.out.println("類上的注解 -> 值: " + classAnno.value() + ", 年齡: " + classAnno.age());
}
// 2. 獲取方法上的注解數(shù)據(jù)
Method method = Student.class.getMethod("study");
MyAnnotation methodAnno = method.getAnnotation(MyAnnotation.class);
if (methodAnno != null) {
System.out.println("方法上的注解 -> 值: " + methodAnno.value() + ", 年齡: " + methodAnno.age());
}
}
}
賦值:在 Student 類中寫下 @MyAnnotation(value = "張三", age = 18) 時,其實就是給注解的屬性賦了值。
取值:在 Main 類中,通過 getAnnotation() 方法,就可以把剛剛賦進去的 "張三" 和 18 動態(tài)拿出來。
本質(zhì):注解本身是不包含任何邏輯的,它僅僅是用來“存數(shù)據(jù)”的標簽。必須配合反射把數(shù)據(jù)取出來,它才有意義。
當我們使用 javac 將其編譯成 .class 字節(jié)碼文件,然后再使用 javap -c 命令對其進行反編譯后,你看到的真實代碼形態(tài)是這樣的:
// 是一個接口
public interface MyAnnotation extends java.lang.annotation.Annotation {
// 所謂的屬性,變成了抽象方法
public abstract String value();
public abstract int age();
}
解析:
- 隱式繼承:所有的注解類,在編譯后都會自動繼承
java.lang.annotation.Annotation接口。 - **
@interface**:僅僅是 Java 編譯器提供的一個語法糖,作用是告訴編譯器:“嘿,請幫我把這個類編譯成繼承了Annotation的接口”。
3.4 為什么注解里的“屬性”要帶括號
注解聲明屬性時,一般要在后面加上一對括號,比如 String value(); 而不是 String value;。
現(xiàn)在知道了它的本質(zhì)是接口,一切就豁然開朗了:
- 在 Java 的接口中,不允許定義普通的成員變量(只能定義
public static final的常量)。 - 為了讓接口能夠“攜帶”數(shù)據(jù),Java 的設(shè)計者只能利用抽象方法來模擬屬性。
- 當你寫下
String value();時,你不是在定義一個變量,而是在定義一個有返回值的方法。當你在代碼中使用@MyAnnotation(value = "Test")時,其實是在告訴 JVM:“待會兒如果有人調(diào)用value()這個方法,你就給他返回"Test"這個字符串”。
3.5 既然是接口,那是誰實例化了它
我們知道,接口是不能直接 new 的。既然注解是接口,那么當我們在運行期通過反射調(diào)用 clazz.getAnnotation(MyAnnotation.class) 時,拿到的那個注解對象,究竟是個什么東西?是誰實現(xiàn)了這個接口?
答案是:JDK 動態(tài)代理。
當我們在代碼里寫下 @MyAnnotation 并通過反射去獲取它時,JVM 在底層做了一系列非常隱蔽的操作:
- 解析常量池:JVM 會讀取 .class 文件常量池中關(guān)于這個注解的配置信息。
- 生成代理類:JVM 在內(nèi)存中動態(tài)生成了一個實現(xiàn)了
MyAnnotation接口的代理類(類似于$Proxy1)。 - 攔截方法調(diào)用:這個代理類內(nèi)部持有一個
AnnotationInvocationHandler(注解調(diào)用處理器)。這個處理器內(nèi)部維護了一個Map<String, Object>,里面存儲了你寫在代碼里的屬性名和屬性值(比如key="value",value="Test")。 - 返回結(jié)果:當你調(diào)用
myAnnotation.value()時,實際上是被代理類攔截了。代理類會去那個 Map 里面,根據(jù)方法名(“value”)取出對應(yīng)的值,并返回給你。
所以整體流程是這樣的:
- 編寫期:你寫下
@interface,看起來像個標簽。 - 編譯期:編譯器把它轉(zhuǎn)成
interface extends Annotation,屬性變方法。 - 運行期:JVM 通過 動態(tài)代理 生成這個接口的實現(xiàn)類對象,并通過反射把你在代碼里配的值,存進代理對象內(nèi)部的 Map 里。最后,你調(diào)用方法獲取值。
簡單總結(jié):
表象:反射拿到的并不是真正的注解實現(xiàn)類,而是 JVM 在運行期臨時生成的一個動態(tài)代理對象(Proxy)。
核心:這個代理對象內(nèi)部持有一個名為
AnnotationInvocationHandler的調(diào)用處理器。機制:這個處理器里面維護了一個
Map,里面存著我們寫注解時賦的值(比如age=18)。當我們通過反射調(diào)用注解的屬性方法時,其實是被這個處理器攔截了,它直接從 Map 里查出對應(yīng)的值返回給我們。所以調(diào)用注解方法,本質(zhì)上是在查 Map 字典!

3.6 自定義注解的使用舉例
“元注解”就是用來標注注解的注解,它們定義了你的自定義注解的作用域和生命周期。
@Retention(保留策略)定義注解存活的時間,由
RetentionPolicy枚舉控制:SOURCE:只在源碼中存在,編譯后被抹除(如@Override,只是給編譯器看的)。CLASS:保留到字節(jié)碼文件中,但 JVM 運行時不加載。RUNTIME:最重要! 保留到運行時,可以通過反射讀取。所有需要配合反射使用的注解,必須設(shè)置為 RUNTIME。
@Target(作用目標)
定義注解可以貼在誰身上,由 ElementType 枚舉控制:
TYPE:類、接口。FIELD:字段屬性。METHOD:方法。PARAMETER:方法參數(shù)。
代碼測試:
import java.lang.annotation.*;
// 元注解:指定注解的保留策略為運行時(只有這個策略才能被反射讀?。?
@Retention(RetentionPolicy.RUNTIME)
// 元注解:指定注解可以標注在字段和方法上
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface MyAnnotation {
// 注解的屬性,本質(zhì)上是接口的抽象方法
boolean required() default true;
}
// 類上的注解
@MyAnnotation("類注解")
public class User {
// 字段上的注解
@MyAnnotation("字段注解")
private String name;
// 方法上的注解
@MyAnnotation("方法注解")
public void sayHi() {}
}
// 反射讀取注解
public class AnnotationReadDemo {
public static void main(String[] args) throws Exception {
Class<User> userClass = User.class;
// 1. 讀取類上的注解
if (userClass.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation classAnnotation = userClass.getAnnotation(MyAnnotation.class);
System.out.println("類注解值:" + classAnnotation.value());
}
// 2. 讀取字段上的注解
Field nameField = userClass.getDeclaredField("name");
if (nameField.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation fieldAnnotation = nameField.getAnnotation(MyAnnotation.class);
System.out.println("字段注解值:" + fieldAnnotation.value());
}
// 3. 讀取方法上的注解
Method sayHiMethod = userClass.getDeclaredMethod("sayHi");
if (sayHiMethod.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation methodAnnotation = sayHiMethod.getAnnotation(MyAnnotation.class);
System.out.println("方法注解值:" + methodAnnotation.value());
}
}
}
四、 反射與注解的結(jié)合
只有反射,只能盲目地操作類;
只有注解,它只是一堆不會生效的死文本;
當反射去讀取注解時,兩個機制就產(chǎn)生了非常好的效果。
4.1 結(jié)合的底層原理
怎么讓注解生效?原理其實極其樸素,歸納起來就是“一套 API,三個步驟”。
在程序運行時,框架的底層其實就在無限循環(huán)地做下面這三件事:
- 全面掃描(找位置):利用反射 API 獲取目標類的所有結(jié)構(gòu)(
Class.getDeclaredFields()、getDeclaredMethods()等)。 - 精準定位(看路標):遍歷這些結(jié)構(gòu),調(diào)用
isAnnotationPresent(TargetAnnotation.class)方法,像雷達一樣探測上面有沒有貼特定的注解。 - 強行介入(做邏輯):一旦探測到注解(返回
true),就利用getAnnotation()把注解里配置的數(shù)據(jù)(如value)讀出來。然后根據(jù)業(yè)務(wù)需求,利用反射 API(如Field.set()或Method.invoke())打破封裝,執(zhí)行特殊的邏輯。

4.2 Spring Boot 中的經(jīng)典應(yīng)用:依賴注入
在 Spring 時代之前,我們寫代碼最大的痛苦就是無休止地 new 對象。而現(xiàn)在,我們在開發(fā) Spring MVC 或 Spring Boot 項目時,極其常用 @Autowired 注解來實現(xiàn)依賴注入(Dependency Injection)。
當我們寫下這行極簡的代碼時:
@Autowired private UserService userService;
表面上看: 我們只是聲明了一個私有變量,連對象都沒創(chuàng)建,更沒有寫 setter 方法。
實際上: Spring 容器在后臺偷偷把一切都打理好了,簡單來說步驟如下:

流程如下:

4.3 代碼實戰(zhàn)
結(jié)合我們上面學(xué)到的所有知識,寫一個最簡陋的 DI 容器來展示這一過程:
import java.lang.reflect.Field;
// 1. 定義測試類
class OrderService {
public void pay() { System.out.println("訂單支付成功!"); }
}
class OrderController {
@AutoInject // 使用我們剛才自定義的注解
private OrderService orderService;
public void doOrder() {
orderService.pay();
}
}
// 2. 手寫迷你容器
public class MiniDiContainer {
public static void main(String[] args) throws Exception {
// 第一步:反射實例化目標對象 (模擬框架啟動)
Class<?> controllerClass = OrderController.class;
Object controllerObj = controllerClass.getDeclaredConstructor().newInstance();
// 第二步:依賴注入的核心邏輯
Field[] fields = controllerClass.getDeclaredFields();
for (Field field : fields) {
// 檢查字段上是否有 @AutoInject 注解
if (field.isAnnotationPresent(AutoInject.class)) {
// 拿到需要注入的類型 (這里是 OrderService)
Class<?> fieldType = field.getType();
// 實例化依賴的對象
Object dependencyObj = fieldType.getDeclaredConstructor().newInstance();
// 暴力注入!
field.setAccessible(true);
field.set(controllerObj, dependencyObj);
System.out.println("完成自動注入: " + field.getName());
}
}
// 第三步:測試調(diào)用
OrderController controller = (OrderController) controllerObj;
controller.doOrder(); // 如果不報錯并打印成功,說明注入生效了!
}
}
總結(jié)
- 反射:讓程序在運行時動態(tài)探索類的結(jié)構(gòu),并操作對象,實現(xiàn)軟編碼與高度可擴展的框架設(shè)計。
- 注解:提供元數(shù)據(jù)標記,告訴框架“做什么”,自身不具邏輯,通過反射才能生效。
- 結(jié)合應(yīng)用:現(xiàn)代框架(如 Spring)靠反射讀取注解,實現(xiàn)依賴注入、事務(wù)控制、路由映射等“開箱即用”的功能。
核心理念:反射是執(zhí)行引擎,注解是指令標記,兩者結(jié)合才成就現(xiàn)代 Java 框架的便捷。
到此這篇關(guān)于Java反射與注解詳細講解的文章就介紹到這了,更多相關(guān)Java反射與注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java設(shè)置httponly?cookie的實現(xiàn)示例
本文主要介紹了Java設(shè)置httponly?cookie的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Mybatis之解決collection一對多問題(顯示的結(jié)果沒有整合到一起)
這篇文章主要介紹了Mybatis之解決collection一對多問題(顯示的結(jié)果沒有整合到一起),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Java文件下載ZIP報錯:Out of Memory的問題排查
本文主要介紹了Java項目中下載大文件(超過2G的ZIP文件)時出現(xiàn)內(nèi)存溢出(OutOfMemory:JavaHeapSpace)的問題,具有一定的參考價值,感興趣的可以了解一下2025-01-01

