
反射 泛型手写 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 Interview6. 类比小结与思考题思考题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(numInvocationsReflectionFactory.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)到底干了什么MethodmethodUser.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 倍工程上的优化手段缓存Method对象避免反复getDeclaredMethod()因为每次都会创建新的Method副本reflectAsm用 ASM 字节码生成代替反射绕过 JNI 和权限检查Lambda 工厂MethodHandleLambdaMetafactory生成函数式接口JIT 可内联2.4 桥方法泛型擦除后的多态补丁第 3 期讲过泛型擦除——编译后ListString和ListInteger都是List。但擦除带来了一个多态问题。看这段代码// 第3期回顾泛型擦除后的类型替代publicclassNodeT{publicTdata;publicvoidsetData(Tdata){this.datadata;}}// 子类指定了具体类型publicclassMyNodeextendsNodeInteger{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);// 强转后调用户写的方法}}桥方法做了两件事保证 JVM 层面的多态性——调用方用Node引用调setData(Object)实际调到MyNode.setData(Object)桥方法桥方法内部做类型强转最终落到用户写的setData(Integer)面试中问到桥方法关键句是“桥方法是编译器为泛型擦除后的多态正确性自动生成的合成方法参数类型使用擦除后的原始类型方法体在做了类型检查后将调用委托给用户定义的参数化方法。”3. 纯手工实战零依赖 Mini ORM 框架3.1 注解定义importjava.lang.annotation.*;Retention(RetentionPolicy.RUNTIME)Target(ElementType.TYPE)publicinterfaceTable{Stringname();}Retention(RetentionPolicy.RUNTIME)Target(ElementType.FIELD)publicinterfaceColumn{Stringname();booleanid()defaultfalse;// 是否主键}3.2 实体类用订单系统做例子。一张t_order表有 id、订单号、金额三个字段Table(namet_order)publicclassOrder{Column(nameid,idtrue)privateLongid;Column(nameorder_no)privateStringorderNo;Column(nameamount)privateDoubleamount;publicOrder(){}publicOrder(Longid,StringorderNo,Doubleamount){this.idid;this.orderNoorderNo;this.amountamount;}// getters setters ...publicLonggetId(){returnid;}publicvoidsetId(Longid){this.idid;}publicStringgetOrderNo(){returnorderNo;}publicvoidsetOrderNo(StringorderNo){this.orderNoorderNo;}publicDoublegetAmount(){returnamount;}publicvoidsetAmount(Doubleamount){this.amountamount;}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 并执行publicstaticTvoidinsert(Connectionconn,Tentity)throwsException{Class?clazzentity.getClass();TabletableAnnoclazz.getAnnotation(Table.class);if(tableAnnonull){thrownewIllegalArgumentException(clazz.getName() 缺少 Table 注解);}StringBuildersqlnewStringBuilder(INSERT INTO tableAnno.name() ();StringBuildervaluesnewStringBuilder( VALUES ();ListObjectparamsnewArrayList();// 反射遍历字段收集 Column 标记的字段for(Fieldfield:clazz.getDeclaredFields()){Columncolfield.getAnnotation(Column.class);if(colnull||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(PreparedStatementpsconn.prepareStatement(sql.toString())){for(inti0;iparams.size();i){ps.setObject(i1,params.get(i));}ps.executeUpdate();}}// 将 ResultSet 一行反射映射为实体对象publicstaticTTmapRow(ClassTclazz,ResultSetrs)throwsException{Tentityclazz.getDeclaredConstructor().newInstance();for(Fieldfield:clazz.getDeclaredFields()){Columncolfield.getAnnotation(Column.class);if(colnull)continue;field.setAccessible(true);Objectvaluers.getObject(col.name());field.set(entity,value);}returnentity;}// 按主键查询publicstaticTTfindById(Connectionconn,ClassTclazz,Objectid)throwsException{TabletableAnnoclazz.getAnnotation(Table.class);if(tableAnnonull){thrownewIllegalArgumentException(clazz.getName() 缺少 Table 注解);}// 找主键字段StringidColumnnull;for(Fieldfield:clazz.getDeclaredFields()){Columncolfield.getAnnotation(Column.class);if(col!nullcol.id()){idColumncol.name();break;}}StringsqlSELECT * FROM tableAnno.name() WHERE idColumn ?;try(PreparedStatementpsconn.prepareStatement(sql)){ps.setObject(1,id);try(ResultSetrsps.executeQuery()){if(rs.next()){returnmapRow(clazz,rs);}}}returnnull;}}3.4 测试运行// 测试代码需 SQLite 或 H2 内存数据库publicclassORMTest{publicstaticvoidmain(String[]args)throwsException{// 1. 建表ConnectionconnDriverManager.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. 插入订单Orderorder1newOrder(null,20260620-001,99.90);SimpleORM.insert(conn,order1);Orderorder2newOrder(null,20260620-002,258.00);SimpleORM.insert(conn,order2);// 3. 按 ID 查询OrderfoundSimpleORM.findById(conn,Order.class,1L);System.out.println(查询结果: found);Orderfound2SimpleORM.findById(conn,Order.class,2L);System.out.println(查询结果: found2);conn.close();}}运行输出查询结果: Order{id1, orderNo20260620-001, amount99.90} 查询结果: Order{id2, orderNo20260620-002, amount258.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-2ORM 框架架构图——注解扫描 → 反射映射 → SQL 生成三层结构(配图见 fig14-2.png)这套流程在 Spring Data JPA、MyBatis 的底层被放大了数百倍——加了缓存、连接池、AOP、事务管理但核心的注解解析和反射映射套路完全一致。4. 避坑指南4.1 反射 泛型创建泛型数组的陷阱// 编译错误generic array creationListString[]arraynewListString[10];// ❌// 变通擦除 强转ListString[]array(ListString[])newList[10];// ⚠ 警告但不报错Java 不允许创建具体泛型类型的数组因为泛型擦除后数组的类型检查机制无法区分ListString[]和ListInteger[]。绕过方式是先创建原始类型数组再强转——但在 ORM 实现中如果要反射创建泛型集合字段如ListOrder会撞到同样的问题。解决方案从Field.getGenericType()拿到ParameterizedType提取实际的类型参数。4.2 反射性能循环中反复 getDeclaredMethod()// 坏写法每次循环都查一次 Methodfor(inti0;i10000;i){Methodmobj.getClass().getDeclaredMethod(getName);m.invoke(obj);}// 好写法缓存 Method 对象Methodmobj.getClass().getDeclaredMethod(getName);for(inti0;i10000;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.inflationThreshold0设为 0让 JVM 一开始就用字节码方式。面试官泛型擦除之后编译器怎么保证多态性求职者通过桥方法。比如NodeT有setData(T)擦除后签名变成setData(Object)。子类MyNode extends NodeInteger重写的setData(Integer)签名与父类不匹配。编译器自动在子类中生成一个setData(Object)桥方法里面做(Integer)强转后再调用户写的setData(Integer)。这样保证了调用方用父类引用可以多态调用到子类方法同时类型安全也在桥方法内部得到了保证。6. 类比小结与思考题反射就像一台照妖镜——普通的 Java 代码只能看到类的公开接口反射能让类在运行时看清自己的五脏六腑包括私有字段、私有方法、注解甚至泛型擦除前原始的类型信息。ORM 框架、依赖注入容器、序列化库无一不是反射的重度用户。思考题在 Mini ORM 的基础上增加OneToMany注解支持一对多关联查询。例如一个User可以有多个Order查询User时自动通过反射填充ListOrder字段。写出设计思路并说明泛型擦除后如何正确获取ListOrder中的Order类型。全系列完结。14 期走下来从面向对象设计原则到集合源码、从泛型擦除到反射 ORM、从 Comparable/Comparator 到七大排序与 TimSort希望这套面试连环炮帮你在简历上少写两行熟悉多写几行吃透。感谢阅读记得点赞、关注、收藏欢迎各位评论区交流