反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理
文章目录
- [反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理](#反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理)
-
- [1. 面试真题引入](#1. 面试真题引入)
- [2. 底层时空解构与源码透视](#2. 底层时空解构与源码透视)
-
- [2.1 `Method.invoke()` 调用链路全景](#2.1
Method.invoke()调用链路全景) - [2.2 `setAccessible(true)` 到底干了什么](#2.2
setAccessible(true)到底干了什么) - [2.3 反射为何拖慢 JIT 内联](#2.3 反射为何拖慢 JIT 内联)
- [2.4 桥方法:泛型擦除后的多态补丁](#2.4 桥方法:泛型擦除后的多态补丁)
- [2.1 `Method.invoke()` 调用链路全景](#2.1
- [3. 纯手工实战:零依赖 Mini ORM 框架](#3. 纯手工实战:零依赖 Mini ORM 框架)
-
- [3.1 注解定义](#3.1 注解定义)
- [3.2 实体类](#3.2 实体类)
- [3.3 ORM 核心引擎](#3.3 ORM 核心引擎)
- [3.4 测试运行](#3.4 测试运行)
- [4. 避坑指南](#4. 避坑指南)
-
- [4.1 反射 + 泛型:创建泛型数组的陷阱](#4.1 反射 + 泛型:创建泛型数组的陷阱)
- [4.2 反射性能:循环中反复 getDeclaredMethod()](#4.2 反射性能:循环中反复 getDeclaredMethod())
- [5. 面试连环炮 Mock Interview](#5. 面试连环炮 Mock Interview)
- [6. 类比小结与思考题](#6. 类比小结与思考题)
1. 面试真题引入
字节跳动三面,面试官翻到你简历上的"熟悉 Java 反射",抬头问了一句:"你用过反射,那你知不知道 Method.invoke() 下面到底发生了什么?"
你答"通过反射调用目标方法"。他接着问:"那如果同一个方法调了 16 次,JVM 内部会做什么?"
你愣住了。
他换了个方向:"泛型擦除之后,编译器怎么保证子类重写方法的多态性不会乱掉?"
反射调用链路、Inflation 阈值、桥方法------这三个问题指向同一个东西:元编程不是会写 getDeclaredMethod() 就够了,你得知道它背后的 JVM 在干什么。
这一期,我们把反射从 API 用法穿透到 JVM 源码层,再用泛型 + 注解 + 反射联合实战,手写一个零依赖的 Mini ORM。
2. 底层时空解构与源码透视
2.1 Method.invoke() 调用链路全景
当你写下 method.invoke(target, args),JVM 内部走过这样一条路:
Method.invoke()
→ MethodAccessor.invoke()
→ NativeMethodAccessorImpl.invoke0() [前 15 次]
→ GeneratedMethodAccessor1.invoke() [第 16 次开始]
关键代码在 NativeMethodAccessorImpl 中:
java
// sun.reflect.NativeMethodAccessorImpl
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private int numInvocations;
public Object invoke(Object obj, Object[] args) throws ... {
// numInvocations 超过阈值(默认15),触发升级
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().generateMethod(...);
setParent(acc); // 替换为 JIT 编译生成的高效 Accessor
}
return invoke0(method, obj, args); // 前 15 次走 Native 调用
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
Inflation 机制 :前 15 次调用走 Native 实现(invoke0),因为它启动快、无编译开销。第 16 次开始,JVM 用 ASM 动态生成一个字节码类(GeneratedMethodAccessor),这个类直接用 invokevirtual 调用目标方法------不再经过 JNI 边界,也不再走反射的安全检查栈。
图14-1:反射调用链路------Method.invoke → MethodAccessor → Native/JIT 切换时序图
(配图见 fig14-1.png)
2.2 setAccessible(true) 到底干了什么
java
Method method = User.class.getDeclaredMethod("getPassword");
method.setAccessible(true);
String pwd = (String) method.invoke(user);
每个 Method 对象内部有一个 MethodAccessor,而 MethodAccessor 持有对 ReflectionFactory 的引用。调用链路在 Native 层会检查 AccessibleObject.override 字段:
Method.invoke()
→ Reflection.ensureMemberAccess()
→ 检查类的访问级别 + override 标志
→ 如果 override == true,跳过 SecurityManager 检查
setAccessible(true) 只做了一件事:把 override 字段设为 true,告诉 JVM "别检查访问权限了"。代价是每次 invoke 仍要走这个判断分支------虽然绕过了权限校验,但检查本身的 CPU 分支预测和指令开销依然存在。这是反射比直接调用慢的根源之一。
2.3 反射为何拖慢 JIT 内联
JIT 编译器有一个关键优化叫内联(Inlining) :把被调用方法的代码直接嵌入调用方,省掉方法调用的栈帧开销。但反射调用对 JIT 来说是个黑盒------它看到的是 method.invoke() 而不是 user.getName(),无法在编译期确定目标方法。
结果是:
method.invoke()永远不会被 JIT 内联到调用方- 对目标方法的间接调用也无法享受内联带来的逃逸分析、锁消除、死代码消除等后续优化
- 所以反射调用在热点路径上可能比直接调用慢 10-50 倍
工程上的优化手段:
- 缓存
Method对象 :避免反复getDeclaredMethod(),因为每次都会创建新的Method副本 - reflectAsm:用 ASM 字节码生成代替反射,绕过 JNI 和权限检查
- Lambda 工厂 :
MethodHandle+LambdaMetafactory生成函数式接口,JIT 可内联
2.4 桥方法:泛型擦除后的多态补丁
第 3 期讲过泛型擦除------编译后 List<String> 和 List<Integer> 都是 List。但擦除带来了一个多态问题。看这段代码:
java
// 第3期回顾:泛型擦除后的类型替代
public class Node<T> {
public T data;
public void setData(T data) { this.data = data; }
}
// 子类指定了具体类型
public class MyNode extends Node<Integer> {
@Override
public void setData(Integer data) { super.setData(data); }
}
擦除后,Node.setData(T data) 变成 Node.setData(Object data)。但 MyNode.setData(Integer data) 的参数类型是 Integer,签名不匹配了------JVM 不会认为这是重写。
编译器怎么修?它自动生成一个桥方法:
java
// 编译器为 MyNode 自动生成的桥方法(反编译可见)
public class MyNode extends Node {
// 用户写的
public void setData(Integer data) { ... }
// 编译器生成的桥方法------参数类型是 Object,匹配父类签名
public void setData(Object data) {
this.setData((Integer) data); // 强转后调用户写的方法
}
}
桥方法做了两件事:
- 保证 JVM 层面的多态性------调用方用
Node引用调setData(Object),实际调到MyNode.setData(Object)桥方法 - 桥方法内部做类型强转,最终落到用户写的
setData(Integer)
面试中问到桥方法,关键句是:"桥方法是编译器为泛型擦除后的多态正确性自动生成的合成方法,参数类型使用擦除后的原始类型,方法体在做了类型检查后将调用委托给用户定义的参数化方法。"
3. 纯手工实战:零依赖 Mini ORM 框架
3.1 注解定义
java
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
String name();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
String name();
boolean id() default false; // 是否主键
}
3.2 实体类
用订单系统做例子。一张 t_order 表,有 id、订单号、金额三个字段:
java
@Table(name = "t_order")
public class Order {
@Column(name = "id", id = true)
private Long id;
@Column(name = "order_no")
private String orderNo;
@Column(name = "amount")
private Double amount;
public Order() {}
public Order(Long id, String orderNo, Double amount) {
this.id = id;
this.orderNo = orderNo;
this.amount = amount;
}
// getters & setters ...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
@Override
public String toString() {
return String.format("Order{id=%d, orderNo='%s', amount=%.2f}",
id, orderNo, amount);
}
}
3.3 ORM 核心引擎
java
import java.lang.reflect.Field;
import java.sql.*;
import java.util.*;
public class SimpleORM {
// 根据实体类生成 INSERT SQL 并执行
public static <T> void insert(Connection conn, T entity) throws Exception {
Class<?> clazz = entity.getClass();
Table tableAnno = clazz.getAnnotation(Table.class);
if (tableAnno == null) {
throw new IllegalArgumentException(clazz.getName() + " 缺少 @Table 注解");
}
StringBuilder sql = new StringBuilder("INSERT INTO " + tableAnno.name() + " (");
StringBuilder values = new StringBuilder(" VALUES (");
List<Object> params = new ArrayList<>();
// 反射遍历字段,收集 @Column 标记的字段
for (Field field : clazz.getDeclaredFields()) {
Column col = field.getAnnotation(Column.class);
if (col == null || col.id()) continue; // 主键自增,跳过
field.setAccessible(true);
sql.append(col.name()).append(",");
values.append("?,");
params.add(field.get(entity));
}
sql.deleteCharAt(sql.length() - 1);
values.deleteCharAt(values.length() - 1);
sql.append(")").append(values).append(")");
try (PreparedStatement ps = conn.prepareStatement(sql.toString())) {
for (int i = 0; i < params.size(); i++) {
ps.setObject(i + 1, params.get(i));
}
ps.executeUpdate();
}
}
// 将 ResultSet 一行反射映射为实体对象
public static <T> T mapRow(Class<T> clazz, ResultSet rs) throws Exception {
T entity = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
Column col = field.getAnnotation(Column.class);
if (col == null) continue;
field.setAccessible(true);
Object value = rs.getObject(col.name());
field.set(entity, value);
}
return entity;
}
// 按主键查询
public static <T> T findById(Connection conn, Class<T> clazz, Object id) throws Exception {
Table tableAnno = clazz.getAnnotation(Table.class);
if (tableAnno == null) {
throw new IllegalArgumentException(clazz.getName() + " 缺少 @Table 注解");
}
// 找主键字段
String idColumn = null;
for (Field field : clazz.getDeclaredFields()) {
Column col = field.getAnnotation(Column.class);
if (col != null && col.id()) {
idColumn = col.name();
break;
}
}
String sql = "SELECT * FROM " + tableAnno.name() + " WHERE " + idColumn + " = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapRow(clazz, rs);
}
}
}
return null;
}
}
3.4 测试运行
java
// 测试代码(需 SQLite 或 H2 内存数据库)
public class ORMTest {
public static void main(String[] args) throws Exception {
// 1. 建表
Connection conn = DriverManager.getConnection("jdbc:h2:mem:test", "sa", "");
conn.createStatement().execute(
"CREATE TABLE t_order (id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"order_no VARCHAR(50), amount DOUBLE)");
// 2. 插入订单
Order order1 = new Order(null, "20260620-001", 99.90);
SimpleORM.insert(conn, order1);
Order order2 = new Order(null, "20260620-002", 258.00);
SimpleORM.insert(conn, order2);
// 3. 按 ID 查询
Order found = SimpleORM.findById(conn, Order.class, 1L);
System.out.println("查询结果: " + found);
Order found2 = SimpleORM.findById(conn, Order.class, 2L);
System.out.println("查询结果: " + found2);
conn.close();
}
}
运行输出:
查询结果: Order{id=1, orderNo='20260620-001', amount=99.90}
查询结果: Order{id=2, orderNo='20260620-002', amount=258.00}
整段 ORM 核心代码不到 80 行,没有引入任何第三方库。它的工作原理就是注解 + 反射 + 泛型的联合应用:
@Table→clazz.getAnnotation(Table.class).name()→ 获取表名@Column→field.getAnnotation(Column.class).name()→ 获取列名field.setAccessible(true)+field.get(entity)→ 读取实体字段值拼 SQLResultSet.getObject()+field.set(entity, value)→ 把查询结果反射塞回实体
图14-2:ORM 框架架构图------注解扫描 → 反射映射 → SQL 生成三层结构
(配图见 fig14-2.png)
这套流程在 Spring Data JPA、MyBatis 的底层被放大了数百倍------加了缓存、连接池、AOP、事务管理,但核心的注解解析和反射映射套路完全一致。
4. 避坑指南
4.1 反射 + 泛型:创建泛型数组的陷阱
java
// 编译错误:generic array creation
List<String>[] array = new List<String>[10]; // ❌
// 变通:擦除 + 强转
List<String>[] array = (List<String>[]) new List[10]; // ⚠ 警告但不报错
Java 不允许创建具体泛型类型的数组,因为泛型擦除后数组的类型检查机制无法区分 List<String>[] 和 List<Integer>[]。绕过方式是先创建原始类型数组再强转------但在 ORM 实现中如果要反射创建泛型集合字段(如 List<Order>),会撞到同样的问题。解决方案:从 Field.getGenericType() 拿到 ParameterizedType,提取实际的类型参数。
4.2 反射性能:循环中反复 getDeclaredMethod()
java
// 坏写法:每次循环都查一次 Method
for (int i = 0; i < 10000; i++) {
Method m = obj.getClass().getDeclaredMethod("getName");
m.invoke(obj);
}
// 好写法:缓存 Method 对象
Method m = obj.getClass().getDeclaredMethod("getName");
for (int i = 0; i < 10000; i++) {
m.invoke(obj);
}
getDeclaredMethod() 每次调用都会创建新的 Method 副本并做一次安全检查。循环 10000 次就产生了 10000 个 Method 对象和 10000 次权限校验。缓存到循环外,Inflation 机制在第 16 次触发后性能会明显改善。
5. 面试连环炮 Mock Interview
面试官:你在项目里用过反射吗?反射调用的性能开销主要在哪里?
求职者 :用过。开销主要在三个地方。第一是 getDeclaredMethod() 每次调用都会创建新的 Method 对象,并触发 SecurityManager 的权限检查。第二是 Method.invoke() 内部需要做参数类型校验和装箱拆箱。第三是反射调用对 JIT 编译器是个黑盒------它看到的是 method.invoke(),无法确定具体的目标方法,所以无法内联,也就享受不到逃逸分析、锁消除这些后续优化。
面试官:那你说 JVM 的前 15 次和后 16 次有什么不同?
求职者 :这是反射的 Inflation 机制。前 15 次 invoke() 走的是 NativeMethodAccessor 的 JNI 实现,启动快但每次都要跨越 JNI 边界。第 16 次开始,JVM 用 ASM 动态生成一个 GeneratedMethodAccessor 字节码类,直接通过 invokevirtual 调用目标方法,不再走 JNI。这个切换阈值默认 15,可以通过 -Dsun.reflect.inflationThreshold=0 设为 0,让 JVM 一开始就用字节码方式。
面试官:泛型擦除之后,编译器怎么保证多态性?
求职者 :通过桥方法。比如 Node<T> 有 setData(T),擦除后签名变成 setData(Object)。子类 MyNode extends Node<Integer> 重写的 setData(Integer) 签名与父类不匹配。编译器自动在子类中生成一个 setData(Object) 桥方法,里面做 (Integer) 强转后再调用户写的 setData(Integer)。这样保证了调用方用父类引用可以多态调用到子类方法,同时类型安全也在桥方法内部得到了保证。
6. 类比小结与思考题
反射就像一台照妖镜------普通的 Java 代码只能看到类的公开接口,反射能让类在运行时看清自己的"五脏六腑",包括私有字段、私有方法、注解,甚至泛型擦除前原始的类型信息。ORM 框架、依赖注入容器、序列化库,无一不是反射的重度用户。
思考题
在 Mini ORM 的基础上,增加 @OneToMany 注解支持一对多关联查询。例如一个 User 可以有多个 Order,查询 User 时自动通过反射填充 List<Order> 字段。写出设计思路,并说明泛型擦除后如何正确获取 List<Order> 中的 Order 类型。
全系列完结。14 期走下来,从面向对象设计原则到集合源码、从泛型擦除到反射 ORM、从 Comparable/Comparator 到七大排序与 TimSort,希望这套"面试连环炮"帮你在简历上少写两行"熟悉",多写几行"吃透"。
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!
