从 Spring XML 配置的一个问题出发,系统整理反射的本质、类加载三阶段及获取 Class 对象的三种方式。**


一、从一个问题开始

学 Spring 的时候,你一定写过这样的 XML 配置:

<bean id="userDao" class="com.xq.dao.impl.UserDaoImpl"></bean>

Spring 启动之后,UserDaoImpl 的对象就有了,可以直接用。

Spring 框架的代码是别人写好编译好的,它在编译的时候根本不知道你的 com.xq.dao.impl.UserDaoImpl 存不存在。它不可能提前写好:

// Spring 源码里不可能有这行
UserDaoImpl userDao = new UserDaoImpl();

那它是怎么做到的?

在程序运行的时候,拿着 "com.xq.dao.impl.UserDaoImpl" 这个字符串,动态地找到这个类,然后创建出对象。

这就是反射。


二、反射的本质

反射(Reflection)是 Java 提供的一种能力:在程序运行时,动态获取任意类的信息,并对其进行操作。

这里的"类信息"包括:

  • 成员变量(Field)
  • 方法(Method)
  • 构造器(Constructor)
  • 父类、接口、注解……

正常情况下,我们在编译期就确定了要用哪个类、调哪个方法。而反射打破了这个限制——类名可以是运行时才知道的一个字符串,就像 XML 里 class 属性的值一样。

Spring、MyBatis 等框架之所以能做到"配置驱动",底层全部依赖反射。


三、类加载的三个阶段

一个 .java 文件,是怎么一步步变成内存里可以操作的对象的?

阶段一:磁盘阶段

.java 源文件经过 javac 编译,生成 .class 字节码文件存在磁盘上。此时类还没有进入内存。

阶段二:类对象阶段

当程序第一次用到某个类时,JVM 的类加载器(ClassLoader) 把对应的 .class 文件读进内存:

  • 方法区中存储这个类的所有信息(字段、方法、构造器……)
  • 中生成一个对应的 Class 对象,作为访问方法区类信息的入口

这个 Class 对象,就是反射一切操作的起点。

阶段三:运行时阶段

通过 Class 对象,可以调用构造器,在堆里创建出这个类的具体实例——也就是我们平时 new 出来的对象。

.java 文件  →(javac)→  .class 文件  →(类加载器)→  Class 对象  →(反射/new)→  实例对象
  磁盘阶段              磁盘阶段           类对象阶段                运行时阶段

四、获取 Class 对象的三种方式

Java 提供了三种方式,分别对应类加载的不同阶段。

方式一:Class.forName("全类名")

Class<?> clazz = Class.forName("com.xq.dao.impl.UserDaoImpl");

对应磁盘阶段。 类名是一个运行时才确定的字符串,这种方式会触发类加载。这也是 Spring 读取 XML 配置时用的方式——class 属性的值直接传进来就能用。

方式二:类名.class

Class<?> clazz = UserDaoImpl.class;

对应类对象阶段。 编译期就确定了类,直接通过类的字面量获取。常用于已知具体类型的场景,比如获取 String.class 传给反射方法。

方式三:对象.getClass()

UserDaoImpl userDao = new UserDaoImpl();
Class<?> clazz = userDao.getClass();

对应运行时阶段。 已经有了实例对象,从对象身上反过来拿到它的 Class。常用于不确定对象具体类型时(比如方法参数是 Object 类型)。

三种方式对比

方式 语法 适用场景
Class.forName() 传入字符串类名 类名运行时才知道(框架、配置驱动)
类名.class 编译期已知类型 传参、泛型等明确知道类型的场景
对象.getClass() 已有实例 判断对象运行时的真实类型

五、Class 对象的唯一性

不管用哪种方式获取,同一个类在同一个类加载器下,Class 对象永远只有一个

Class<?> c1 = Class.forName("com.xq.dao.impl.UserDaoImpl");
Class<?> c2 = UserDaoImpl.class;
UserDaoImpl obj = new UserDaoImpl();
Class<?> c3 = obj.getClass();

System.out.println(c1 == c2); // true
System.out.println(c2 == c3); // true

三个 == 比较的是对象地址,全部为 true

这是因为类加载器在加载一个类时,会先检查方法区里是否已经存在对应的 Class 对象,如果已经存在就直接复用,不会重复创建。


六、回头看 Spring 的工作流程

现在可以完整描述 Spring XML 配置的底层过程了:

// Spring 源码里,大致是这样处理 XML 里的 bean 配置的:

// 第一步:从 XML 读取 class 属性的值(一个字符串)
String className = "com.xq.dao.impl.UserDaoImpl";

// 第二步:通过反射拿到 Class 对象
Class<?> clazz = Class.forName(className);

// 第三步:通过 Class 对象获取构造器,创建实例
Object instance = clazz.getDeclaredConstructor().newInstance();

// 这个 instance 就是 Spring 帮你创建好、放进容器里的 Bean

现在再看 XML 里的那行配置,是不是完全不一样了?

<bean id="userDao" class="com.xq.dao.impl.UserDaoImpl"></bean>

class 属性里的值,就是传给 Class.forName() 的那个字符串。


七、小结

问题 答案
反射是什么? 运行时动态获取类信息并操作的能力
为什么需要反射? 编译期无法确定类名时(如框架读配置),只能靠反射
反射的起点是什么? Class 对象
怎么获取 Class 对象? forName().classgetClass() 三种方式
同一个类的 Class 对象有几个? 永远只有一个(同一类加载器下)

下一篇:拿到 Class 对象之后,怎么获取字段、调用方法、创建对象。

更多推荐