Java 从入门到精通(十一):反射、注解与动态代理——框架底层的三块基石

本篇属于「知其然更知其所以然」的一章。反射、注解、动态代理这三样东西,日常写业务代码几乎碰不到,但只要你点开 Spring 的 AutowiredAnnotationBeanPostProcessor、MyBatis 的 MapperProxy、Jackson 的 BeanDeserializer,就会发现它们无处不在。理解这三块基石,你读框架源码时就不会再迷路,也能在真正需要”通用能力”的场景里写出靠谱的底层工具,而不是把反射当成万能胶乱抹一气。阅读建议:跟着代码敲一遍,尤其是第六节的 DI 容器和第七节的代理切面,跑通之后很多面试题会自然解开。

写在前面:为什么这三个东西要一起讲

很多教材把反射、注解、动态代理拆成三章,结果读者学完只记住了三套 API,串不起来。实际上它们是一条流水线上的三道工序:注解负责”打标记”(声明元数据),反射负责”读标记 + 执行动作”(运行期解析与调用),动态代理负责”把动作织入调用过程”(无侵入增强)。

Spring 里 @Transactional 就是一个典型闭环:你在方法上打一个注解(注解),Spring 容器启动时用反射扫描到它并判读事务属性(反射),然后为目标对象生成一个代理对象,让每次方法调用先经过事务拦截器(动态代理)。少了任何一环,这个能力都实现不了。

所以本篇的顺序是:先把反射讲透(它是唯一真正”做事”的那个),再讲注解(给反射提供输入),最后讲代理(把反射包装成优雅的形态)。

一、反射是什么:编译期确定 vs 运行期确定

1.1 从一行最普通的代码说起

1
2
User user = new User();
user.setName("张三");

这行代码在编译期就已经完全确定了:编译器知道 User 有 setName 方法、参数是 String、返回值是 void。生成的字节码里是一条 invokevirtual 指令,指向常量池中一个明确的符号引用,JVM 加载类时把它解析成直接引用,之后每次调用都是一次近乎”硬编码”的跳转。

反射把这个过程推迟到了运行期:你手上只有一个字符串 "com.example.User" 和另一个字符串 "setName",要等程序跑起来才知道有没有这个类、有没有这个方法、参数类型对不对。

1
2
3
4
5
// 编译期:不知道类名、方法名、参数类型,全部来自运行期的字符串
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 调用、无法内联)、安全与封装风险(setAccessible 可以撬开 private)、可维护性损失(字符串硬编码、编译期失去类型保护、IDE 无法帮你重构)。

合理的边界可以概括成三条:

  1. 写通用基础设施时用反射(框架、ORM、序列化、测试框架、插件系统)。
  2. 写业务逻辑时不要用反射。能用 if 解决的不要用 Class.forName;能用接口解决的不要用 Method.invoke。
  3. 热路径上必须缓存反射对象。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 三种获取方式对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 与包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:创建对象的两种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:读写字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 时代它”看起来能用”,但依赖的是未定义行为:另一个类里 static final int MAX = 100 在编译期就被内联进调用方的字节码,你改了字段值,别人读到的还是 100。JDK 9 引入模块系统后,反射修改 final 的口子越收越紧(records、hidden classes 的 trusted final 完全禁止),JDK 17 之后很多场景直接抛 IllegalAccessException。把它当成不存在的能力。

3.3 Method:调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 数组反射与动态二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 系列方法可以把它们读出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 上大致经历这些步骤:

  1. 可变参数打包:invoke(obj, args...) 本身是可变参数方法,调用方要先构造一个 Object[](这是逃逸分析能优化掉的少数部分)。
  2. 访问检查:若 override == false,执行 Reflection.getCallerClass() 拿到调用者类,再走 Reflection.verifyMemberAccess() + checkAccess(),逐层检查 public / 同包 / 子类 / 模块导出关系。这一步每次调用都会做。
  3. 获取 MethodAccessor:Method 内部持有一个 MethodAccessor 引用,首次调用时通过 ReflectionFactory 创建。
  4. DelegatingMethodAccessorImpl:只是一个可变的委托壳子,方便后续”热替换”实现。
  5. NativeMethodAccessorImpl:真正的第一版实现,内部是 JNI 调用 Method.invoke0。它维护一个 numInvocations 计数器,超过阈值(默认 15 次)后触发膨胀(inflation):由 MethodAccessorGenerator 在运行时生成一段字节码(类名类似 GeneratedMethodAccessor1),并把它塞进 DelegatingMethodAccessorImpl 替换掉 Native 版本。
  6. GeneratedMethodAccessor:生成的是普通 Java 字节码(做了参数拆箱、类型转换、直接 invokevirtual),可以被 JIT 编译优化甚至内联,性能接近直接调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 观察膨胀现象:打开 -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 的官方替代品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 注解的本质

1
2
3
4
5
6
// 自定义一个注解,反编译后可以看到:
// 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() 数组元素
1
2
3
4
5
6
7
8
9
10
11
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 用反射读取运行时注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 + 迷你序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 + 反射实现调用日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 容器

下面这段是可直接运行的完整示例,涵盖组件扫描、依赖注入、单例缓存三个核心环节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 需要依赖: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 逻辑非常直白:

  1. 如果 proxyTargetClass = true(<aop:aspectj-autoproxy proxy-target-class="true"/> 或 @EnableAspectJAutoProxy(proxyTargetClass = true)),直接用 CGLIB。
  2. 否则如果目标类实现了接口,用 JDK 代理。
  3. 否则(没有接口)用 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 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 代理在业务中的四类典型用法

  1. 日志与监控:方法级耗时、入参出参埋点、链路追踪(SkyWalking 的 Java Agent 本质上是更强的字节码增强)。
  2. 声明式事务:@Transactional 的开启/提交/回滚,业务代码零侵入。
  3. 缓存:@Cacheable 在调用前查缓存、命中直接返回,未命中才走目标方法并回填。
  4. 限流与熔断:在 invoke 里先过令牌桶 / 信号量,超限快速失败,这是 Sentinel、Resilience4j 的常见织入方式。

记住这条判断标准:当你发现”很多方法都要在前后加同一段逻辑”,而且这段逻辑与业务无关时,就该考虑代理了。反之,只有一两个方法需要特殊处理,直接写在方法里更清楚。

八、安全、边界与高频面试题

8.1 setAccessible 的危害与模块化限制

setAccessible(true) 能撬开 private,这在生产代码里基本等于主动放弃封装:你依赖了别人明确声明为内部实现的东西,对方一次小版本升级就能让你崩掉。JDK 9 引入模块系统后,官方从 JVM 层面收紧了这条口子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 假设目标是 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 参数(仅在确实需要时,且要清楚后果):

1
2
3
4
5
6
# 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 一条实践准则

反射是给框架用的,注解是给框架看的,代理是框架给你的。 业务代码里如果出现了 Class.forName、Method.invoke、Proxy.newProxyInstance,先停下来问一句:能不能用接口、策略模式、或已有的框架能力替代?如果答案是”我在写一个通用组件”,那就放手去写,但请把反射对象缓存起来、把异常处理干净、把模块化兼容性考虑进去。

8.5 全篇小结与下一篇预告

本篇沿着”元数据 → 解析 → 织入”这条线,把三块基石串了起来:Class 对象是入口,Constructor/Field/Method 是三把工具,invoke 的膨胀机制决定了它的性能下限,注解为反射提供了声明式输入,动态代理则把反射包装成了对用户无感知的优雅增强。理解它们之后,Spring 的 IoC 与 AOP 就不再是”魔法”,而是一套可以被你自己复现的工程实现。

下一篇我们将进入 Java 并发编程的核心:JMM 与 volatile、synchronized 的底层实现,从 CPU 缓存一致性协议讲到对象头里的 Mark Word,把”可见性、原子性、有序性”这三件事真正讲清楚。


系列导航:Java 从入门到精通(一) · (二)面向对象 · (三)集合框架 · (四)异常与泛型 · (五)IO 与 NIO · (六)并发基础 · (七)JUC 工具类 · (八)JVM 内存与垃圾回收 · (九)类加载机制 · (十)Lambda 与 Stream · (十一)反射、注解与动态代理(本篇)