欢迎光临
我们一直在努力

【Java基础】反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理

反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理


文章目录

  • 反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理
    • 1. 面试真题引入
    • 2. 底层时空解构与源码透视
      • 2.1 `Method.invoke()` 调用链路全景
      • 2.2 `setAccessible(true)` 到底干了什么
      • 2.3 反射为何拖慢 JIT 内联
      • 2.4 桥方法:泛型擦除后的多态补丁
    • 3. 纯手工实战:零依赖 Mini ORM 框架
      • 3.1 注解定义
      • 3.2 实体类
      • 3.3 ORM 核心引擎
      • 3.4 测试运行
    • 4. 避坑指南
      • 4.1 反射 + 泛型:创建泛型数组的陷阱
      • 4.2 反射性能:循环中反复 getDeclaredMethod()
    • 5. 面试连环炮 Mock Interview
    • 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 中:

// 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) 到底干了什么

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。但擦除带来了一个多态问题。看这段代码:

    // 第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 不会认为这是重写。

    编译器怎么修?它自动生成一个桥方法:

    // 编译器为 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 注解定义

    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、订单号、金额三个字段:

    @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 核心引擎

    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 测试运行

    // 测试代码(需 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) → 读取实体字段值拼 SQL
  • ResultSet.getObject() + field.set(entity, value) → 把查询结果反射塞回实体
  • 图14-2:ORM 框架架构图——注解扫描 → 反射映射 → SQL 生成三层结构 (配图见 fig14-2.png)

    这套流程在 Spring Data JPA、MyBatis 的底层被放大了数百倍——加了缓存、连接池、AOP、事务管理,但核心的注解解析和反射映射套路完全一致。


    4. 避坑指南

    4.1 反射 + 泛型:创建泛型数组的陷阱

    // 编译错误: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()

    // 坏写法:每次循环都查一次 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,希望这套"面试连环炮"帮你在简历上少写两行"熟悉",多写几行"吃透"。

    感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!! 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【Java基础】反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址