说说反射的用途及实现
反射的用途
Java 反射机制是一个非常强大的功能,在很多的项目比如 Spring,MyBatis 都都可以看到反射的身影。通过反射机制,我们可以在运行期间获取对象的类型信息。利用这一点我们可以实现工厂模式和代理模式等设计模式,同时也可以解决 Java 泛型擦除等令人苦恼的问题。
把”用途”落到具体框架行为上会更直观:
- Spring IoC:读取 XML/注解中的类名 →
Class.forName加载 → 反射构造器实例化 → 反射 setter/字段注入依赖。没有反射,容器就无法在”不知道具体类”的前提下创建对象 - MyBatis:把 ResultSet 的列映射到 POJO 字段、把
#{param}绑定到方法参数,全靠反射读写属性 - JDK 动态代理:
Proxy.newProxyInstance在运行期生成类并转发调用,AOP 的基石 - 泛型擦除补偿:运行期通过
getGenericReturnType等取回声明时的泛型信息(Jackson 反序列化List<User>就依赖它) - 测试与工具:JUnit 反射调用测试方法;Lombok、序列化框架、IDE 提示都建立在类型 introspection 上
获取 Class 对象的四种方式
- 通过
getClass()方法:已有实例时,obj.getClass() - 通过
Class.forName()方法:只有全限定类名时(配置驱动加载,会触发静态初始化) - 使用
类.class:编译期已知类型时(不触发静态初始化,最轻量) - 通过类加载器实现,
getClassLoader():loader.loadClass(name),与 forName 的区别是默认不初始化类
Class<?> c1 = user.getClass(); // 实例 → Class |
反射能做什么:核心 API
- 构造:
clazz.getDeclaredConstructor(String.class).newInstance(...)(JDK9 起推荐,取代Class.newInstance) - 方法:
getDeclaredMethod(name, paramTypes)→method.invoke(obj, args) - 字段:
field.setAccessible(true)后可读写私有字段(注入与序列化框架的常规操作) - 注解:
method.getAnnotation(Log.class),注解驱动的入口 - 泛型:
getGenericSuperclass/getGenericParameterTypes
实现原理:从 Class 文件到 MethodAccessor
JVM 视角下,类加载器把 Class 文件的二进制流解析为方法区的类型数据,并在堆中生成对应的 java.lang.Class 对象作为访问入口。反射调用 method.invoke 时:
- 权限检查(非 public 或未 setAccessible 会抛 IllegalAccessException)
- 获取 MethodAccessor:JDK 默认实现是委派式——前 15 次调用用
NativeMethodAccessorImpl(JNI,启动快),超过阈值(sun.reflect.inflationThreshold=15)后inflation 动态生成字节码版DelegatingClassLoader实现,之后调用接近直接调用速度 - 参数装箱/拆箱与返回值装箱带来额外开销
这解释了反射慢的两个来源:元数据解析与安全检查、基本类型装箱。也解释了为什么”反射慢”要加限定:热点路径上经过 inflation 后的反射调用开销已很小,真正贵的是首次解析与装箱。
使用注意
- 性能:缓存 Method/Field 对象避免重复查找;热点循环慎用反射或改用 MethodHandle/LambdaMetafactory
- 安全:
setAccessible(true)绕过封装,JDK9+ 模块系统对 JDK 内部 API 的反射访问默认拒绝(–add-opens 例外) - 破坏不可变性:反射改 final 字段、改枚举实例是重大隐患,序列化框架也只在必要时这么做
- 异常语义:反射把目标异常包进
InvocationTargetException,排查时要拆 cause
反射是”把类型信息变成数据”的机制:框架因此获得了对未知类型的操作能力,代价是封装被打开、性能有边界。理解 inflation 与装箱这两个细节,就能准确判断反射在自己的代码里到底贵不贵。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 非鱼小站!
评论
WalineDisqus











