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配置/注解(如
- 示例:
Spring内部通过反射解析上述配置,动态创建<!-- Spring XML配置 --> <bean id="userService" class="com.example.UserService"> <property name="userDao" ref="userDao"/> </bean>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))。
- 通过反射获取实体类的所有字段(
- 示例:
MyBatis内部通过反射调用该接口方法,并将结果映射为// MyBatis Mapper接口 @Select("SELECT id, name FROM user WHERE id = #{id}") User selectUserById(int id);User对象。
二、注解处理
1. 单元测试框架(JUnit)
- 作用:自动识别并执行带
@Test注解的方法。 - 原理:
- 扫描测试类,通过反射获取所有方法(
Class.getDeclaredMethods())。 - 检查方法是否标注
@Test(Method.isAnnotationPresent(Test.class))。 - 对标注方法通过
Method.invoke()执行测试。
- 扫描测试类,通过反射获取所有方法(
2. 自定义注解实现业务逻辑
- 场景:权限校验、日志记录、参数校验等。
示例:自定义
@RequirePermission注解,结合反射实现接口权限控制:// 自定义注解 @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动态代理示例:
// 目标接口 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. 动态加载配置类
- 场景:根据配置文件动态切换数据源、策略类等。
- 示例:
String 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):可调用有参构造器(推荐)。Constructor<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 目标模块;。