Java之反射
前言:
在 Java语言中,反射是一种强大而优秀的机制,通过反射,我们可以在运行时检查和修改类、接口、字段和方法的信息,甚至动态地创建对象、调用方法和访问私有成员。
可以毫不夸张地说,没有反射,很多优秀的框架不复存在,没有这些优秀的框架(比如Spring),Java可能会逊色很多,因此,这篇文章,我们一起来深入探讨Java反射以及其背后的原理。
什么是反射?
- Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
- 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
Java反射最核心的类位于JDK源码 java.lang.reflect包下,比如Class、Constructor、Field 和 Method等,他们提供了对类和对象运行时信息进行检查和操作的方法。JDK源码截图如下:
反射的原理
反射的原理主要可以从下面 4个点来阐述:
- 类加载:当 Java程序运行时,类加载器会根据类的名称查找并加载类的字节码文件,然后将字节码文件转换为可执行的 Java类,并将其存储在运行时数据区域的方法区中。
- 创建 Class对象:在类加载过程中,Java虚拟机会自动创建对应的Class对象,Class对象包含了类的元数据信息,并提供了访问和操作类的接口。
- 获取 Class对象:Class对象通过多种方式获取,最常见的方式有 3种: 类的 .class属性、类实例的 getClass()方法、Class.forName()。
- 访问和操作:通过Class对象获取类的字段、方法、构造函数等信息,使用Field类和Method类来访问和操作字段和方法,甚至可以调用私有的字段和方法。
发射的特点
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成动态代理
如何使用反射?
了解了Java反射原理,我们再通过一个例子来演示如何使用它,如下示例,通过反射给 Person类中的 greet()方法传入一个 name变量,并打印结果:
public class ReflectionDemo {
class Person{
private void getName(String name){
System.out.println("Hi,My City name is " + name +"~");
}
}
//通过反射获取person中getName属性
public static void main(String[] args) throws Exception {
//获取类的Class对象
Class<Person> clazz = Person.class;
//获取类的名称
String className = clazz.getName();
System.out.println("Class name" +className);
//获取类的修饰符
int modifiers = clazz.getModifiers();
System.out.println("Modifiers" +modifiers);
//获取类的方法信息
Method[] methods = clazz.getDeclaredMethods();
System.out.println("Methods");
for (Method method : methods) {
System.out.println("Method name" +method.getName());
}
//创建类的实例
Object obj = clazz.getDeclaredConstructor().newInstance();
//调用类的方法
Method getMethod = clazz.getDeclaredMethod("getName", String.class);
getMethod.setAccessible(true);
getMethod.invoke(obj,"nanjing");
}
}
过程分析:
- 首先,在示例代码通过获Person.class取了 Person的Class对象;
- 然后,使用clazz.getName()获取了类的名称,通过clazz.getModifiers()获取了类的修饰符,并打印输出;
- 接下来,通过clazz.getDeclaredMethods()获取类的所有方法,并依次打印输出方法的名称;
- 接着,通过clazz.getDeclaredConstructor().newInstance()方法创建了 Person 的实例;
- 再接着,使用clazz.getDeclaredMethod()方法获取了 greet()方法的引用。为了调用私有方法,我们需要调用setAccessible(true)来设置方法的可访问性。
- 最后,使用Method.invoke()方法调用了 greet()方法,传递参数name = Java。
运行结果如下图:
反射优缺点
上面内容的讲解已经侧面反映出了Java反射的一些优点,这里再详细的总结下反射的优缺点:
优点:
- 动态性:反射允许我们在运行时动态地获取和操作类的信息,而不需要在编译时确定。这为编写灵活的、可扩展的代码提供了便利。
- 灵活性:通过反射,我们可以绕过访问修饰符的限制,访问和修改私有成员、调用私有方法等。这为我们在特殊情况下进行一些高级操作提供了可能。
- 框架开发:反射在开发框架和库时非常有用。通过反射,框架可以动态地加载和实例化类,解析注解,处理回调等。这为框架提供了更大的灵活性和可扩展性。
- 调试和探索:反射使得我们可以在运行时探索代码背后的信息,例如获取类的结构、方法、字段等。这对于调试和理解复杂的代码非常有帮助。
缺点:
- 性能开销:相比于直接调用代码,使用反射会带来更高的性能开销。反射涉及到动态查找、方法调用等操作,这些操作比直接调用代码更加耗时。因此,在对性能要求较高的场景下,过度使用反射可能导致性能下降。
- 安全性和稳定性:反射打破了封装性和类型安全性,通过反射,我们可以绕过访问修饰符的限制,调用私有方法等,这可能导致代码的不稳定性和安全隐患。因此,使用反射时需要格外小心,确保代码的正确性和稳定性。
为什么需要反射?
反射机制在 Java中的作用不言而喻,下面列举了反射机制的一些常见场景和原因:
运行时类型检查:反射机制允许在运行时获取类的信息,包括字段、方法和构造方法等。因此,在进行运行时类型检查,以确保代码在处理不同类型的对象时能够正确地进行操作。
动态创建对象:通过反射,可以在运行时动态地创建对象,而不需要在编译时知道具体的类名。这对于某些需要根据条件或配置来创建对象的情况非常有用,例如工厂模式或依赖注入框架。
访问和修改私有成员:反射机制可以绕过访问权限限制,访问和修改类的私有字段和方法。虽然这破坏了封装性原则,但在某些特定情况下,这种能力可以帮助我们进行一些特殊操作,例如单元测试、调试或框架的内部实现。
动态调用方法:反射机制允许我们在运行时动态地调用类的方法,甚至可以根据运行时的条件来选择不同的方法。这对于实现插件化系统、处理回调函数或实现动态代理等功能非常有用。
框架和库的实现:许多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反射机制动态地创建代理对象,并在代理对象中添加额外的逻辑,从而实现对目标对象的增强。
总结
java反射,我们可以更好的理解一些优秀框架运行机制的同时,也能更好的运用框架到我们的项目中。