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

反射 + 泛型手写 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 桥方法:泛型擦除后的多态补丁)
    • [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 倍

工程上的优化手段:

  1. 缓存 Method 对象 :避免反复 getDeclaredMethod(),因为每次都会创建新的 Method 副本
  2. reflectAsm:用 ASM 字节码生成代替反射,绕过 JNI 和权限检查
  3. 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); // 强转后调用户写的方法
    }
}

桥方法做了两件事:

  1. 保证 JVM 层面的多态性------调用方用 Node 引用调 setData(Object),实际调到 MyNode.setData(Object) 桥方法
  2. 桥方法内部做类型强转,最终落到用户写的 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 行,没有引入任何第三方库。它的工作原理就是注解 + 反射 + 泛型的联合应用:

  1. @Tableclazz.getAnnotation(Table.class).name() → 获取表名
  2. @Columnfield.getAnnotation(Column.class).name() → 获取列名
  3. field.setAccessible(true) + field.get(entity) → 读取实体字段值拼 SQL
  4. ResultSet.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,希望这套"面试连环炮"帮你在简历上少写两行"熟悉",多写几行"吃透"。

感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!