反射是 Java 動態性的基石,讓程式在**執行期**檢查、修改類結構與行為。框架 (Spring、Hibernate、MyBatis)、註解處理器、動態代理、熱插拔外掛均建立於此。本文系統整理反射核心 API、常見操作模式、效能考量與安全風險。
JVM 為每個載入的類建立一個 java.lang.Class<T> 實例,包含該類的完整元數據:
Field):名稱、類型、修飾符、註解Method):名稱、回傳類型、參數列表、修飾符、註解、異常宣告Constructor):參數列表、修飾符、註解// 1. 類字面常量 (編譯期已知類)
Class<User> clazz1 = User.class;
// 2. 實例 .getClass() (運行期物件)
User user = new User();
Class<?> clazz2 = user.getClass();
// 3. 字串全限定名動態載入 (最常用於框架)
Class<?> clazz3 = Class.forName("com.example.User"); // 可能拋 ClassNotFoundException
// 驗證三者同一
assert clazz1 == clazz2 && clazz2 == clazz3; // JVM 保證同一類載入器下單例
// 無參建構子
User user = (User) clazz.getDeclaredConstructor().newInstance();
// 帶參建構子
Constructor<User> ctor = clazz.getDeclaredConstructor(String.class, int.class);
User user2 = ctor.newInstance("Alice", 25);
// 繞過 private 建構子 (單例模式破壞)
ctor.setAccessible(true);
Field nameField = clazz.getDeclaredField("name"); // 獲取宣告欄位 (含私有)
nameField.setAccessible(true); // 關鍵:暴力反射,繞過封裝檢查
nameField.set(user, "Bob"); // 相當於 user.name = "Bob"
String name = (String) nameField.get(user); // 讀取
// 靜態欄位
Field staticField = clazz.getDeclaredField("COUNT");
staticField.setAccessible(true);
staticField.set(null, 100); // 靜態欄位傳 null
// 實例方法
Method toString = clazz.getDeclaredMethod("toString");
toString.setAccessible(true);
String result = (String) toString.invoke(user); // user.toString()
// 帶參方法
Method setAge = clazz.getDeclaredMethod("setAge", int.class);
setAge.invoke(user, 30); // user.setAge(30)
// 靜態方法
Method staticMethod = clazz.getDeclaredMethod("staticFactory", String.class);
staticMethod.invoke(null, "param"); // 靜態方法傳 null
// 所有 public 方法 (含繼承)
Method[] pubMethods = clazz.getMethods();
// 所有宣告方法 (不含繼承、含私有)
Method[] declaredMethods = clazz.getDeclaredMethods();
// 註解處理
if (clazz.isAnnotationPresent(Entity.class)) {
Entity entity = clazz.getAnnotation(Entity.class);
System.out.println("Table: " + entity.name());
}
// 泛型資訊 (ParameterizedType)
Field listField = clazz.getDeclaredField("items");
Type genericType = listField.getGenericType(); // List
if (genericType instanceof ParameterizedType) {
Type[] args = ((ParameterizedType) genericType).getActualTypeArguments();
System.out.println("泛型參數: " + args[0]); // class java.lang.String
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 方法呼叫慢 (約 10-50x 直接呼叫) | JIT 難優化、存取檢查、參數裝箱/陣列分配 | 1. 緩存 Method/Field/Constructor 物件 (不可變,線程安全) 2. setAccessible(true) 一次性關閉檢查3. 高頻熱點改用 MethodHandle / VarHandle (JDK 7/9+) |
| 頻繁 newInstance 慢 | 建構子驗證、安全檢查 | 緩存 Constructor,或用 Objenesis 庫繞過建構子直接分配記憶體 |
| 類載入開銷 | Class.forName 觸發初始化 (<clinit>) | 預熱載入、延遲載入、ClassLoader.loadClass 不初始化 |
InvocationHandler + 反射呼叫目標方法,實現 AOP。@JsonProperty)。@Autowired、@Value、建構子注入參數解析。@Test 方法、執行生命週期回調。ClassLoader 載入外部 JAR,反射實例化擴充點。setAccessible(true) 可存取/修改 private 狀態,導致不變性失效 (如 String 內部 value 陣列被修改)。SecurityManager 禁止 suppressAccessChecks 權限;JDK 17+ 移除 SecurityManager,改用 --add-opens 模組系統控制。module-info.java 未 opens 封包時,反射存取非 public 成員會拋 InaccessibleObjectException。解決:--add-opens java.base/java.lang=ALL-UNNAMED 或模組宣告 opens com.yourpkg to java.base。// MethodHandle: 直接方法引用,JIT 可內聯,效能接近直接呼叫
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(User.class, "getName", MethodType.methodType(String.class));
String name = (String) mh.invokeExact(user); // 無裝箱、無陣列、可內聯
// VarHandle: 直接欄位/陣列元素存取,支援原子操作 (CAS)
VarHandle nameHandle = MethodHandles.lookup()
.in(User.class)
.findVarHandle(User.class, "name", String.class);
String name = (String) nameHandle.get(user); // 讀取
nameHandle.set(user, "New"); // 寫入
nameHandle.compareAndSet(user, "Old", "New"); // CAS
建議:框架開發、高頻熱點路徑優先考慮 MethodHandle/VarHandle;一般業務動態化需求反射足夠且可讀性更好。