✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉
🍎个人主页:Leo的博客
💞当前专栏:Java从入门到精通
✨特色专栏:MySQL学习
🥭本文内容:反射机制详解
📚个人知识库: Leo知识库,欢迎大家访问
目录
- 前言
- 什么是反射
- 反射的场景
- 1. 动态对象创建
- 示例代码:
- 2. 动态方法调用
- 示例代码:
- 3. 访问和修改私有字段
- 示例代码:
- 4. 框架和库的实现
- Spring 示例:
- 5. 动态代理
- 示例代码:
- 反射的基础
- 认识Class类
- 认识类加载
- 类加载机制
- 类的加载流程
- 学会使用反射
- 获取Class对象的几种方式
- 1. 通过类名(静态方式)
- 2. 通过对象的 `getClass()` 方法(动态方式)
- 3. 通过 `Class.forName()` 方法(动态加载)
- 4. 通过类加载器(ClassLoader)
- 说明:
- API基本使用
- 反射的基本原理
前言
大家好,我是Leo哥🫣🫣🫣,今天一起来回顾一下反射机制。
什么是反射
反射(Reflection) 是编程语言中的一种能力,它允许程序在运行时动态地检查和操作程序元素,比如类、方法、字段等。通俗地说,反射就是程序能够在运行时查看自己并做出相应调整的能力。
如果了解过框架底册的话,那么对反射一定不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
假设你有一辆汽车,通常你需要知道汽车的型号、颜色等信息来驾驶它。但如果汽车具备 反射 能力,它自己就能告诉你它的型号、颜色,甚至你可以通过特定的方法直接改变它的某些属性。编程中的反射就是这样一种机制,让程序能够动态地查看和修改自身。
反射的场景
平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring、Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
其实反射有许多具体的应用场景的。
反射是一种强大的工具,它允许程序在运行时检查和操作类的结构,包括类的属性、方法和构造函数。反射有许多具体的应用场景,以下是一些常见的应用场景和示例:
1. 动态对象创建
反射可以根据类名在运行时创建对象实例。这对于工厂模式或需要根据配置文件创建实例的场景非常有用。
示例代码:
Class<?> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();
2. 动态方法调用
反射允许在运行时调用对象的方法,这在需要根据配置或用户输入决定调用哪个方法时特别有用。
示例代码:
Method method = clazz.getMethod("myMethod", String.class);
method.invoke(instance, "Hello");
3. 访问和修改私有字段
反射可以绕过访问控制机制访问和修改私有字段。这对于调试、框架开发和需要操作内部状态的场景非常有用。
示例代码:
Field field = clazz.getDeclaredField("privateField");
field.setAccessible(true);
field.set(instance, "newValue");
4. 框架和库的实现
许多流行的框架和库(如 Spring、Hibernate)使用反射来实现依赖注入、对象关系映射和自动化测试等功能。
Spring 示例:
Spring 使用反射来自动注入依赖项,例如:
@Autowired
private MyService myService;
Spring 在运行时使用反射来查找和注入 MyService
的实例。
5. 动态代理
Java 动态代理机制基于反射,允许在运行时创建接口的代理实例。动态代理常用于 AOP(面向切面编程)和拦截器模式。
示例代码:
InvocationHandler handler = new MyInvocationHandler(targetObject);
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(),
handler
);
其实注解中也大量使用到了反射机制,例如,我们比较熟悉的Spring框架,我们通过使用一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?
这些都是因为反射机制,获取到类/属性/方法/方法的参数上的注解。你获取到注解之后就可以做进一步处理了。
反射的基础
简单来说,反射就是为把Java类中的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
这里我们首先需要理解 Class类,以及类的加载机制。 然后基于此我们如何通过反射获取Class类以及类中的成员变量、方法、构造方法等。
认识Class类
在 Java 反射中,Class
类是核心部分。它表示正在运行的 Java 应用程序中的类或接口。每个类或接口都有一个与之对应的 Class
对象。当 JVM 加载某个类时,它会创建一个 Class
对象来表示这个类。
Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class ans enum)或接口(interface and annotation)(每个Java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName(“类名”)等方法获取Class对象)。
通过Class的源码,我们可以得出以下结论:
- Class类也是类的一种,与class关键字是不一样的。
Class<T>
类实现了多个接口,包括Serializable
、GenericDeclaration
、Type
和AnnotatedElement
,并且不能被继承(使用了final
关键字)。- 定义了三个私有静态常量
ANNOTATION
、ENUM
和SYNTHETIC
,分别用于标识注解、枚举和合成类。 - Class类有一个私有构造函数,只有JVM能够调用,用于初始化
Class
对象,防止默认构造函数的生成。 - 私有构造函数通过参数
ClassLoader
初始化classLoader
字段,确保其非空以避免JIT优化问题。
认识类加载
类加载机制
Java类的加载过程包括加载、验证、准备、解析、初始化、使用和卸载七个步骤。加载阶段将类字节码加载到内存中,验证阶段确保类的字节码符合规范,准备阶段为静态变量分配内存,解析阶段将符号引用转换为直接引用,初始化阶段执行类的初始化代码,使用阶段允许类实例化和使用,最后在卸载阶段回收类的内存。
类的加载流程
首先就是编译过程。Java源代码文件(如Person.java
和Car.java
)被编译成字节码文件(如Person.class
和Car.class
)。
然后就是进入了类加载,类加载器(Class Loader) 负责加载字节码文件。字节码文件可以来自本地文件系统、网络等不同来源。
在方法区中,类加载器将加载的类字节码文件存储到方法区中,并为每个类创建一个对应的数据结构(如Person类数据结构
和Car类数据结构
)。
在堆区中,JVM在堆区中为每个类创建一个唯一的Class
对象(如Person Class对象
和Car Class对象
),用于表示该类的元数据。
最后,当程序创建类的实例时(如Person
类的实例P1
、P2
等,和Car
类的实例C1
、C2
等),这些实例对象在堆区中被分配内存。每个实例对象通过其Class
对象获取类的元数据,从而实现反射机制,即可以获取类的内部信息(如方法、属性、构造函数等)以及反向控制实例对象的能力。
通过下面这张图你能更清晰的了解到类加载机制。
学会使用反射
获取Class对象的几种方式
要通过反射获取具体的信息,那么首先需要获取到Class对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。
1. 通过类名(静态方式)
这是获取 Class
对象最常用和最简单的一种方式,适用于在编译时已知类的情况下。
public class MyClass {
// 类的定义
}
public class Main {
public static void main(String[] args) {
Class<?> clazz = MyClass.class;
System.out.println("Class name: " + clazz.getName());
}
}
- 使用
MyClass.class
可以直接获取MyClass
的Class
对象。 - 这种方式在编译时检查类的存在和合法性,如果类不存在或拼写错误,编译时就会报错。
2. 通过对象的 getClass()
方法(动态方式)
当你已经有一个类的实例对象时,可以通过该对象的 getClass()
方法获取其 Class
对象。
public class MyClass {
// 类的定义
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
Class<?> clazz = obj.getClass();
System.out.println("Class name: " + clazz.getName());
}
}
- 使用实例对象的
getClass()
方法可以获取该对象的Class
对象。 - 这种方式适用于在运行时已经有类实例的情况下。
3. 通过 Class.forName()
方法(动态加载)
这种方式适用于需要在运行时根据类名字符串动态加载类的场景。
public class Main {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("com.example.MyClass");
System.out.println("Class name: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
- 使用
Class.forName("com.example.MyClass")
可以动态加载MyClass
类。 - 这种方式适用于在运行时需要根据类名字符串加载类的场景,例如配置文件中指定类名。
4. 通过类加载器(ClassLoader)
这种方式用于需要自定义类加载器的场景,例如在开发插件系统或模块化系统时。
public class Main {
public static void main(String[] args) {
try {
ClassLoader classLoader = Main.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
System.out.println("Class name: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
说明:
- 使用类加载器的
loadClass()
方法可以加载指定类。 - 这种方式适用于需要自定义类加载逻辑的场景。
API基本使用
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取类的 Class 对象
Class<?> clazz = Class.forName("com.example.MyClass");
// 获取类名
System.out.println("Class Name: " + clazz.getName());
// 获取修饰符
int modifiers = clazz.getModifiers();
System.out.println("Is Public: " + Modifier.isPublic(modifiers));
System.out.println("Is Abstract: " + Modifier.isAbstract(modifiers));
// 获取父类
Class<?> superClass = clazz.getSuperclass();
System.out.println("Superclass: " + superClass.getName());
// 获取实现的接口
Class<?>[] interfaces = clazz.getInterfaces();
System.out.println("Interfaces:");
for (Class<?> iface : interfaces) {
System.out.println(iface.getName());
}
// 获取字段
Field[] fields = clazz.getDeclaredFields();
System.out.println("Fields:");
for (Field field : fields) {
System.out.println(field.getName() + " - " + field.getType().getName());
}
// 获取方法
Method[] methods = clazz.getDeclaredMethods();
System.out.println("Methods:");
for (Method method : methods) {
System.out.println(method.getName() + " - " + method.getReturnType().getName());
}
// 获取构造函数
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
System.out.println("Constructors:");
for (Constructor<?> constructor : constructors) {
System.out.println(constructor.getName());
}
// 创建实例
Constructor<?> constructor = clazz.getConstructor();
Object instance = constructor.newInstance();
System.out.println("Instance: " + instance);
// 调用方法
Method method = clazz.getMethod("methodName", String.class);
method.invoke(instance, "Hello");
// 访问和修改字段
Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true);
field.set(instance, "newValue");
System.out.println("Field Value: " + field.get(instance));
} catch (Exception e) {
e.printStackTrace();
}
}
}
反射的基本原理
首先我们编写的 Java 源文件(.java)被保存在磁盘上。然后,这些源文件通过 Java 编译器(javac)编译生成字节码文件(.class)。在编译过程中,编译器会检查源文件中的语法错误,如果存在语法错误,将无法生成字节码文件。接着,JVM 将这些字节码文件加载到内存中。
Java 是面向对象的编程语言,所有事物都可以被看作对象。当字节码文件被加载到内存中时,JVM 会将其表示为一个 Class 对象。这个 Class 对象包含了与类有关的所有信息。
一旦获得了 Class 对象,就可以通过反射机制操作类中的所有内容。Java 将类的成员变量、构造器、成员方法都看作对象,并将其封装到相应的类中。通过 Class 对象,可以调用相应的方法获取类的成员变量对象、构造器对象、成员方法对象。然后,通过这些对象调用方法,就相当于该类在操作自身的属性、构造器和成员方法。这就是反射的基本原理。
每个类在编译后都会生成一个同名的 .class 文件,该文件保存着 Class 对象的内容。类加载实际上是 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver")
这种方式来控制类的加载,该方法会返回一个 Class 对象。反射提供了运行时获取类信息的能力,允许在运行时加载类,即使在编译时期该类的 .class 文件不存在,也可以在运行时加载并使用反射机制操作。