文章目录
- Java反射
-
- 一、基础概念
-
- [1. 定义](#1. 定义)
- [2. 核心原理](#2. 核心原理)
- 二、核心类库
- 三、基本操作
-
- [1. 获取 Class 对象的三种方式](#1. 获取 Class 对象的三种方式)
- [2. 实例化对象](#2. 实例化对象)
- [3. 访问字段](#3. 访问字段)
- [4. 调用方法](#4. 调用方法)
- [5. 操作构造器](#5. 操作构造器)
- 四、高级特性
-
- [1. 反射与泛型](#1. 反射与泛型)
- [2. 反射与注解](#2. 反射与注解)
- [3. 动态代理](#3. 动态代理)
- 五、应用场景
-
- [1. 框架开发](#1. 框架开发)
- [2. 注解处理](#2. 注解处理)
- [3. 动态扩展](#3. 动态扩展)
- [4. 调试与工具](#4. 调试与工具)
- 六、优缺点分析
-
- [1. 优点](#1. 优点)
- [2. 缺点](#2. 缺点)
- 七、注意事项与最佳实践
-
- [1. 性能优化](#1. 性能优化)
- [2. 安全问题](#2. 安全问题)
- [3. 代码规范](#3. 代码规范)
- [4. 版本兼容性](#4. 版本兼容性)
- Java反射核心应用场景
-
- 一、框架开发(最核心场景)
-
- [1. Spring IoC(控制反转)容器](#1. Spring IoC(控制反转)容器)
- [2. Spring AOP(面向切面编程)](#2. Spring AOP(面向切面编程))
- [3. MyBatis ORM映射](#3. MyBatis ORM映射)
- 二、注解处理
-
- [1. 单元测试框架(JUnit)](#1. 单元测试框架(JUnit))
- [2. 自定义注解实现业务逻辑](#2. 自定义注解实现业务逻辑)
- 三、动态扩展与插件化
-
- [1. Java SPI(Service Provider Interface)机制](#1. Java SPI(Service Provider Interface)机制)
- [2. 插件化开发](#2. 插件化开发)
- 四、动态代理
- 五、调试与工具类
-
- [1. IDE代码补全与类结构查看](#1. IDE代码补全与类结构查看)
- [2. 反射工具库](#2. 反射工具库)
- 六、实际业务场景
-
- [1. 动态加载配置类](#1. 动态加载配置类)
- [2. 对象拷贝(BeanUtils)](#2. 对象拷贝(BeanUtils))
- [3. JSON序列化/反序列化](#3. JSON序列化/反序列化)
- 总结:反射的核心价值
- Java反射-八股文常考面试题
-
- 一、基础概念篇
-
- [1. 什么是Java反射?](#1. 什么是Java反射?)
- [2. 反射的核心原理是什么?](#2. 反射的核心原理是什么?)
- [3. 获取`Class`对象的三种方式(高频)](#3. 获取
Class对象的三种方式(高频))
- 二、核心操作篇
-
- [1. 如何通过反射实例化对象?](#1. 如何通过反射实例化对象?)
- [2. 如何访问/修改字段(Field)?](#2. 如何访问/修改字段(Field)?)
- [3. 如何调用方法(Method)?](#3. 如何调用方法(Method)?)
- [4. `setAccessible(true)`的作用与风险?](#4.
setAccessible(true)的作用与风险?)
- 三、应用场景篇
-
- [1. 反射在框架中的应用(高频)](#1. 反射在框架中的应用(高频))
- [2. 动态代理与反射的关系?](#2. 动态代理与反射的关系?)
- [3. 注解处理(如JUnit `@Test`)](#3. 注解处理(如JUnit
@Test))
- 四、优缺点与性能篇
-
- [1. 反射的优缺点?](#1. 反射的优缺点?)
- [2. 反射为什么慢?如何优化?](#2. 反射为什么慢?如何优化?)
- 五、进阶原理篇
-
- [1. 泛型擦除后,如何通过反射获取泛型信息?](#1. 泛型擦除后,如何通过反射获取泛型信息?)
- [2. JDK动态代理 vs CGLIB动态代理(高频)](#2. JDK动态代理 vs CGLIB动态代理(高频))
- [3. Java 9+模块系统对反射的影响?](#3. Java 9+模块系统对反射的影响?)
Java反射
一、基础概念
1. 定义
Java反射是指在运行时动态获取类的元信息(如类的结构、方法、字段、构造器等),并能动态操作类或对象的能力(如实例化对象、调用方法、修改字段值)。它是Java动态性的核心体现。
2. 核心原理
- Class对象 :反射的入口。每个类被加载后,JVM会自动生成一个
java.lang.Class对象,该对象包含了类的完整元数据。 - 类加载机制 :反射依赖于JVM的类加载过程(加载→链接→初始化),通过
Class对象可逆向访问类的结构信息。
二、核心类库
反射主要依赖java.lang和java.lang.reflect包下的类,核心类如下:
| 类名 | 作用说明 |
|---|---|
java.lang.Class |
反射的入口类,代表类的元数据,可获取类的构造器、方法、字段等信息。 |
Constructor |
代表类的构造方法,可用于实例化对象。 |
Method |
代表类的方法,可用于动态调用方法。 |
Field |
代表类的字段(成员变量),可用于动态获取或修改字段值。 |
Modifier |
工具类,用于解析类、方法、字段的修饰符(如public、static、final)。 |
Array |
工具类,用于动态创建和访问数组。 |
ParameterizedType |
代表参数化类型(如List<String>),可获取泛型的实际类型参数。 |
Annotation |
代表注解,可通过反射获取类、方法、字段上的注解信息。 |
三、基本操作
1. 获取 Class 对象的三种方式
- 类名.class :编译时确定,最安全(如
String.class)。 - 对象.getClass() :通过实例获取(如
"hello".getClass())。 - Class.forName("全限定类名") :动态加载,需处理
ClassNotFoundException(如Class.forName("java.util.ArrayList"))。
2. 实例化对象
- 通过
Class.newInstance():调用无参构造器(Java 9后过时,推荐用Constructor)。 - 通过
Constructor.newInstance(Object... initargs):可调用有参构造器(需先获取Constructor对象)。
3. 访问字段
- 获取字段 :
getField(String name):获取public字段(包括父类)。getDeclaredField(String name):获取所有声明的字段(包括私有,不包括父类)。
- 操作字段值 :
get(Object obj):获取字段值。set(Object obj, Object value):设置字段值。- 私有字段需调用
setAccessible(true)打破封装。
4. 调用方法
- 获取方法 :
getMethod(String name, Class<?>... parameterTypes):获取public方法(包括父类)。getDeclaredMethod(String name, Class<?>... parameterTypes):获取所有声明的方法(包括私有,不包括父类)。
- 调用方法 :
invoke(Object obj, Object... args):执行方法,静态方法obj传null。- 私有方法需调用
setAccessible(true)。
5. 操作构造器
getConstructor(Class<?>... parameterTypes):获取public构造器。getDeclaredConstructor(Class<?>... parameterTypes):获取所有构造器(包括私有)。
四、高级特性
1. 反射与泛型
由于Java泛型在编译时会类型擦除,运行时需通过反射获取泛型信息:
Field.getGenericType():返回字段的泛型类型(如ParameterizedType)。ParameterizedType.getActualTypeArguments():获取泛型的实际类型参数(如List<String>中的String)。
2. 反射与注解
可通过反射获取类、方法、字段上的注解:
getAnnotation(Class<T> annotationClass):获取指定类型的注解。getAnnotations():获取所有注解(包括继承的)。getDeclaredAnnotations():获取直接声明的注解(不包括继承的)。
3. 动态代理
反射是动态代理的基础,核心类:
Proxy:用于创建代理对象。InvocationHandler:处理代理方法的调用逻辑。- 示例:
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)。
五、应用场景
1. 框架开发
- Spring IoC:通过反射实例化Bean,读取配置文件中的类名并动态加载。
- Spring AOP:基于动态代理(反射实现)实现方法拦截。
- MyBatis:通过反射映射SQL结果到Java对象,调用Mapper接口方法。
2. 注解处理
- JUnit :通过反射识别
@Test注解并执行测试方法。 - 自定义注解:结合反射实现权限校验、日志记录等功能。
3. 动态扩展
- SPI机制 :
ServiceLoader通过反射加载配置文件中定义的实现类。 - 插件化开发:动态加载外部Jar包中的类并调用其方法。
4. 调试与工具
- IDE:通过反射提供代码补全、类结构查看等功能。
- 反射工具类 :如Apache Commons Lang的
FieldUtils、MethodUtils。
六、优缺点分析
1. 优点
- 动态性:运行时才确定类和方法,提高代码灵活性。
- 通用性:可编写通用代码处理不同类(如框架的通用工具)。
- 解耦:减少硬编码,便于扩展和维护。
2. 缺点
- 性能开销:比直接调用慢(涉及动态类型解析、安全检查),频繁调用需缓存反射对象。
- 安全风险:可访问私有成员,破坏封装性(需合理控制权限)。
- 可读性差:反射代码晦涩难懂,调试和维护成本高。
七、注意事项与最佳实践
1. 性能优化
- 缓存
Class、Constructor、Method、Field对象,避免重复获取。 - 尽量减少
setAccessible(true)的使用,或仅在初始化时调用一次。
2. 安全问题
- Java 9+模块系统中,若模块未
opens给其他模块,setAccessible会抛出InaccessibleObjectException,需在module-info.java中声明opens。 - 合理使用
SecurityManager(Java 17后默认禁用)限制反射权限。
3. 代码规范
- 避免过度使用反射,能用直接调用则不用反射。
- 处理反射异常(如
IllegalAccessException、InvocationTargetException),避免吞异常。
4. 版本兼容性
- 关注Java版本对反射的调整(如Java 9模块系统、Java 16+对非法反射访问的警告升级为错误)。
Java反射核心应用场景
反射的核心价值在于运行时动态性 与解耦能力,以下是其最经典、最常用的落地场景:
一、框架开发(最核心场景)
1. Spring IoC(控制反转)容器
-
作用:通过反射动态实例化Bean并管理依赖注入,避免硬编码。
-
原理 :
- 读取XML配置/注解(如
@Component、@Bean)中的类全限定名。 - 调用
Class.forName()加载类,通过Constructor.newInstance()实例化对象。 - 通过反射调用
setter方法或直接注入字段(Field.set())完成依赖装配。
- 读取XML配置/注解(如
-
示例 :
xml<!-- Spring XML配置 --> <bean id="userService" class="com.example.UserService"> <property name="userDao" ref="userDao"/> </bean>Spring内部通过反射解析上述配置,动态创建
UserService并注入UserDao。
2. Spring AOP(面向切面编程)
- 作用:基于动态代理(反射实现)实现方法拦截,用于日志、事务、权限控制等横切关注点。
- 原理 :
- JDK动态代理:通过
Proxy.newProxyInstance()创建代理对象,InvocationHandler.invoke()内部用反射调用目标方法。 - CGLIB动态代理:通过字节码生成子类,重写方法时用反射调用父类原方法。
- JDK动态代理:通过
3. MyBatis ORM映射
-
作用 :将SQL查询结果自动映射到Java对象,无需手动
set字段。 -
原理 :
- 通过反射获取实体类的所有字段(
Class.getDeclaredFields())。 - 根据字段名匹配SQL结果集的列名。
- 调用
Field.set()将列值注入到对象字段(私有字段需setAccessible(true))。
- 通过反射获取实体类的所有字段(
-
示例 :
java// MyBatis Mapper接口 @Select("SELECT id, name FROM user WHERE id = #{id}") User selectUserById(int id);MyBatis内部通过反射调用该接口方法,并将结果映射为
User对象。
二、注解处理
1. 单元测试框架(JUnit)
- 作用 :自动识别并执行带
@Test注解的方法。 - 原理 :
- 扫描测试类,通过反射获取所有方法(
Class.getDeclaredMethods())。 - 检查方法是否标注
@Test(Method.isAnnotationPresent(Test.class))。 - 对标注方法通过
Method.invoke()执行测试。
- 扫描测试类,通过反射获取所有方法(
2. 自定义注解实现业务逻辑
-
场景:权限校验、日志记录、参数校验等。
-
示例 :自定义
@RequirePermission注解,结合反射实现接口权限控制:java// 自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); // 所需权限 } // 切面/拦截器中通过反射校验 public void checkPermission(Method method) { if (method.isAnnotationPresent(RequirePermission.class)) { String requiredPerm = method.getAnnotation(RequirePermission.class).value(); // 校验当前用户是否拥有requiredPerm权限 } }
三、动态扩展与插件化
1. Java SPI(Service Provider Interface)机制
- 作用:动态加载外部实现类,实现框架的可扩展性。
- 原理 :
- 在
META-INF/services目录下定义接口文件,内容为实现类全限定名。 ServiceLoader通过反射读取文件,调用Class.forName()加载实现类并实例化。
- 在
- 示例:JDBC驱动加载、Dubbo扩展点加载均基于SPI。
2. 插件化开发
- 场景:IDE插件、应用市场插件、模块化系统。
- 原理 :
- 动态加载外部Jar包(
URLClassLoader)。 - 通过反射获取插件类,调用约定的接口方法(如
Plugin.execute())。
- 动态加载外部Jar包(
四、动态代理
-
作用:在不修改原代码的情况下,对方法进行增强(如日志、监控、事务)。
-
分类 :
- JDK动态代理:基于接口,通过
Proxy和InvocationHandler实现(核心是反射)。 - CGLIB动态代理:基于继承,通过字节码生成子类(内部也依赖反射调用父类方法)。
- JDK动态代理:基于接口,通过
-
JDK动态代理示例 :
java// 目标接口 public interface UserService { void addUser(); } // 目标实现类 public class UserServiceImpl implements UserService { public void addUser() { System.out.println("添加用户"); } } // 调用处理器 public class LogHandler implements InvocationHandler { private Object target; public LogHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("前置日志"); Object result = method.invoke(target, args); // 反射调用目标方法 System.out.println("后置日志"); return result; } } // 创建代理对象 UserService proxy = (UserService) Proxy.newProxyInstance( UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces(), new LogHandler(new UserServiceImpl()) ); proxy.addUser(); // 调用代理方法
五、调试与工具类
1. IDE代码补全与类结构查看
- 原理:IDE通过反射加载项目类,获取类的方法、字段、构造器等信息,实时展示给开发者。
2. 反射工具库
- 场景:简化反射操作,避免重复代码。
- 示例 :
- Apache Commons Lang的
FieldUtils、MethodUtils:封装了字段/方法的获取、设置、调用等操作。 - Spring的
ReflectionUtils:提供findField()、invokeMethod()等便捷方法。
- Apache Commons Lang的
六、实际业务场景
1. 动态加载配置类
-
场景:根据配置文件动态切换数据源、策略类等。
-
示例 :
javaString strategyClass = config.getProperty("payment.strategy"); // 从配置读取类名 PaymentStrategy strategy = (PaymentStrategy) Class.forName(strategyClass).newInstance(); strategy.pay(); // 动态调用策略方法
2. 对象拷贝(BeanUtils)
- 原理:通过反射获取源对象的所有字段,将值复制到目标对象的对应字段。
- 示例 :Spring的
BeanUtils.copyProperties(source, target)、Apache Commons BeanUtils的BeanUtils.copyProperties()。
3. JSON序列化/反序列化
- 原理 :
- 序列化:通过反射获取对象的所有字段,将字段名和值转换为JSON。
- 反序列化:通过反射实例化对象,根据JSON键名匹配字段并注入值。
- 示例:Jackson、Gson等JSON库的核心实现均依赖反射。
总结:反射的核心价值
| 价值点 | 说明 |
|---|---|
| 动态性 | 运行时才确定类、方法、字段,无需编译期硬编码。 |
| 解耦 | 减少类之间的直接依赖,提高代码扩展性(如框架可插拔、策略动态切换)。 |
| 通用性 | 编写通用代码处理不同类(如ORM映射、对象拷贝、JSON序列化)。 |
Java反射-八股文常考面试题
一、基础概念篇
1. 什么是Java反射?
Java反射是指在运行时动态获取类的元信息(如构造器、方法、字段、注解等),并能动态操作类或对象的能力(实例化对象、调用方法、修改字段值)。它是Java动态性的核心体现。
2. 反射的核心原理是什么?
- 每个类被JVM加载后,会自动生成一个
java.lang.Class对象,该对象包含类的完整元数据(结构、方法、字段等)。 - 反射通过
Class对象逆向访问类的信息,无需在编译期确定具体类。
3. 获取Class对象的三种方式(高频)
| 方式 | 示例 | 特点 |
|---|---|---|
| 类名.class | String.class |
编译期确定,最安全 |
| 对象.getClass() | "hello".getClass() |
通过实例获取 |
| Class.forName("全限定名") | Class.forName("java.util.ArrayList") |
动态加载,需处理异常 |
二、核心操作篇
1. 如何通过反射实例化对象?
-
方式1 :
Class.newInstance()(Java 9+过时):调用无参构造器。 -
方式2 :
Constructor.newInstance(Object... args):可调用有参构造器(推荐)。javaConstructor<User> constructor = User.class.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); // 若构造器私有,需打破封装 User user = constructor.newInstance("张三", 25);
2. 如何访问/修改字段(Field)?
- 获取字段 :
getField(String name):获取public字段(含父类)。getDeclaredField(String name):获取所有声明字段(含私有,不含父类)。
- 操作字段 :
get(Object obj):获取字段值。set(Object obj, Object value):设置字段值。- 私有字段需调用
setAccessible(true)。
3. 如何调用方法(Method)?
- 获取方法 :
getMethod(String name, Class<?>... paramTypes):获取public方法(含父类)。getDeclaredMethod(String name, Class<?>... paramTypes):获取所有声明方法(含私有,不含父类)。
- 调用方法 :
invoke(Object obj, Object... args):执行方法,静态方法obj传null。- 私有方法需调用
setAccessible(true)。
4. setAccessible(true)的作用与风险?
- 作用:打破Java的访问修饰符限制,可访问私有成员(构造器、方法、字段)。
- 风险 :
- 破坏封装性,可能导致对象状态不一致。
- Java 9+模块系统中,若模块未
opens给其他模块,会抛出InaccessibleObjectException。
三、应用场景篇
1. 反射在框架中的应用(高频)
- Spring IoC:读取配置文件/注解中的类名,通过反射实例化Bean并管理依赖。
- Spring AOP:基于动态代理(反射实现)实现方法拦截(如日志、事务)。
- MyBatis:通过反射将SQL结果映射到Java对象,调用Mapper接口方法。
2. 动态代理与反射的关系?
- 动态代理的核心是反射,JDK动态代理通过
Proxy.newProxyInstance()创建代理对象,内部依赖InvocationHandler的invoke()方法(通过反射调用目标方法)。
3. 注解处理(如JUnit @Test)
- JUnit通过反射扫描测试类,识别
@Test注解的方法,然后通过反射调用这些方法执行测试。
四、优缺点与性能篇
1. 反射的优缺点?
| 优点 | 缺点 |
|---|---|
| 动态性:运行时确定类和方法,灵活性高 | 性能开销:比直接调用慢 |
| 通用性:可编写通用代码处理不同类 | 安全风险:可访问私有成员,破坏封装 |
| 解耦:减少硬编码,便于扩展 | 可读性差:代码晦涩,维护成本高 |
2. 反射为什么慢?如何优化?
- 慢的原因 :
- 运行时动态类型解析、安全检查(如访问修饰符校验)。
- 每次调用反射方法都需重新查找元数据。
- 优化方式 :
- 缓存
Class、Constructor、Method、Field对象,避免重复获取。 - 尽量减少
setAccessible(true)的使用,或仅在初始化时调用一次。
- 缓存
五、进阶原理篇
1. 泛型擦除后,如何通过反射获取泛型信息?
- Java泛型在编译期会类型擦除,但可通过以下方式获取:
Field.getGenericType():返回字段的泛型类型(如ParameterizedType)。ParameterizedType.getActualTypeArguments():获取泛型的实际类型参数(如List<String>中的String)。
2. JDK动态代理 vs CGLIB动态代理(高频)
| 维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 实现方式 | 基于反射,要求目标类实现接口 | 基于继承,通过字节码生成子类 |
| 限制 | 只能代理实现接口的类 | 可代理普通类(不能是final类) |
| 性能 | 略低(反射调用) | 略高(字节码生成) |
| Spring默认选择 | 目标类实现接口时使用 | 目标类未实现接口时使用 |
3. Java 9+模块系统对反射的影响?
- 模块系统(Project Jigsaw)引入
module-info.java,若模块未通过opens关键字将包开放给其他模块,反射调用setAccessible(true)会抛出InaccessibleObjectException。 - 解决方式:在
module-info.java中声明opens 包名 to 目标模块;。