【Java基础】反射 + 泛型手写 ORM:你写的框架,Spring 也在用同一套原理
2026/6/25 18:21:43 网站建设 项目流程

反射 + 泛型手写 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.1Method.invoke()调用链路全景

当你写下method.invoke(target, args),JVM 内部走过这样一条路:

Method.invoke() → MethodAccessor.invoke() → NativeMethodAccessorImpl.invoke0() [前 15 次] → GeneratedMethodAccessor1.invoke() [第 16 次开始]

关键代码在NativeMethodAccessorImpl中:

// sun.reflect.NativeMethodAccessorImplclassNativeMethodAccessorImplextendsMethodAccessorImpl{privateintnumInvocations;publicObjectinvoke(Objectobj,Object[]args)throws...{// numInvocations 超过阈值(默认15),触发升级if(++numInvocations>ReflectionFactory.inflationThreshold()&&!ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())){MethodAccessorImplacc=(MethodAccessorImpl)newMethodAccessorGenerator().generateMethod(...);setParent(acc);// 替换为 JIT 编译生成的高效 Accessor}returninvoke0(method,obj,args);// 前 15 次走 Native 调用}privatestaticnativeObjectinvoke0(Methodm,Objectobj,Object[]args);}

Inflation 机制:前 15 次调用走 Native 实现(invoke0),因为它启动快、无编译开销。第 16 次开始,JVM 用 ASM 动态生成一个字节码类(GeneratedMethodAccessor),这个类直接用invokevirtual调用目标方法——不再经过 JNI 边界,也不再走反射的安全检查栈。

图14-1:反射调用链路——Method.invoke → MethodAccessor → Native/JIT 切换时序图
(配图见 fig14-1.png)

2.2setAccessible(true)到底干了什么

Methodmethod=User.class.getDeclaredMethod("getPassword");method.setAccessible(true);Stringpwd=(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。但擦除带来了一个多态问题。看这段代码:

// 第3期回顾:泛型擦除后的类型替代publicclassNode<T>{publicTdata;publicvoidsetData(Tdata){this.data=data;}}// 子类指定了具体类型publicclassMyNodeextendsNode<Integer>{@OverridepublicvoidsetData(Integerdata){super.setData(data);}}

擦除后,Node.setData(T data)变成Node.setData(Object data)。但MyNode.setData(Integer data)的参数类型是Integer,签名不匹配了——JVM 不会认为这是重写。

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

// 编译器为 MyNode 自动生成的桥方法(反编译可见)publicclassMyNodeextendsNode{// 用户写的publicvoidsetData(Integerdata){...}// 编译器生成的桥方法——参数类型是 Object,匹配父类签名publicvoidsetData(Objectdata){this.setData((Integer)data);// 强转后调用户写的方法}}

桥方法做了两件事:

  1. 保证 JVM 层面的多态性——调用方用Node引用调setData(Object),实际调到MyNode.setData(Object)桥方法
  2. 桥方法内部做类型强转,最终落到用户写的setData(Integer)

面试中问到桥方法,关键句是:“桥方法是编译器为泛型擦除后的多态正确性自动生成的合成方法,参数类型使用擦除后的原始类型,方法体在做了类型检查后将调用委托给用户定义的参数化方法。”


3. 纯手工实战:零依赖 Mini ORM 框架

3.1 注解定义

importjava.lang.annotation.*;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public@interfaceTable{Stringname();}@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public@interfaceColumn{Stringname();booleanid()defaultfalse;// 是否主键}

3.2 实体类

用订单系统做例子。一张t_order表,有 id、订单号、金额三个字段:

@Table(name="t_order")publicclassOrder{@Column(name="id",id=true)privateLongid;@Column(name="order_no")privateStringorderNo;@Column(name="amount")privateDoubleamount;publicOrder(){}publicOrder(Longid,StringorderNo,Doubleamount){this.id=id;this.orderNo=orderNo;this.amount=amount;}// getters & setters ...publicLonggetId(){returnid;}publicvoidsetId(Longid){this.id=id;}publicStringgetOrderNo(){returnorderNo;}publicvoidsetOrderNo(StringorderNo){this.orderNo=orderNo;}publicDoublegetAmount(){returnamount;}publicvoidsetAmount(Doubleamount){this.amount=amount;}@OverridepublicStringtoString(){returnString.format("Order{id=%d, orderNo='%s', amount=%.2f}",id,orderNo,amount);}}

3.3 ORM 核心引擎

importjava.lang.reflect.Field;importjava.sql.*;importjava.util.*;publicclassSimpleORM{// 根据实体类生成 INSERT SQL 并执行publicstatic<T>voidinsert(Connectionconn,Tentity)throwsException{Class<?>clazz=entity.getClass();TabletableAnno=clazz.getAnnotation(Table.class);if(tableAnno==null){thrownewIllegalArgumentException(clazz.getName()+" 缺少 @Table 注解");}StringBuildersql=newStringBuilder("INSERT INTO "+tableAnno.name()+" (");StringBuildervalues=newStringBuilder(" VALUES (");List<Object>params=newArrayList<>();// 反射遍历字段,收集 @Column 标记的字段for(Fieldfield:clazz.getDeclaredFields()){Columncol=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(PreparedStatementps=conn.prepareStatement(sql.toString())){for(inti=0;i<params.size();i++){ps.setObject(i+1,params.get(i));}ps.executeUpdate();}}// 将 ResultSet 一行反射映射为实体对象publicstatic<T>TmapRow(Class<T>clazz,ResultSetrs)throwsException{Tentity=clazz.getDeclaredConstructor().newInstance();for(Fieldfield:clazz.getDeclaredFields()){Columncol=field.getAnnotation(Column.class);if(col==null)continue;field.setAccessible(true);Objectvalue=rs.getObject(col.name());field.set(entity,value);}returnentity;}// 按主键查询publicstatic<T>TfindById(Connectionconn,Class<T>clazz,Objectid)throwsException{TabletableAnno=clazz.getAnnotation(Table.class);if(tableAnno==null){thrownewIllegalArgumentException(clazz.getName()+" 缺少 @Table 注解");}// 找主键字段StringidColumn=null;for(Fieldfield:clazz.getDeclaredFields()){Columncol=field.getAnnotation(Column.class);if(col!=null&&col.id()){idColumn=col.name();break;}}Stringsql="SELECT * FROM "+tableAnno.name()+" WHERE "+idColumn+" = ?";try(PreparedStatementps=conn.prepareStatement(sql)){ps.setObject(1,id);try(ResultSetrs=ps.executeQuery()){if(rs.next()){returnmapRow(clazz,rs);}}}returnnull;}}

3.4 测试运行

// 测试代码(需 SQLite 或 H2 内存数据库)publicclassORMTest{publicstaticvoidmain(String[]args)throwsException{// 1. 建表Connectionconn=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. 插入订单Orderorder1=newOrder(null,"20260620-001",99.90);SimpleORM.insert(conn,order1);Orderorder2=newOrder(null,"20260620-002",258.00);SimpleORM.insert(conn,order2);// 3. 按 ID 查询Orderfound=SimpleORM.findById(conn,Order.class,1L);System.out.println("查询结果: "+found);Orderfound2=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 反射 + 泛型:创建泛型数组的陷阱

// 编译错误:generic array creationList<String>[]array=newList<String>[10];// ❌// 变通:擦除 + 强转List<String>[]array=(List<String>[])newList[10];// ⚠ 警告但不报错

Java 不允许创建具体泛型类型的数组,因为泛型擦除后数组的类型检查机制无法区分List<String>[]List<Integer>[]。绕过方式是先创建原始类型数组再强转——但在 ORM 实现中如果要反射创建泛型集合字段(如List<Order>),会撞到同样的问题。解决方案:从Field.getGenericType()拿到ParameterizedType,提取实际的类型参数。

4.2 反射性能:循环中反复 getDeclaredMethod()

// 坏写法:每次循环都查一次 Methodfor(inti=0;i<10000;i++){Methodm=obj.getClass().getDeclaredMethod("getName");m.invoke(obj);}// 好写法:缓存 Method 对象Methodm=obj.getClass().getDeclaredMethod("getName");for(inti=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,希望这套"面试连环炮"帮你在简历上少写两行"熟悉",多写几行"吃透"。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询