引言
Java反射是Java语言中的一种动态机制,它允许在运行时检查和操作类的结构和行为。反射的强大功能使得程序可以在运行时动态加载类、调用方法和访问字段,从而极大地增强了Java程序的灵活性和扩展性。
反射的基本概念
反射(Reflection)是指程序在运行时能够自我检查和操作自身的能力。通过反射,可以获取类的构造器、方法、字段等信息,并能动态调用对象的方法、设置或获取对象的字段值。
反射关键信息
Class
: 代表类的实体,在运行时加载类时会创建对应的Class对象。Constructor
: 代表类的构造方法。Method
: 代表类的方法。Field
: 代表类的字段。
Java反射
最核心的类位于JDK源码 java.lang.reflect
包下,比如Class、Constructor、Field 和 Method
等,他们提供了对类和对象运行时信息进行检查和操作的方法。
反射基本原理
Java反射的核心在于Class
类,它包含了关于类的所有信息。在Java虚拟机(JVM
)加载类时,会为每个类创建一个对应的Class
对象,该对象保存了类的元数据。通过这些元数据,程序可以在运行时获取类的详细信息并进行操作。
主要可以从下面 4个点来阐述:
- 类加载:当 Java程序运行时,类加载器会根据类的名称查找并加载类的字节码文件,然后将字节码文件转换为可执行的 Java类,并将其存储在运行时数据区域的方法区中。
- 创建 Class对象:在类加载过程中,Java虚拟机会自动创建对应的
Class对象
,Class对象包含了类的元数据信息,并提供了访问和操作类的接口。 - 获取 Class对象:Class对象通过多种方式获取,最常见的方式有 3种: 类的 .class属性、类实例的 getClass()方法、Class.forName()。
- 访问和操作:通过
Class对象
获取类的字段、方法、构造函数等信息,使用Field
类和Method
类来访问和操作字段和方法,甚至可以调用私有的字段和方法。
通过上述的分析可以看出:反射机制需要基于Java虚拟机
对类的加载、存储和访问机制的支持,通过反射,可以在运行时动态地探索和操作类的信息,实现灵活的编程和代码的动态行为。
反射应用场景
很多优秀的框架内部都使用了Java反射
,这里重点讲解下给 Java打下半壁江山的 Spring生态(Spring Framework,Spring MVC,SpringBoot, SpringCloud...),以 Spring Framework为例:
- 依赖注入(Dependency Injection) : 依赖注入,可以把程序员主动创建对象的事情交给 Spring管理,大大提升了对象创建的灵活性。当我们在配置文件或用注解定义 Bean时,Spring会使用反射来动态地实例化对象,并将依赖的其他对象注入到这些实例中。
- 自动装配(Autowired) : 当 Spring容器启动时,它会扫描应用程序中的所有类,并使用反射来查找和识别带有 @Autowired注解的字段、方法或构造函数。再自动将 Bean注入到需要的位置,实现对象之间的自动连接。
- AOP(Aspect-Oriented Programming) : AOP 利用了动态代理和反射机制。通过定义切面(Aspect)和切点(Pointcut),Spring可以在运行时使用反射来创建代理对象,从而实现横切关注点(cross-cutting concerns)的功能,如日志记录、事务管理等。
- 动态代理(Dynamic Proxy) : Spring利用 Java反射机制动态地创建代理对象,并在代理对象中添加额外的逻辑,从而实现对目标对象的增强。
- 框架扩展和定制: Spring通过反射机制来实现对应用程序的扩展和定制的。例如,Spring提供了BeanPostProcessor接口,允许开发人员在 Bean初始化前后插入自定义逻辑,这是通过反射来实现的。
另外,还有一些耳熟能详的框架也使用了Java反射
:
- JUnit:JUnit是一个优秀的单元测试框架,它利用了 Java反射机制动态地加载和执行测试方法。
- Jackson:Jackson是一个 JSON处理的 Java库,它利用反射来实现 JSON与 Java对象之间的转换,动态读取和写入 Java对象的属性,并将其转换为 JSON格式。
- Hibernate ORM:Hibernate和 MyBatis一样,都是对象关系映射框架,通过反射来实现对象与数据库表之间的映射关系。
总结以下几点:
- 框架设计: 许多Java框架(如Spring、Hibernate)广泛使用反射来实现依赖注入、面向切面编程等功能。
- 调试和测试: 反射允许动态访问和修改对象,方便调试和测试私有方法和字段。
- 动态代理: 通过反射实现动态代理,增强程序的灵活性和可扩展性。
- 类浏览器和可视化工具: 反射帮助开发工具展示类的结构和关系。
反射基本使用
获取类的Class对象
Class> clazz = Class.forName("com.example.MyClass");
// 或者
Class> clazz = MyClass.class;
// 或者
Class> clazz = myObject.getClass();
获取构造方法并实例化对象
Constructor> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance("example");
获取和调用方法
Method method = clazz.getMethod("myMethod", String.class);
method.invoke(instance, "Hello");
获取和修改字段
Field field = clazz.getDeclaredField("myField");
// 允许访问私有字段
field.setAccessible(true);
field.set(instance, "New Value");
String value = (String) field.get(instance);
反射工具类
@Slf4j
public class ReflectionUtil {
/**
* 获取属性名以及对应的属性值
*
* @param o 对象
* @return map
*/
public static Map getFieldNameAndValue(Object o) {
Map resMap = new LinkedHashMap<>();
Class> clazz = o.getClass();
while (Objects.nonNull(clazz)) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
if (Objects.isNull(resMap.get(fieldName))) {
resMap.put(fieldName, getFieldValueByName(fieldName, o));
}
}
clazz = clazz.getSuperclass();
}
return resMap;
}
/**
* 获取属性名以及对应的属性值
*
* @param list 对象数组
* @return list\
反射源码解读
反射的实现依赖于JVM提供的本地方法接口(JNI),通过调用本地方法实现对类信息的获取和操作。
获取Class对象的源码
public static Class> forName(String className) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
return classLoader.loadClass(className);
}
调用方法的源码
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
从源码可以看出:Method.invoke()
方法,真实返回的是接口MethodAccessor.invoke()
方法。MethodAccessor
接口有三个实现类,具体是调用哪个类的 invoke 方法?
跟到源码最后可以发现:Method.invoke()
方法最终调用 native的invoke0()
,应用层面的操作最终转换成对操作系统 c/c++方法的调用。
反射优缺点
优点
- 灵活性: 反射允许在运行时动态操作类,提高了程序的灵活性和扩展性。
- 动态代理: 通过反射可以实现动态代理机制,广泛应用于AOP(面向切面编程)等领域。
- 通用性: 反射可以用来编写通用的框架和库,增强代码的重用性。
缺点
- 性能开销: 反射操作较为耗时,可能会影响程序性能。
- 安全问题: 反射可以绕过访问控制,修改私有字段和方法,可能引发安全问题。
- 代码复杂性: 使用反射可能增加代码的复杂性和维护难度。
为什么需要反射
反射机制
在 Java中的作用不言而喻,下面列举了反射机制的一些常见场景和原因:
- 运行时类型检查:反射机制允许在运行时获取类的信息,包括字段、方法和构造方法等。因此,在进行运行时类型检查,以确保代码在处理不同类型的对象时能够正确地进行操作。
- 动态创建对象:通过反射,可以在运行时动态地创建对象,而不需要在编译时知道具体的类名。这对于某些需要根据条件或配置来创建对象的情况非常有用,例如工厂模式或依赖注入框架。
- 访问和修改私有成员:反射机制可以绕过访问权限限制,访问和修改类的私有字段和方法。虽然这破坏了封装性原则,但在某些特定情况下,这种能力可以帮助我们进行一些特殊操作,例如单元测试、调试或框架的内部实现。
- 动态调用方法:反射机制允许我们在运行时动态地调用类的方法,甚至可以根据运行时的条件来选择不同的方法。这对于实现插件化系统、处理回调函数或实现动态代理等功能非常有用。
- 框架和库的实现:许多Java框架和库在其实现中广泛使用了反射机制。它们利用反射来自动发现和加载类、实现依赖注入、处理注解、配置文件解析和动态代理等。反射机制使得这些框架和库更加灵活和扩展。
总结
Java反射是一个强大的工具,极大地增强了Java语言的动态性和灵活性。然而,在使用反射时需要权衡其性能开销和安全风险。Java反射
有优点也有缺点,从整体上看,Java反射
是以牺牲了小部分的性能换取了更好的扩展性和灵活性,牺牲小我成就大我
,而且,随着现代硬件设备能力越来越强,这点小性能的牺牲是完全值得的。理解反射的原理和使用场景,可以更好地应用反射技术来解决实际开发中的问题。