在 Java语言中,反射是一种强大而优秀的机制,通过反射,我们可以在运行时检查和修改类、接口、字段和方法的信息,甚至动态地创建对象、调用方法和访问私有成员。
可以毫不夸张地说,没有反射,很多优秀的框架不复存在,没有这些优秀的框架(比如Spring),Java可能会逊色很多,因此,这篇文章,我们一起来深入探讨Java反射以及其背后的原理。
一、什么是反射
先看看 Oracle官方对java反射的说明:
Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
它是通过 Java反射 API 来实现,其中最核心的类位于 java.lang.reflect 包下,如 Class、Constructor、Field 和 Method等,这些类提供了对类和对象的运行时信息进行检查和操作的方法。如下图,展示了 JDK源码中 java.lang.reflect 包所有的类:
二、反射的原理
反射的原理主要可以从下面 4个点来阐述:
- 类加载:当 Java程序运行时,类加载器会根据类的名称查找并加载类的字节码文件,然后将字节码文件转换为可执行的 Java类,并将其存储在运行时数据区域的方法区中。
- 创建 Class对象:在类加载过程中,Java虚拟机会自动创建对应的Class对象,Class对象包含了类的元数据信息,并提供了访问和操作类的接口。
- 获取 Class对象:Class对象通过多种方式获取,最常见的方式有 3种: 类的 .class属性、类实例的 getClass()方法、Class.forName()。
- 访问和操作:通过Class对象获取类的字段、方法、构造函数等信息,使用Field类和Method类来访问和操作字段和方法,甚至可以调用私有的字段和方法。
通过上述的分析可以看出:反射机制需要基于Java虚拟机对类的加载、存储和访问机制的支持,通过反射,可以在运行时动态地探索和操作类的信息,实现灵活的编程和代码的动态行为。
三、如何使用反射
在讲解了 Java反射原理之后,我们通过一个真实的例子来展示如何使用 Java反射机制。如下示例 demo,通过反射给 Person 类中的 greet() 方法传入一个 name,然后输出:
过程分析:
- 首先,在示例代码通过获Person.class取了 Person的Class对象;
- 然后,使用clazz.getName()获取了类的名称,通过clazz.getModifiers()获取了类的修饰符,并打印输出;
- 接下来,通过clazz.getDeclaredMethods()获取类的所有方法,并依次打印输出方法的名称;
- 接着,通过clazz.getDeclaredConstructor().newInstance()方法创建了 Person 的实例;
- 再接着,使用clazz.getDeclaredMethod()方法获取了 greet()方法的引用。为了调用私有方法,我们需要调用setAccessible(true)来设置方法的可访问性。
- 最后,使用Method.invoke()方法调用了 greet()方法,传递参数name = Java。
运行示例结果如下图:
上述示例,我们通过详细的步骤展示了如何使用反射获取类的信息和动态调用方法。你也可以尝试在 Person 中添加更多的方法和字段,并使用反射来获取和操作它们。
四、部分源码解读
在上述示例讲解时,最后是调用 Method.invoke() 实现 Person.greet()的调用,因此,这里我们主要分析 invoke()方案,官方源码截图:
从上面源码截图看出:Method.invoke() 方法,真实返回的是接口 MethodAccessor.invoke()方法。MethodAccessor 接口有三个实现类,具体是调用哪个类的 invoke 方法?
进入acquireMethodAccessor方法,可以看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法决定。
再进入 DelegatingMethodAccessorImpl 的 invoke方法:
DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子类NativeMethodAccessorImpl重写,这时候返回的是native invoke0,如下图:
跟到源码最后可以发现:Method.invoke()方法最终调用 native的invoke0(),应用层面的操作最终转换成对操作系统 c/c++方法的调用。
五、反射优缺点
上面内容的讲解已经侧面反映出了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反射机制动态地创建代理对象,并在代理对象中添加额外的逻辑,从而实现对目标对象的增强。
- 框架扩展和定制: Spring通过反射机制来实现对应用程序的扩展和定制的。例如,Spring提供了BeanPostProcessor接口,允许开发人员在 Bean初始化前后插入自定义逻辑,这是通过反射来实现的。
另外,还有一些耳熟能详的框架也使用了Java反射:
- JUnit:JUnit是一个优秀的单元测试框架,它利用了 Java反射机制动态地加载和执行测试方法。
- Jackson:Jackson是一个 JSON处理的 Java库,它利用反射来实现 JSON与 Java对象之间的转换,动态读取和写入 Java对象的属性,并将其转换为 JSON格式。
- Hibernate ORM:Hibernate和 MyBatis一样,都是对象关系映射框架,通过反射来实现对象与数据库表之间的映射关系。
八、总结
本文讲解了Java反射的原理和使用方式,因为有了Java反射,很多优秀的框架应运而生,从而使得 Java 生态越来越完善,因此,反射是绝大多数框架的基石。
Java反射有优点也有缺点,从整体上看,Java反射是以牺牲了小部分的性能换取了更好的扩展性和灵活性,牺牲小我成就大我,而且,随着现代硬件设备能力越来越强,这点小性能的牺牲是完全值得的。
掌握Java反射,我们可以更好的理解一些优秀框架的运行机制,比如:Spring。它可以帮助我们更好的使用框架,遇到问题时也能更好的去分析和解决。