本篇属于「知其然更知其所以然」的一章。反射、注解、动态代理这三样东西,日常写业务代码几乎碰不到,但只要你点开 Spring 的 、MyBatis 的 、Jackson 的 ,就会发现它们无处不在。理解这三块基石,你读框架源码时就不会再迷路,也能在真正需要"通用能力"的场景里写出靠谱的底层工具,而不是把反射当成万能胶乱抹一气。阅读建议:跟着代码敲一遍,尤其是第六节的 DI 容器和第七节的代理切面,跑通之后很多面试题会自然解开。
写在前面:为什么这三个东西要一起讲
很多教材把反射、注解、动态代理拆成三章,结果读者学完只记住了三套 API,串不起来。实际上它们是一条流水线上的三道工序:注解负责"打标记"(声明元数据),反射负责"读标记 + 执行动作"(运行期解析与调用),动态代理负责"把动作织入调用过程"(无侵入增强)。
Spring 里 @Transactional 就是一个典型闭环:你在方法上打一个注解(注解),Spring 容器启动时用反射扫描到它并判读事务属性(反射),然后为目标对象生成一个代理对象,让每次方法调用先经过事务拦截器(动态代理)。少了任何一环,这个能力都实现不了。
所以本篇的顺序是:先把反射讲透(它是唯一真正"做事"的那个),再讲注解(给反射提供输入),最后讲代理(把反射包装成优雅的形态)。
一、反射是什么:编译期确定 vs 运行期确定
1.1 从一行最普通的代码说起
User user = new User();
user.setName("张三");
这行代码在编译期就已经完全确定了:编译器知道 User 有 setName 方法、参数是 String、返回值是 void。生成的字节码里是一条 invokevirtual 指令,指向常量池中一个明确的符号引用,JVM 加载类时把它解析成直接引用,之后每次调用都是一次近乎"硬编码"的跳转。
反射把这个过程推迟到了运行期:你手上只有一个字符串 "com.example.User" 和另一个字符串 "setName",要等程序跑起来才知道有没有这个类、有没有这个方法、参数类型对不对。
// 编译期:不知道类名、方法名、参数类型,全部来自运行期的字符串
Class<?> clazz = Class.forName(className); // 来自配置文件或注解
Object obj = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getMethod("setName", String.class);
m.invoke(obj, "张三"); // 运行期才做参数校验与分派
这种"推迟"带来了一个关键能力:代码可以操作它编译时不认识的类型。这正是框架存在的意义——Spring 编译时不认识你的 @Service 类,MyBatis 编译时不认识你的 UserMapper,Jackson 编译时不认识你要反序列化的任何 POJO。
1.2 框架为什么到处都在用反射
| 框架 | 反射用在哪 | 典型类 |
|---|---|---|
| Spring | 扫描类路径、实例化 Bean、注入字段、调用初始化方法 | ClassPathScanningCandidateComponentProvider、AutowiredAnnotationBeanPostProcessor |
| MyBatis | 接口方法 → SQL 映射、结果集 → 对象属性填充 | MapperProxy、DefaultResultSetHandler |
| Jackson / Fastjson | 属性名 ↔ 字段名映射、getter/setter 调用 | BeanSerializer、BeanDeserializer |
| JUnit | 发现 @Test 方法并调用 |
BlockJUnit4ClassRunner |
| Dubbo / Feign | 接口方法 → 远程调用参数拼装 | InvokerInvocationHandler |
共同点是:框架代码必须在不知道用户类型的情况下,完成"创建对象 / 读写属性 / 调用方法"这三件事。而这三件事恰好就是反射的 Constructor、Field、Method 三个核心类。
1.3 反射的代价与边界
反射不是银弹,它有三笔明确的账单:性能开销(访问检查、JNI 调用、无法内联)、安全与封装风险( 可以撬开 private)、可维护性损失(字符串硬编码、编译期失去类型保护、IDE 无法帮你重构)。
合理的边界可以概括成三条:
- 写通用基础设施时用反射(框架、ORM、序列化、测试框架、插件系统)。
- 写业务逻辑时不要用反射。能用
if解决的不要用Class.forName;能用接口解决的不要用Method.invoke。 - 热路径上必须缓存反射对象。
Class.forName/getMethod这些查找操作的成本远高于invoke本身,把Method对象缓存成静态常量,往往是最大的一次优化。
1.4 反射的三种典型误用
- 用反射代替多态:拿到
type字符串后if ("A".equals(type))再Method.invoke,本质上是在用字符串重新实现一遍switch。正确做法是定义一个接口 + 多个实现类 + 一个Map<String, Handler>,把运行期分派交给 JVM 的虚方法表。 - 用反射绕过不合理的设计:某个字段是
private、没有 setter,于是setAccessible(true)强改。这不是在解决问题,是在掩盖问题——正确做法是推动接口暴露,或者用包级可见的测试钩子。 - 在循环里做反射查找:
for (...) { Method m = clazz.getMethod("xxx"); m.invoke(obj); },查找成本被放大 N 倍。把getMethod提到循环外,通常一个改动就能快一个数量级。
二、Class 对象:反射的入口
2.1 Class 对象的本质
JVM 每加载一个类(或接口、数组、基本类型、void),都会在堆(JDK 8 之后是元空间管理的类元数据 + 堆中的 java.lang.Class 实例)中为其创建一个唯一的 Class 对象。这个对象就是该类在运行期的"元数据句柄",反射的一切都从这里出发。
关键点:一个类在一个 ClassLoader 下只有一个 Class 对象,但同一个类被两个不同的 ClassLoader 加载,会产生两个不同的 Class 对象,且 a.equals(b) 为 false、a.isAssignableFrom(b) 也为 false——这是热部署、插件隔离、以及 ClassCastException 里那句令人抓狂的 Xxx cannot be cast to Xxx 的根本原因。
2.2 三种获取方式对比
public class ClassObtainDemo {
static class Holder {
static { System.out.println("Holder 静态块执行"); }
public static final String NAME = "holder";
}
public static void main(String[] args) throws Exception {
// 1. 类字面量:编译期已知类型,不触发初始化
Class<Holder> c1 = Holder.class;
System.out.println("方式1 拿到: " + c1.getName());
// 2. 对象.getClass():需要已有实例,实例创建时必然已完成初始化
Class<? extends Holder> c2 = new Holder().getClass();
// 3. Class.forName:运行期按全限定名加载,默认触发静态初始化
Class<?> c3 = Class.forName("ClassObtainDemo$Holder");
System.out.println(c1 == c2 && c2 == c3); // true,同一个 Class 对象
}
}
| 获取方式 | 语法 | 是否加载类 | 是否触发静态初始化 | 编译期类型安全 | 典型场景 |
|---|---|---|---|---|---|
| 类字面量 | User.class |
否(仅解析符号引用,首次主动使用时才加载) | 不触发(属于被动引用) | 是,可写 Class<User> |
已知类型时首选,如 String.class、int.class |
| 对象实例 | obj.getClass() |
已加载 | 已触发(否则拿不到实例) | 返回 Class<? extends T> |
已有对象,需要动态判断实际类型 |
| 全限定名 | Class.forName("x.y.Z") |
是(用调用者的 ClassLoader) | 默认触发(initialize=true) |
否,返回 Class<?> |
JDBC 驱动加载、插件、配置驱动 |
这里有个很容易踩的坑:Class.forName 的第二个参数控制是否初始化。Class.forName(name, false, loader) 只加载不初始化,常用于"只想探测类是否存在"的场景;而 ClassLoader.loadClass(name) 默认就是只加载不初始化。JDBC 4.0 之前的 Class.forName("com.mysql.cj.jdbc.Driver") 之所以必须写,正是因为要靠它触发 Driver 的静态注册块。
2.3 基本类型的 Class 与包装类
public class PrimitiveClassDemo {
public static void main(String[] args) {
System.out.println(int.class); // int
System.out.println(Integer.class); // class java.lang.Integer
System.out.println(Integer.TYPE); // int —— 等价于 int.class
System.out.println(int.class == Integer.TYPE); // true
System.out.println(int.class == Integer.class);// false,完全不同的两个 Class
System.out.println(void.class); // void
System.out.println(int[].class.getName()); // [I
System.out.println(String[].class.getName()); // [Ljava.lang.String;
System.out.println(int[][].class.getName()); // [[I
// 数组类型的 Class 可以用 Class.forName 按 JVM 内部名获取
try {
Class<?> arr = Class.forName("[Ljava.lang.String;");
System.out.println(arr == String[].class); // true
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
注意 int.class 和 Integer.class 是两个不同的 Class,这在反射调用方法时非常重要:setAge(int) 的参数类型必须写 int.class,写 Integer.class 会 NoSuchMethodException。另外 getName() 对数组返回 JVM 内部表示([I、[[Ljava.lang.String;),要得到可读形式请用 getCanonicalName() 或 getTypeName()。
2.4 Class 常用 API 速查
| API | 作用 | 注意点 |
|---|---|---|
getName() |
全限定名,数组返回 [I 形式 |
内部类是 a.b.Outer$Inner |
getSimpleName() |
去掉包名 | 匿名内部类返回空字符串 |
getCanonicalName() |
规范名,数组返回 int[] 形式 |
可能为 null |
getSuperclass() |
直接父类 | 接口、Object、int.class 返回 null |
getInterfaces() |
直接实现的接口数组 | 不含父类间接实现的接口 |
isInterface() / isEnum() / isArray() / isPrimitive() |
类型判定 | isEnum 对匿名枚举子类返回 false |
isAssignableFrom(Class) |
当前类是否能接收参数类型(A.isAssignableFrom(B) ≡ B 是 A 的子类型) |
判断"能否赋值"用它,别搞反方向 |
instanceof / isInstance(Object) |
判断对象是否属于某类型 | isInstance 是 instanceof 的动态版 |
getModifiers() |
返回修饰符位掩码 | 配合 Modifier.isPublic/Static/Final/Abstract 解析 |
getDeclaredClasses() / getEnclosingClass() |
内部类 / 外部类 | 用于扫描嵌套配置类 |
getComponentType() |
数组元素类型 | 非数组返回 null |
getClassLoader() |
加载该类的 ClassLoader | 可能为 null(Bootstrap 加载的类,如 String.class) |
isAssignableFrom 是最容易搞反的一个,记住这句口诀:A.isAssignableFrom(B) 为 true,表示"能把 B 的对象赋给 A 类型的引用",即 A 是 B 的父类或接口。
三、反射核心 API 实战
3.1 Constructor:创建对象的两种方式
public class ConstructorDemo {
public static class User {
private String name;
private int age;
public User() {}
private User(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return name + ":" + age; }
}
public static void main(String[] args) throws Exception {
Class<User> c = User.class;
// getConstructor 只能拿 public 构造;getDeclaredConstructor 可以拿任意修饰符
Constructor<User> pub = c.getConstructor();
Constructor<User> priv = c.getDeclaredConstructor(String.class, int.class);
priv.setAccessible(true); // 暴力访问,突破 private
User u1 = pub.newInstance();
User u2 = priv.newInstance("李四", 20);
System.out.println(u2); // 李四:20
// Class.newInstance() 自 JDK 9 起已废弃:它只调无参构造,且会吞掉受检异常
// User u3 = c.newInstance(); // Deprecated,不要用
}
}
newInstance 与 new 的区别:new 是编译期确定、字节码直接分配,Constructor.newInstance 要走访问检查、参数装箱、异常包装(目标构造抛出的异常会被包装成 InvocationTargetException,需要用 getCause() 取真实异常),并且无法被 JIT 内联。性能上后者慢一到两个数量级,但它能在运行期决定要 new 什么。
3.2 Field:读写字段
public class FieldDemo {
public static class Account {
private String owner = "默认";
private static int total = 0;
private final String id = "A001"; // final 实例字段
public static final String TYPE = "ACCOUNT";
}
public static void main(String[] args) throws Exception {
Class<Account> c = Account.class;
Account a = new Account();
// 1. 读写 private 实例字段
Field owner = c.getDeclaredField("owner");
owner.setAccessible(true);
System.out.println(owner.get(a)); // 默认
owner.set(a, "王五");
System.out.println(owner.get(a)); // 王五
// 2. 静态字段:get/set 的对象参数传 null
Field total = c.getDeclaredField("total");
total.setAccessible(true);
total.set(null, 100);
System.out.println(total.get(null)); // 100
// 3. 基本类型字段可以用 setInt/getInt 避免装箱
System.out.println(total.getInt(null)); // 100
// 4. final 字段:JDK 8~11 对实例 final 字段 setAccessible(true) 后通常可改,
// 但读取方可能已被 JIT 常量折叠,读到旧值;JDK 12+ 对静态 final、
// record / hidden class 的 trusted final 字段一律拒绝。
Field id = c.getDeclaredField("id");
id.setAccessible(true);
id.set(a, "A002");
System.out.println(a.id); // 很可能仍是 A001(常量折叠)
}
}
不要试图修改 final 字段。 JDK 8 时代它"看起来能用",但依赖的是未定义行为:另一个类里 在编译期就被内联进调用方的字节码,你改了字段值,别人读到的还是 100。JDK 9 引入模块系统后,反射修改 final 的口子越收越紧(records、hidden classes 的 trusted final 完全禁止),JDK 17 之后很多场景直接抛 。把它当成不存在的能力。
3.3 Method:调用方法
public class MethodDemo {
public static class Calc {
public int add(int a, int b) { return a + b; }
private String join(String sep, String... parts) { return String.join(sep, parts); }
public static void ping() { System.out.println("pong"); }
}
public static void main(String[] args) throws Exception {
Class<Calc> c = Calc.class;
Calc calc = new Calc();
// 1. 参数类型必须精确匹配:getMethod 只找 public(含继承),getDeclaredMethod 找本类全部
Method add = c.getMethod("add", int.class, int.class);
Object r = add.invoke(calc, 1, 2);
System.out.println(r); // 3,基本类型被自动装箱成 Integer
// 2. 可变参数:字节码层面就是数组,参数类型写数组类型
Method join = c.getDeclaredMethod("join", String.class, String[].class);
join.setAccessible(true);
// 关键:可变参数必须再包一层 Object[],否则会被展开成三个参数
System.out.println(join.invoke(calc, new Object[]{"-", new String[]{"a", "b", "c"}}));
// 3. 静态方法:invoke 的第一个参数传 null
Method ping = c.getMethod("ping");
ping.invoke(null);
// 4. 目标方法抛出的异常会被包装
try {
c.getMethod("add", int.class, int.class).invoke(calc, "x", 2);
} catch (InvocationTargetException e) {
System.out.println("真实异常: " + e.getCause());
} catch (IllegalArgumentException e) {
System.out.println("参数类型不匹配: " + e.getMessage());
}
}
}
三个高频坑:参数类型要精确(int.class 不能写 Integer.class)、可变参数要二次包装(new Object[]{...})、异常要 getCause()。
3.4 数组反射与动态二维数组
import java.lang.reflect.Array;
public class ArrayReflectDemo {
public static void main(String[] args) {
// 动态创建 int[5]
Object arr = Array.newInstance(int.class, 5);
Array.setInt(arr, 0, 42);
System.out.println(Array.getInt(arr, 0)); // 42
System.out.println(Array.getLength(arr)); // 5
// 动态创建二维数组 String[3][4]:两步构造
Object matrix = Array.newInstance(String[].class, 3);
for (int i = 0; i < 3; i++) {
Object row = Array.newInstance(String.class, 4);
Array.set(matrix, i, row);
}
Array.set(Array.get(matrix, 1), 2, "hello");
System.out.println(((String[][]) matrix)[1][2]); // hello
// 通用工具:任意数组转字符串(注意区分基本类型数组与对象数组)
System.out.println(describe(new int[]{1, 2, 3}));
System.out.println(describe(new String[]{"a", "b"}));
}
static String describe(Object o) {
Class<?> c = o.getClass();
if (!c.isArray()) return String.valueOf(o);
int len = Array.getLength(o);
StringBuilder sb = new StringBuilder(c.getComponentType().getSimpleName())
.append('[').append(len).append("]{");
for (int i = 0; i < len; i++) {
if (i > 0) sb.append(", ");
sb.append(Array.get(o, i));
}
return sb.append('}').toString();
}
}
注意 Object[] 与 int[] 不能直接互转:new int[]{1,2}.getClass() 不是 Object[].class 的子类型,(Object[]) Array.get(...) 在基本类型数组上会 ClassCastException。用 Array.getXxx 系列方法最稳。
3.5 泛型信息获取
Java 泛型是擦除的,但擦除不等于"什么都不剩":签名(Signature)属性会保留声明处的泛型信息,getGenericXxx 系列方法可以把它们读出来。
import java.lang.reflect.*;
import java.util.*;
public class GenericDemo {
static class Repo<T> {
public Map<String, List<T>> findAll() { return null; }
public T[] toArray() { return null; }
public void save(List<? extends Number> nums) {}
}
static void printType(Type t, String label) {
System.out.print(label + " -> " + t.getTypeName() + " | ");
if (t instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) t;
System.out.println("raw=" + p.getRawType().getSimpleName()
+ ", actualArgs=" + Arrays.toString(p.getActualTypeArguments())
+ ", owner=" + p.getOwnerType());
} else if (t instanceof GenericArrayType) {
System.out.println("数组元素类型=" + ((GenericArrayType) t).getGenericComponentType());
} else if (t instanceof TypeVariable) {
TypeVariable<?> v = (TypeVariable<?>) t;
System.out.println("类型变量 " + v.getName()
+ ", 上界=" + Arrays.toString(v.getBounds())
+ ", 声明者=" + v.getGenericDeclaration());
} else if (t instanceof WildcardType) {
WildcardType w = (WildcardType) t;
System.out.println("通配符 上界=" + Arrays.toString(w.getUpperBounds())
+ ", 下界=" + Arrays.toString(w.getLowerBounds()));
} else {
System.out.println("普通 Class");
}
}
public static void main(String[] args) throws Exception {
printType(Repo.class.getMethod("findAll").getGenericReturnType(), "findAll 返回值");
printType(Repo.class.getMethod("toArray").getGenericReturnType(), "toArray 返回值");
printType(Repo.class.getMethod("save", List.class)
.getGenericParameterTypes()[0], "save 参数");
// 父类的真实泛型参数:MyBatis 的 Mapper 解析就靠这一招
printType(Repo.class.getTypeParameters()[0], "类上的类型变量 T");
}
}
这套 API 是 MyBatis 解析 interface UserMapper extends BaseMapper<User>、Spring 解析 Repository<User> 的关键:通过 getGenericInterfaces() 拿到 ParameterizedType,再取 getActualTypeArguments()[0] 就得到了实体类型 User。
四、反射性能:invoke 到底慢在哪
4.1 完整调用链路
一次 Method.invoke 在 HotSpot 上大致经历这些步骤:
- 可变参数打包:
invoke(obj, args...)本身是可变参数方法,调用方要先构造一个Object[](这是逃逸分析能优化掉的少数部分)。 - 访问检查:若
override == false,执行Reflection.getCallerClass()拿到调用者类,再走Reflection.verifyMemberAccess()+checkAccess(),逐层检查public/ 同包 / 子类 / 模块导出关系。这一步每次调用都会做。 - 获取 MethodAccessor:
Method内部持有一个MethodAccessor引用,首次调用时通过ReflectionFactory创建。 - DelegatingMethodAccessorImpl:只是一个可变的委托壳子,方便后续"热替换"实现。
- NativeMethodAccessorImpl:真正的第一版实现,内部是 JNI 调用
Method.invoke0。它维护一个numInvocations计数器,超过阈值(默认 15 次)后触发膨胀(inflation):由MethodAccessorGenerator在运行时生成一段字节码(类名类似GeneratedMethodAccessor1),并把它塞进DelegatingMethodAccessorImpl替换掉 Native 版本。 - GeneratedMethodAccessor:生成的是普通 Java 字节码(做了参数拆箱、类型转换、直接
invokevirtual),可以被 JIT 编译优化甚至内联,性能接近直接调用。
// 观察膨胀现象:打开 -Dsun.reflect.inflationThreshold=1 与默认值对比
// 也可以用 -verbose:class 观察 GeneratedMethodAccessor 类的生成
import java.lang.reflect.Method;
public class InflationDemo {
public static class Target { public int echo(int x) { return x; } }
public static void main(String[] args) throws Exception {
Method m = Target.class.getMethod("echo", int.class);
Target t = new Target();
// 前若干次走 NativeMethodAccessorImpl(JNI,慢)
for (int i = 0; i < 20; i++) m.invoke(t, i);
// 超过阈值后走 GeneratedMethodAccessor(字节码,快)
long s = System.nanoTime();
for (int i = 0; i < 5_000_000; i++) m.invoke(t, i);
System.out.printf("预热后 500 万次 invoke 耗时: %.0f ms%n", (System.nanoTime() - s) / 1e6);
long s2 = System.nanoTime();
int acc = 0;
for (int i = 0; i < 5_000_000; i++) acc += t.echo(i);
System.out.printf("直接调用 500 万次耗时: %.0f ms (acc=%d)%n",
(System.nanoTime() - s2) / 1e6, acc);
}
}
相关 VM 参数:-Dsun.reflect.inflationThreshold=N 调整膨胀阈值(设为 1 表示第二次调用就生成字节码访问器,设为很大则长期停留在 Native 实现);-Dsun.reflect.noInflation=true 直接跳过 Native 阶段、首次就生成字节码版(代价是首次调用要付出生成与类加载的时间,且每个 Method 都会生成一个类,元空间压力变大)。
4.2 setAccessible(true) 为什么能提速
从上面的链路可以看出,setAccessible(true) 把 Method 的 override 标志置为 true,直接砍掉了第 2 步:不再需要 Reflection.getCallerClass() 和逐层的成员访问校验。同时生成版访问器里也不需要插入访问检查逻辑。在 JDK 8 时代,这一项通常能带来 20%~50% 的提升。
但要注意两件事:
setAccessible(true)不等于绕过膨胀,Native → Generated 的切换依然会发生。- 在 JDK 9+ 模块化环境下,
setAccessible(true)对未opens的包会抛InaccessibleObjectException,不是"一定能成功"。
4.3 三种调用方式的量级对比
下表是量级参考,具体数字随 JDK 版本、硬件、JIT 预热程度变化很大,重点是数量级关系而不是绝对值:
| 调用方式 | 相对耗时(直接调用 = 1) | 特点 |
|---|---|---|
直接调用 obj.method() |
1 | 可被内联,最优 |
MethodHandle(static final 常量持有) |
1 ~ 3 | JDK 7 引入,JIT 友好,可内联 |
缓存的 Method.invoke(已膨胀 + setAccessible) |
5 ~ 20 | 框架中最常用,够快 |
未膨胀的 Method.invoke(前 15 次) |
50 ~ 200 | JNI 调用,慢 |
Class.forName + getMethod + invoke 全套 |
1000+ | 查找成本远大于调用,必须缓存 |
结论很直白:真正拖慢反射的往往不是 invoke,而是反复的 Class.forName 和 getMethod。把 Class/Method/Field 缓存进 Map 或静态常量,通常就能拿到 90% 的收益。
4.4 MethodHandle 与 VarHandle
java.lang.invoke.MethodHandle 是 JDK 7 引入的、比反射更贴近 JVM 的"方法指针":它是类型化的(MethodType)、不依赖字符串查找、由 JVM 在链接期做签名校验,JIT 可以把常量持有的 MethodHandle 完全内联,性能接近直接调用。invokedynamic(lambda、字符串拼接、record 的方法)底层就是它。
java.lang.invoke.VarHandle(JDK 9)则是字段/数组元素的"安全句柄",除了读写,还支持 CAS、volatile 语义、内存屏障等原子操作,是 AtomicXxxFieldUpdater 和 Unsafe 的官方替代品。
import java.lang.invoke.*;
public class MethodHandleDemo {
public static class User { public String name = "n"; public String getName() { return name; } }
public static void main(String[] args) throws Throwable {
// 1. MethodHandle 调用方法:注意它是在 MethodHandles.Lookup 的"权限上下文"里查找
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class);
MethodHandle getter = lookup.findVirtual(User.class, "getName", mt);
System.out.println((String) getter.invokeExact(new User())); // n
// 2. VarHandle 读写字段,支持 volatile / CAS
VarHandle vh = lookup.findVarHandle(User.class, "name", String.class);
User u = new User();
vh.set(u, "张三");
System.out.println(vh.get(u)); // 张三
System.out.println(vh.compareAndSet(u, "张三", "李四")); // true
System.out.println(u.name); // 李四
}
}
4.5 框架里的优化策略
- 缓存元数据:Spring 的
CachedIntrospectionResults、Jackson 的BeanPropertyWriter都是把反射结果缓存成访问器对象,避免每次解析。 LambdaMetafactory生成函数式访问器:不用Method.invoke,而是在运行期生成一个实现了Function/BiConsumer的类,get/set 变成普通接口调用。MyBatis 的PropertyAccessor、很多 JSON 库的高性能路径都这么干。- 一次性 setAccessible:启动时统一处理,而不是每次调用时判断。
- 必要时用字节码增强(ASM / ByteBuddy / CGLIB):直接生成访问类,彻底消灭反射调用。
五、注解基础:会说话的元数据
5.1 注解的本质
// 自定义一个注解,反编译后可以看到:
// public interface MyAnno extends java.lang.annotation.Annotation { ... }
public @interface MyAnno {
String value(); // 元素(element),不是方法
int count() default 1; // 带默认值
}
注解就是一个继承了 java.lang.annotation.Annotation 的接口,编译后 JVM 会为它生成一个动态代理实现($Proxy1 implements MyAnno, Annotation),你通过反射拿到的注解对象其实就是那个代理实例,调用 anno.value() 会走 AnnotationInvocationHandler 从 MemberValues 的 Map 里取值。
元素类型限制:只能是基本类型、String、Class、枚举、注解,以及它们的一维数组。不能用 null 作默认值,不能是任意对象类型,不能是多维数组。
5.2 元注解
| 元注解 | 作用 | 关键点 |
|---|---|---|
@Retention |
保留策略:SOURCE(编译后丢弃)/ CLASS(写进 class 文件但反射读不到,默认)/ RUNTIME(运行期可用反射读取) |
想靠反射解析必须 RUNTIME;@Override 是 SOURCE;Lombok、MapStruct 的注解多为 SOURCE |
@Target |
可标注位置:TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE |
不写则可标在任何位置 |
@Documented |
是否被 Javadoc 收录 | 仅影响文档 |
@Inherited |
是否允许子类继承父类类上的注解 | 只对 @Target(TYPE) 生效,对方法/字段无效;接口上的注解不会被继承 |
@Repeatable |
是否可重复标注(JDK 8) | 需要配套一个"容器注解",容器必须含 value() 数组元素 |
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 运行期可见,反射才能读到
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Inherited
public @interface Payable {
String channel() default "wechat";
int timeout() default 30;
Class<?> fallback() default Object.class;
}
5.2.1 @Inherited 的三个陷阱
@Inherited 有个经典陷阱:它只作用于类继承,而且 getAnnotations() 才能拿到继承来的注解,getDeclaredAnnotations() 拿不到。另外,方法上的注解永远不会被继承或合并——子类重写方法后,父类的 @Transactional 就丢了(Spring 对此有额外的查找逻辑,会去父类/接口上找)。
5.3 注解 vs 配置文件
注解的优势是就近声明、类型安全、IDE 可跳转、重构友好;劣势是硬编码在源码里,改配置要重新编译,且无法按环境差异化。配置文件的优势是外置、不改代码可调整、适合运维接管。实践中的取舍:
- 结构性、稳定的元数据用注解(
@Entity、@RequestMapping、@Autowired)。 - 需要按环境变化的用配置(数据源地址、超时时间、线程池参数)。
- 两者可以结合:注解里写
${xxx}占位符,由框架从配置解析填充。
5.4 编译期注解处理(APT)
注解并非只有运行期一条路。APT(Annotation Processing Tool,javax.annotation.processing.Processor) 允许你在编译期扫描注解并生成新的源文件,不需要反射、零运行期开销。
- Lombok:
@Data、@Getter是SOURCE级别,它并没有用标准 APT API,而是通过修改 javac 的 AST 直接"注入"方法,所以运行时 class 里已经存在 getter,运行时完全无感知。 - MapStruct:
@Mapper走标准 APT,编译期生成UserMapperImpl,把 DTO 转换变成普通方法调用,比 BeanUtils 反射拷贝快得多。 - Dagger / ButterKnife / AutoService:都是 APT 的典型用户。
选择依据:如果能力可以在编译期固化(生成代码就够了),优先 APT;只有必须"运行期根据环境动态决定"时才用反射注解。
六、注解解析与实战
6.1 用反射读取运行时注解
import java.lang.annotation.*;
import java.lang.reflect.*;
public class ReadAnnoDemo {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Metric { String value() default ""; int threshold() default 100; }
static class Service {
@Metric(value = "order.create", threshold = 200)
public void createOrder() {}
public void cancelOrder() {}
}
public static void main(String[] args) {
for (Method m : Service.class.getDeclaredMethods()) {
Metric metric = m.getAnnotation(Metric.class); // 拿不到返回 null
if (metric != null) {
System.out.printf("%s -> name=%s, threshold=%d%n",
m.getName(), metric.value(), metric.threshold());
}
// 参数上的注解:getParameterAnnotations() 返回二维数组
Annotation[][] pa = m.getParameterAnnotations();
}
}
}
6.2 实战一:@JsonField + 迷你序列化器
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
// 1. 定义注解:标记字段在 JSON 里的名字,以及是否忽略
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonField {
String value() default ""; // JSON 中的 key,默认用字段名
boolean ignore() default false;
}
// 2. 待序列化的对象
class Person {
@JsonField("user_name") private String name;
@JsonField private int age;
@JsonField(ignore = true) private String password;
private List<String> tags;
Person(String name, int age, String password, List<String> tags) {
this.name = name; this.age = age; this.password = password; this.tags = tags;
}
}
// 3. 迷你序列化器:只处理 String/基本类型/包装/集合/嵌套对象
public class MiniJsonSerializer {
public static String toJson(Object obj) throws IllegalAccessException {
if (obj == null) return "null";
if (obj instanceof String) return "\"" + obj + "\"";
if (obj instanceof Number || obj instanceof Boolean) return obj.toString();
if (obj instanceof Collection) {
StringBuilder sb = new StringBuilder("[");
for (Iterator<?> it = ((Collection<?>) obj).iterator(); it.hasNext(); ) {
sb.append(toJson(it.next()));
if (it.hasNext()) sb.append(",");
}
return sb.append("]").toString();
}
Map<String, String> kv = new LinkedHashMap<>();
for (Field f : obj.getClass().getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
JsonField ann = f.getAnnotation(JsonField.class);
if (ann != null && ann.ignore()) continue; // 被忽略
String key = (ann != null && !ann.value().isEmpty())
? ann.value() : f.getName(); // 默认字段名
f.setAccessible(true);
kv.put(key, toJson(f.get(obj)));
}
StringBuilder sb = new StringBuilder("{");
for (Iterator<Map.Entry<String, String>> it = kv.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, String> e = it.next();
sb.append("\"").append(e.getKey()).append("\":").append(e.getValue());
if (it.hasNext()) sb.append(",");
}
return sb.append("}").toString();
}
public static void main(String[] args) throws Exception {
System.out.println(toJson(new Person("张三", 18, "123456", Arrays.asList("vip", "new"))));
// {"user_name":"张三","age":18,"tags":["vip","new"]}
}
}
这个例子已经能看到 Jackson 的雏形:注解声明映射规则 → 反射读取字段 → 缓存并生成访问器。真实实现会额外处理循环引用、日期格式、@JsonIgnore 的继承语义等。
6.3 实战二:@Log + 反射实现调用日志
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.Arrays;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Log {
String value() default "";
boolean slowOnly() default false; // 是否只在耗时超阈值时打印
int thresholdMs() default 0;
}
interface OrderApi { Long create(String sku, int num); } // JDK 代理要求必须有接口
class OrderService implements OrderApi {
@Log(value = "创建订单", thresholdMs = 5)
public Long create(String sku, int num) {
try { Thread.sleep(12); } catch (InterruptedException ignored) {}
return 10001L;
}
}
public class LogAspect {
/** 用代理包装目标对象:每次方法调用前后插入日志逻辑 */
@SuppressWarnings("unchecked")
public static <T> T wrap(T target) {
Class<?> iface = target.getClass().getInterfaces()[0];
return (T) Proxy.newProxyInstance(iface.getClassLoader(),
new Class<?>[]{iface},
(proxy, method, args) -> {
Log log = target.getClass().getMethod(method.getName(),
method.getParameterTypes()).getAnnotation(Log.class);
boolean needLog = log != null;
long start = needLog ? System.nanoTime() : 0;
try {
Object ret = method.invoke(target, args); // 反射调用真实对象
return ret;
} finally {
if (needLog) {
long ms = (System.nanoTime() - start) / 1_000_000;
if (!log.slowOnly() || ms >= log.thresholdMs()) {
System.out.printf("[LOG] %s 参数=%s 耗时=%dms%n",
log.value(), Arrays.toString(args), ms);
}
}
}
});
}
public static void main(String[] args) {
OrderApi svc = wrap(new OrderService());
System.out.println(svc.create("SKU-1", 2));
// [LOG] 创建订单 参数=[SKU-1, 2] 耗时=12ms
// 10001
}
}
6.4 实战三:注解 + 反射实现迷你 DI 容器
下面这段是可直接运行的完整示例,涵盖组件扫描、依赖注入、单例缓存三个核心环节。
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Component { String value() default ""; } // value 即 bean 名
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.CONSTRUCTOR})
@interface Autowired {}
// ---- 业务类 ----
interface UserRepo { String findById(long id); }
@Component("userRepo")
class JdbcUserRepo implements UserRepo {
public String findById(long id) { return "user-" + id; }
}
@Component
class UserService {
@Autowired private UserRepo repo; // 按类型注入
public String getUserName(long id) { return repo.findById(id); }
}
// ---- 容器 ----
public class MiniContainer {
private final Map<String, Object> beans = new ConcurrentHashMap<>();
private final Map<Class<?>, Object> byType = new ConcurrentHashMap<>();
public void register(Class<?>... classes) throws Exception {
// 第一遍:实例化所有 @Component
for (Class<?> c : classes) {
Component ann = c.getAnnotation(Component.class);
if (ann == null) continue;
String name = ann.value().isEmpty()
? decapitalize(c.getSimpleName()) : ann.value();
Object instance = c.getDeclaredConstructor().newInstance();
beans.put(name, instance);
for (Class<?> itf : c.getInterfaces()) byType.put(itf, instance);
byType.put(c, instance);
}
// 第二遍:字段注入(必须在全部实例化之后,否则可能拿到半成品)
for (Object bean : beans.values()) inject(bean);
}
private void inject(Object bean) throws Exception {
for (Field f : bean.getClass().getDeclaredFields()) {
if (!f.isAnnotationPresent(Autowired.class)) continue;
Object dep = byType.get(f.getType());
if (dep == null) throw new IllegalStateException("未找到依赖: " + f.getType());
f.setAccessible(true);
f.set(bean, dep);
}
}
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) { return (T) byType.get(type); }
private static String decapitalize(String s) {
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
}
public static void main(String[] args) throws Exception {
MiniContainer ctx = new MiniContainer();
ctx.register(JdbcUserRepo.class, UserService.class);
System.out.println(ctx.getBean(UserService.class).getUserName(7L)); // user-7
}
}
它和 Spring 的差距在于:没有循环依赖处理(Spring 用三级缓存)、没有 @Qualifier 区分同类型多实现、没有 BeanPostProcessor 扩展点、没有作用域与生命周期回调。但核心骨架是一致的:扫描 → 实例化 → 注入 → 缓存 → 需要时织入代理。
6.5 Spring 中注解的作用链条简述
@Autowired:AutowiredAnnotationBeanPostProcessor在 Bean 属性填充阶段(populateBean)用反射遍历字段/方法,查找该注解,构造DependencyDescriptor,再从BeanFactory按类型(必要时按@Qualifier、@Primary)解析出依赖实例,最后field.set。@Transactional:InfrastructureAdvisorAutoProxyCreator(一个BeanPostProcessor)在 Bean 初始化后判断类/方法上是否有@Transactional,有则创建AnnotationTransactionAttributeSource解析事务属性,生成TransactionInterceptor织入代理;调用时由拦截器完成"开启事务 → 执行目标 → 提交/回滚"。
一句话总结:注解只是标记,真正干活的是"扫描注解的反射代码 + 把逻辑织入调用的代理"。
七、动态代理:无侵入增强的实现
7.1 静态代理的问题
静态代理要求你为每个被代理类手写或生成一个代理类:接口增加一个方法,所有代理类都要改;代理逻辑(日志、事务)稍有变化就要改 N 个文件。这是一场维护灾难。动态代理的目标就是:在运行期自动生成代理类,让增强逻辑只写一份。
7.2 JDK 动态代理
import java.lang.reflect.*;
import java.util.Arrays;
interface UserService { String getUserName(long id); void delete(long id); }
class UserServiceImpl implements UserService {
public String getUserName(long id) { return "user-" + id; }
public void delete(long id) { System.out.println("删除 " + id); }
}
public class JdkProxyDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
// 三个参数:类加载器、要代理的接口列表、调用处理器
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class<?>[]{UserService.class},
(p, method, params) -> {
System.out.println("[前置] " + method.getName()
+ " 参数=" + Arrays.toString(params));
long s = System.nanoTime();
try {
return method.invoke(target, params); // 转发给真实对象
} finally {
System.out.printf("[后置] 耗时 %.3f ms%n",
(System.nanoTime() - s) / 1e6);
}
});
System.out.println(proxy.getUserName(7L));
proxy.delete(7L);
// 观察生成的代理类
System.out.println(proxy.getClass().getName()); // com.sun.proxy.$Proxy0
System.out.println(Proxy.isProxyClass(proxy.getClass())); // true
System.out.println(Arrays.toString(proxy.getClass().getInterfaces()));
System.out.println(proxy.getClass().getSuperclass()); // class java.lang.reflect.Proxy
}
}
7.2.1 $Proxy0 的类结构与反编译要点
用 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true(JDK 8)或 -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true(JDK 9+)可以把生成的 class 文件落到磁盘。反编译 $Proxy0 你会看到它的结构:
| 结构要素 | 内容 |
|---|---|
| 继承关系 | public final class $Proxy0 extends Proxy implements UserService(单继承被占用,所以必须基于接口) |
| 静态字段 | 每个被代理方法一个 private static Method m1/m2/m3...,在静态块里通过 Class.forName + getMethod 初始化 |
| 构造器 | public $Proxy0(InvocationHandler h),直接调用 super(h) |
| 方法体 | public String getUserName(long id) { return (String) super.h.invoke(this, m3, new Object[]{id}); } |
| 三个 Object 方法 | equals/hashCode/toString 也被转发到 InvocationHandler |
| 类修饰符 | final,无法再被继承 |
完整的调用链是:proxy.getUserName(7) → $Proxy0.getUserName(生成的字节码,把 this、缓存好的静态 Method m3、装箱后的参数数组准备好)→ InvocationHandler.invoke(proxy, m3, args) → 你的增强逻辑 → m3.invoke(target, args)(反射调用真实对象)→ 返回结果(基本类型拆箱、强转)。
容易忽略的两点:一是 invoke 的第一个参数 proxy 不要在增强逻辑里再调用它的方法,否则会递归死循环,要调用就调 target;二是 代理类会缓存(默认在 Proxy 的 WeakCache 里),同一个接口重复生成不会无限产生新类。
7.3 CGLIB 动态代理
CGLIB(底层用 ASM)不要求接口,它的做法是在运行期生成目标类的子类字节码,重写非 final 的公开方法,在方法里调用 MethodInterceptor.intercept()。
// 需要依赖:cglib:cglib:3.3.0(Spring 已内置重打包版本 spring-core 中的 org.springframework.cglib)
import org.springframework.cglib.proxy.*;
import java.lang.reflect.Method;
class OrderService { // 注意:没有实现任何接口
public String pay(long orderId) { return "paid-" + orderId; }
public final String audit() { return "final 方法,无法被代理"; }
}
public class CglibDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, params, methodProxy) -> {
System.out.println("[cglib 前置] " + method.getName());
// methodProxy.invokeSuper 走 FastClass 索引,比 method.invoke 快
Object ret = methodProxy.invokeSuper(obj, params);
System.out.println("[cglib 后置] " + ret);
return ret;
});
OrderService proxy = (OrderService) enhancer.create();
System.out.println(proxy.getClass().getName()); // OrderService$$EnhancerByCGLIB$$xxxx
System.out.println(proxy.pay(99L));
System.out.println(proxy.audit()); // 直接走父类,不会被增强
}
}
CGLIB 的三个硬限制:不能代理 final 类(无法继承)、不能代理 final / private 方法(无法重写)、目标类必须有可访问构造器(生成子类要调用 super())。另外它的 FastClass 机制通过给方法编号、用 switch 分派,绕开了反射调用,这也是它在多次调用场景下性能好于 JDK 代理的原因之一。
7.4 JDK 代理与 CGLIB 对比
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现方式 | 运行时生成实现指定接口的代理类 | 运行时生成目标类的子类(ASM 字节码) |
| 前提 | 目标必须实现接口 | 目标类不能是 final、方法不能是 final/private |
| 生成速度 | 较快(生成逻辑简单) | 较慢(要生成 FastClass 等多个类) |
| 调用性能 | JDK 8+ 已大幅优化,多数场景与 CGLIB 持平甚至更快 | 单次调用通常略优(FastClass 索引调用) |
| 内存/元空间 | 较轻 | 较重,每个代理类附带多个辅助类 |
| 依赖 | JDK 自带,零依赖 | 需要额外依赖(Spring 已内置) |
| 典型场景 | RPC 客户端(Feign/Dubbo)、基于接口的 AOP | 类没有接口、需要代理具体类 |
toString/equals |
也被转发到处理器 | 同样会被拦截 |
7.5 Spring AOP 如何在两者之间选
Spring 的 DefaultAopProxyFactory 逻辑非常直白:
- 如果
proxyTargetClass = true(<aop:aspectj-autoproxy proxy-target-class="true"/>或@EnableAspectJAutoProxy(proxyTargetClass = true)),直接用 CGLIB。 - 否则如果目标类实现了接口,用 JDK 代理。
- 否则(没有接口)用 CGLIB。
重要变化:从 Spring Boot 2.x / Spring 5.2 起,proxyTargetClass 的默认值被改成了 true,也就是默认全部使用 CGLIB。原因是 JDK 代理有个经典坑:如果 Bean 有接口,注入时按接口类型拿到的代理对象无法强转回实现类(ClassCastException),大量用户踩坑;再加上 JDK 8 之后 CGLIB 的性能劣势基本消失,于是官方干脆统一为 CGLIB。在 JDK 17 环境下的现代 Spring Boot 项目,默认就是 CGLIB 代理,理解这一点能帮你少排查很多"为什么这个类的 @Transactional 不生效"(答案是:同一类内部方法自调用,压根没走代理)。
7.6 实战:用 JDK 代理实现耗时统计 + 重试的迷你 RPC 客户端
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Random;
interface RemoteApi { String query(String keyword); }
public class RpcProxy {
private static final Random RND = new Random();
@SuppressWarnings("unchecked")
public static <T> T create(Class<T> iface) {
return (T) Proxy.newProxyInstance(iface.getClassLoader(),
new Class<?>[]{iface},
(proxy, method, args) -> {
// 1. Object 上的方法直接本地处理,避免误当成远程调用
if (method.getDeclaringClass() == Object.class) {
return switch (method.getName()) {
case "toString" -> "RpcProxy@" + iface.getName();
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == args[0];
default -> throw new UnsupportedOperationException();
};
}
int maxRetry = 3;
for (int i = 1; ; i++) {
long s = System.nanoTime();
try {
System.out.printf("[RPC] %s(%s) 第 %d 次尝试%n",
method.getName(), Arrays.toString(args), i);
String r = remoteCall(method.getName(), args);
System.out.printf("[RPC] 成功,耗时 %.2f ms%n",
(System.nanoTime() - s) / 1e6);
return r;
} catch (RuntimeException e) {
System.out.printf("[RPC] 失败(%s),耗时 %.2f ms%n",
e.getMessage(), (System.nanoTime() - s) / 1e6);
if (i >= maxRetry) throw e; // 重试耗尽
}
}
});
}
private static String remoteCall(String method, Object[] args) {
if (RND.nextInt(3) == 0) throw new RuntimeException("网络超时"); // 模拟 1/3 失败率
return method + "-result";
}
public static void main(String[] args) {
RemoteApi api = create(RemoteApi.class);
System.out.println(api.query("java"));
}
}
7.7 代理在业务中的四类典型用法
- 日志与监控:方法级耗时、入参出参埋点、链路追踪(SkyWalking 的 Java Agent 本质上是更强的字节码增强)。
- 声明式事务:
@Transactional的开启/提交/回滚,业务代码零侵入。 - 缓存:
@Cacheable在调用前查缓存、命中直接返回,未命中才走目标方法并回填。 - 限流与熔断:在
invoke里先过令牌桶 / 信号量,超限快速失败,这是 Sentinel、Resilience4j 的常见织入方式。
记住这条判断标准:当你发现"很多方法都要在前后加同一段逻辑",而且这段逻辑与业务无关时,就该考虑代理了。反之,只有一两个方法需要特殊处理,直接写在方法里更清楚。
八、安全、边界与高频面试题
8.1 setAccessible 的危害与模块化限制
setAccessible(true) 能撬开 private,这在生产代码里基本等于主动放弃封装:你依赖了别人明确声明为内部实现的东西,对方一次小版本升级就能让你崩掉。JDK 9 引入模块系统后,官方从 JVM 层面收紧了这条口子。
// 假设目标是 JDK 内部类 java.util.ArrayList 的私有字段(java.base 模块未 opens java.util)
import java.lang.reflect.Field;
import java.util.ArrayList;
public class ModuleAccessDemo {
public static void main(String[] args) throws Exception {
Field f = ArrayList.class.getDeclaredField("elementData");
try {
f.setAccessible(true);
} catch (RuntimeException e) {
// JDK 9+:InaccessibleObjectException(RuntimeException 子类)
// JDK 8:这里会成功
System.out.println("被模块系统拦截: " + e.getClass().getSimpleName()
+ " - " + e.getMessage());
}
}
}
规则是这样的:只有声明了 opens(或 open module)的包,才允许对其非 public 成员做 setAccessible(true)。跨模块访问未导出的类型会抛 IllegalAccessException,访问未 open 的包会抛 InaccessibleObjectException。
应急方案是加 VM 参数(仅在确实需要时,且要清楚后果):
# JDK 9~16:放宽非法反射访问(JDK 16 起默认 deny)
java --illegal-access=permit -jar app.jar
# 精确开放某个包(推荐,粒度最小)
java --add-opens java.base/java.util=ALL-UNNAMED -jar app.jar
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
到了 JDK 17(JEP 403),JDK 内部元素被强封装,--illegal-access 的作用被极大限制,很多老框架(尤其是深度依赖 sun.misc.Unsafe、反射修改 JDK 内部字段的库)必须显式 --add-opens 才能跑起来。这也是很多团队升级 JDK 17 时的第一道坎——升级前先用 jdeps --jdk-internals 扫一遍依赖。
8.2 反射与序列化漏洞
反射是 Java 反序列化漏洞链条上的关键一环:ObjectInputStream 可以在运行期根据字节流里的类名 Class.forName 并实例化任意类;攻击者构造恶意的 gadget chain(如 InvokerTransformer 通过反射调用 Runtime.exec),就能实现远程代码执行。防御手段包括:
- 使用
ObjectInputFilter(JDK 9 起,JDK 17 可用 JEP 415 的过滤器工厂)做白名单校验。 - 不反序列化不可信数据,改用 JSON/Protobuf 等纯数据格式。
- 及时升级 commons-collections、fastjson 等历史高危库。
8.3 高频面试题
| # | 问题 | 要点 |
|---|---|---|
| 1 | 反射是什么?有什么优缺点? | 运行期获取类元数据并操作;优点是通用灵活,缺点是性能、安全、可维护性 |
| 2 | 获取 Class 对象有哪几种方式?区别? | Xxx.class(不初始化)、obj.getClass()(需实例)、Class.forName(默认初始化) |
| 3 | getMethods() 与 getDeclaredMethods() 区别? |
前者含继承的 public 方法,后者只含本类所有修饰符但不含继承 |
| 4 | Method.invoke 为什么慢? |
参数装箱、访问检查、JNI(未膨胀时)、无法内联;膨胀后生成字节码访问器 |
| 5 | inflation 机制是什么? | 前 15 次走 NativeMethodAccessorImpl,超过阈值由 MethodAccessorGenerator 生成 GeneratedMethodAccessor 替换;-Dsun.reflect.inflationThreshold 可调 |
| 6 | 如何优化反射性能? | 缓存 Class/Method/Field、setAccessible(true)、MethodHandle、LambdaMetafactory、字节码增强 |
| 7 | Class.forName 会触发静态初始化吗? |
默认会;forName(name, false, loader) 和 ClassLoader.loadClass 不会 |
| 8 | 注解的本质是什么? | 继承 Annotation 的接口,运行期由 JVM 生成动态代理实例 |
| 9 | @Retention 三种策略区别? |
SOURCE 编译后丢弃 / CLASS 进 class 文件但反射读不到 / RUNTIME 可被反射读取 |
| 10 | @Inherited 对方法注解生效吗? |
不生效,只对类继承有效,且接口上的注解不会被继承 |
| 11 | JDK 动态代理为什么必须基于接口? | 生成的代理类必须 extends Proxy,Java 单继承,只能靠实现接口扩展 |
| 12 | $Proxy0 里的方法是怎么转发的? |
静态块缓存 Method 对象,方法体调用 InvocationHandler.invoke(this, m, args) |
| 13 | JDK 代理与 CGLIB 的区别? | 接口 vs 继承子类;final 限制;性能与依赖差异 |
| 14 | Spring 默认用哪种代理? | Spring 5.2+ / Boot 2.x 起默认 CGLIB(proxyTargetClass=true) |
| 15 | 为什么同一个类里 this.method() 的 @Transactional 不生效? |
没走代理对象,直接是目标对象内部调用 |
| 16 | JDK 9 之后反射访问 JDK 内部类被拦截怎么办? | InaccessibleObjectException,用 --add-opens 精确开放,或升级框架 |
| 17 | 反射能修改 final 字段吗? |
实例字段在特定版本"看似可行"但不可靠(常量折叠);静态 final 与 record / hidden class 的 trusted final 已被禁止,不要依赖 |
| 18 | 泛型擦除后怎么拿到真实类型? | getGenericReturnType / getGenericParameterTypes / getGenericSuperclass + ParameterizedType.getActualTypeArguments() |
8.4 一条实践准则
反射是给框架用的,注解是给框架看的,代理是框架给你的。 业务代码里如果出现了 、、,先停下来问一句:能不能用接口、策略模式、或已有的框架能力替代?如果答案是"我在写一个通用组件",那就放手去写,但请把反射对象缓存起来、把异常处理干净、把模块化兼容性考虑进去。
8.5 全篇小结与下一篇预告
本篇沿着"元数据 → 解析 → 织入"这条线,把三块基石串了起来:Class 对象是入口,Constructor/Field/Method 是三把工具,invoke 的膨胀机制决定了它的性能下限,注解为反射提供了声明式输入,动态代理则把反射包装成了对用户无感知的优雅增强。理解它们之后,Spring 的 IoC 与 AOP 就不再是"魔法",而是一套可以被你自己复现的工程实现。
下一篇我们将进入 Java 并发编程的核心:JMM 与 volatile、synchronized 的底层实现,从 CPU 缓存一致性协议讲到对象头里的 Mark Word,把"可见性、原子性、有序性"这三件事真正讲清楚。
系列导航:Java 从入门到精通(一) · (二)面向对象 · (三)集合框架 · (四)异常与泛型 · (五)IO 与 NIO · (六)并发基础 · (七)JUC 工具类 · (八)JVM 内存与垃圾回收 · (九)类加载机制 · (十)Lambda 与 Stream · (十一)反射、注解与动态代理(本篇)