Skip to content

反射详解

字数: 0 字 时长: 0 分钟

1.前言

大家好,我是Leo哥🫣🫣🫣,今天一起来回顾一下反射机制。

2.什么是反射

反射(Reflection) 是编程语言中的一种能力,它允许程序在运行时动态地检查和操作程序元素,比如类、方法、字段等。通俗地说,反射就是程序能够在运行时查看自己并做出相应调整的能力。

如果了解过框架底册的话,那么对反射一定不陌生。

反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。

假设你有一辆汽车,通常你需要知道汽车的型号、颜色等信息来驾驶它。但如果汽车具备 反射 能力,它自己就能告诉你它的型号、颜色,甚至你可以通过特定的方法直接改变它的某些属性。编程中的反射就是这样一种机制,让程序能够动态地查看和修改自身。

3.反射的场景

平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。

但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring、Spring Boot、MyBatis 等等框架中都大量使用了反射机制。

其实反射有许多具体的应用场景的。

反射是一种强大的工具,它允许程序在运行时检查和操作类的结构,包括类的属性、方法和构造函数。反射有许多具体的应用场景,以下是一些常见的应用场景和示例:

3.1 动态对象创建

反射可以根据类名在运行时创建对象实例。这对于工厂模式或需要根据配置文件创建实例的场景非常有用。

示例代码:

Java
Class<?> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();

3.2 动态方法调用

反射允许在运行时调用对象的方法,这在需要根据配置或用户输入决定调用哪个方法时特别有用。

示例代码:

Java
Method method = clazz.getMethod("myMethod", String.class);
method.invoke(instance, "Hello");

3.3 访问和修改私有字段

反射可以绕过访问控制机制访问和修改私有字段。这对于调试、框架开发和需要操作内部状态的场景非常有用。

示例代码:

Java
Field field = clazz.getDeclaredField("privateField");
field.setAccessible(true);
field.set(instance, "newValue");

3.4 框架和库的实现

许多流行的框架和库(如 Spring、Hibernate)使用反射来实现依赖注入、对象关系映射和自动化测试等功能。

Spring 示例:

Spring 使用反射来自动注入依赖项,例如:

Java
@Autowired
private MyService myService;

Spring 在运行时使用反射来查找和注入 MyService 的实例。

3.5 动态代理

Java 动态代理机制基于反射,允许在运行时创建接口的代理实例。动态代理常用于 AOP(面向切面编程)和拦截器模式。

示例代码:

Java
InvocationHandler handler = new MyInvocationHandler(targetObject);
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
    targetObject.getClass().getClassLoader(),
    targetObject.getClass().getInterfaces(),
    handler
);

其实注解中也大量使用到了反射机制,例如,我们比较熟悉的Spring框架,我们通过使用一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?

这些都是因为反射机制,获取到类/属性/方法/方法的参数上的注解。你获取到注解之后就可以做进一步处理了。

4.反射的基础

简单来说,反射就是为把Java类中的各种成分映射成一个个的Java对象。

例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。

这里我们首先需要理解 Class类,以及类的加载机制。 然后基于此我们如何通过反射获取Class类以及类中的成员变量、方法、构造方法等。

5.认识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对象)。

image-20240620134343301

通过Class的源码,我们可以得出以下结论:

  • Class类也是类的一种,与class关键字是不一样的。
  • Class<T>类实现了多个接口,包括SerializableGenericDeclarationTypeAnnotatedElement,并且不能被继承(使用了final关键字)。
  • 定义了三个私有静态常量ANNOTATIONENUMSYNTHETIC,分别用于标识注解、枚举和合成类。
  • Class类有一个私有构造函数,只有JVM能够调用,用于初始化Class对象,防止默认构造函数的生成。
  • 私有构造函数通过参数ClassLoader初始化classLoader字段,确保其非空以避免JIT优化问题。

6.认识类加载

6.1 类加载机制

Java类的加载过程包括加载、验证、准备、解析、初始化、使用和卸载七个步骤。加载阶段将类字节码加载到内存中,验证阶段确保类的字节码符合规范,准备阶段为静态变量分配内存,解析阶段将符号引用转换为直接引用,初始化阶段执行类的初始化代码,使用阶段允许类实例化和使用,最后在卸载阶段回收类的内存。

image-20240620134403072

6.2 类的加载流程

首先就是编译过程。Java源代码文件(如Person.javaCar.java)被编译成字节码文件(如Person.classCar.class)。

然后就是进入了类加载,**类加载器(Class Loader)**负责加载字节码文件。字节码文件可以来自本地文件系统、网络等不同来源。

在方法区中,类加载器将加载的类字节码文件存储到方法区中,并为每个类创建一个对应的数据结构(如Person类数据结构Car类数据结构)。

在堆区中,JVM在堆区中为每个类创建一个唯一的Class对象(如Person Class对象Car Class对象),用于表示该类的元数据。

最后,当程序创建类的实例时(如Person类的实例P1P2等,和Car类的实例C1C2等),这些实例对象在堆区中被分配内存。每个实例对象通过其Class对象获取类的元数据,从而实现反射机制,即可以获取类的内部信息(如方法、属性、构造函数等)以及反向控制实例对象的能力。

通过下面这张图你能更清晰的了解到类加载机制。

image-20240620134425190

7.学会使用反射

7.1 获取Class对象的几种方式

要通过反射获取具体的信息,那么首先需要获取到Class对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。

1. 通过类名(静态方式)

这是获取 Class 对象最常用和最简单的一种方式,适用于在编译时已知类的情况下。

Java
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 可以直接获取 MyClassClass 对象。
  • 这种方式在编译时检查类的存在和合法性,如果类不存在或拼写错误,编译时就会报错。

2. 通过对象的 getClass() 方法(动态方式)

当你已经有一个类的实例对象时,可以通过该对象的 getClass() 方法获取其 Class 对象。

Java
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() 方法(动态加载)

这种方式适用于需要在运行时根据类名字符串动态加载类的场景。

Java
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)

这种方式用于需要自定义类加载器的场景,例如在开发插件系统或模块化系统时。

Java
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() 方法可以加载指定类。
  • 这种方式适用于需要自定义类加载逻辑的场景。

7.2 API基本使用

image-20240620134535255

image-20240620134602235

Java
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();
        }
    }
}

8.反射的基本原理

首先我们编写的 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 文件不存在,也可以在运行时加载并使用反射机制操作。

本站访客数 人次 本站总访问量