反射基础
当我们需要检查或修改 Java 虚拟机中正在运行的应用程序的运行时行为时通常会使用反射。反射提高了程序的扩展性,程序可以通过使用外部用户定义类的完全限定名(fully-qualified names)来创建扩展性对象的实例或者获取类的信息。
我们在业务代码中很少用到反射,但是在一些框架中如 Spring,MyBatis 等都大量的使用了反射。一个最常见的例子是,Spring 创建 Bean 用的就是反射机制。
类的反射
通过类的完全限定名(fully-qualified names),我们可以获取到这个类的一个类实例。
方法一:Class.forName()
这是最常用的一种方法。
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");

方法二:ClassLoader.loadClass()
Class<?> clazz = ClassLoadTest.class.getClassLoader().loadClass("tech.devguide.reflection.classload.model.User");

Class.forName() vs ClassLoader.loadClass()
两者都能获加载类,并且取到类的信息,但是两者是有一些区别的。
Class.forName()- 使用类加载器加装 class,类记载器为调用者的类加载器,一般为应用类加载器:
AppClassLoader。 - 初始化类,所有静态成员变量会被初始化,静态代码块会被执行。
- 使用类加载器加装 class,类记载器为调用者的类加载器,一般为应用类加载器:
ClassLoader.loadClass()- 只加载类,并不初始化类。
实例化
方法一:使用Class#newInstance()方法实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
User user = (User) clazz.newInstance();
这个方法从JDK9开始已被标记为过时。以下为关于过时的说明
This method propagates any exception thrown by the nullary constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed by the compiler. The
Constructor.newInstancemethod avoids this problem by wrapping any exception thrown by the constructor in a (checked)InvocationTargetException.The call
clazz. newInstance()
can be replaced by
clazz. getDeclaredConstructor().newInstance()
方法二:使用构造函数实例化
先对 User 类做如下修改:
@Data
public class User {
private String name;
private int age;
public User() {
}
public User(String name) {
this.name = name;
}
private User(String name, int age) {
this.name = name;
this.age = age;
}
}
使用无参构造函数实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor();
user = constructor.newInstance();
使用有参构造函数实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor(String.class);
User user = constructor.newInstance("张三");
System.out.println(user.getName());
如果使用了lombok在获取有参构造函数时可能会报错,错误信息如下:
Fatal error compiling: java.lang.NoSuchFieldError:
Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'
这是因为 lombok 版本太低,与高版本 JDK 不兼容的问题导致,lombok 支持 JDK 21 的最低版本为 1.18.30,具体可见:
使用私有有参构造函数实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
User user = constructor.newInstance("张三", 12);
System.out.println(user.getName());
这里需要注意的是需要通过 constructor.setAccessible(true) 设置下访问权限,否则该构造函数是无法访问的。
在通过参数获取构造函数时,要注意基本类型和包装类型之间是不等价的,如上面的例子,如果将 int.class 改为 Integer.class 是无法获取到构造函数的。
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, Integer.class);
错误信息如下
Exception in thread "main" java.lang.NoSuchMethodException: tech.devguide.reflection.classload.model.User.<init>(java.lang.String,java.lang.Integer)
at java.base/java.lang.Class.getConstructor0(Class.java:3689)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2858)
at tech.devguide.reflection.classload.ClassLoadTest.main(ClassLoadTest.java:29)
Note
这里的 <init> 就是构造函数。具体在《JVM上篇:内存与垃圾回收篇--类加载子系统》章节由详细的讲解
getDeclaredConstructors 和 getConstructors 的区别
两者的区别在于 getConstructors 仅获取访问权限为 public 的构造函数,而 getDeclaredConstructors 能够获取到所有构造函数。
这篇文章中 《Difference between Loading a class using ClassLoader and Class.forName》 有更详细的说明。
构造函数 (Constructor) 实例中的信息
通过 Constructor 实例,可以获取到构造函数的修饰符、参数数量、参数信息、注解等信息。
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, Integer.class);
// 获取构造方法修饰符
int modifier = constructor.getModifiers();
System.out.println(Modifier.isPrivate(modifier));
// 获取参数数量
int parameterCount = constructor.getParameterCount();
System.out.println(parameterCount);
// 获取参数类型
Class<?>[] parameterTypes = constructor.getParameterTypes();
System.out.println(parameterTypes);
// 获取参数信息
Parameter[] parameters = constructor.getParameters();
System.out.println(parameters);
// 获取构造函数的注解
Annotation[] declaredAnnotations = constructor.getDeclaredAnnotations();
System.out.println(declaredAnnotations);
字段处理
通过 Field 处理字段
通过 Class#getDeclaredField 方法可以获取到字段(java.lang.reflect.Field)实例。并通过 Field 实例设置字段值和获取字段值。
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "张三"); // 设置name字段的值
String name = (String) field.get(user); // 获取name字段的值
System.out.println(name);
getField() 和 getDeclaredField() 两者的区别
getField()- 仅获取
public字段。 - 如果本类中没有要查找的字段,会递归向上查找父类中是否包含这个字段。
- 仅获取
getDeclaredField()- 包含所有字段
- 仅查找本类,不会向上查找父类。
这时会有一个问题,如果我们要查找的字段在父类中,而这个字段是 private 字段,那么通过这两个方法都无法获取到这个字段。这时就需要在找不到所查字段时,获取父类信息,然后在父类上查找。以下工具类提供了类似的功能
org.apache.commons.lang3.reflect.FieldUtils#getField(Class, String)org.springframework.security.util.FieldUtils#getField(Class, String)cn.hutool.core.util.ReflectUtil#getField(Class, String)
通过 getter 和 setter 方法处理字段
Method setName = clazz.getDeclaredMethod("setName", String.class);
if (!setName.isAccessible()) {
setName.setAccessible(true);
}
setName.invoke(user, "李四");
System.out.println(user.getName());
isAccessible() 方法可判断方法是否有访问权限,如果没有访问权限,需要通过 setAccessible 方法修改访问权限。
getMethod() 和 getDeclaredMethod() 两者的区别
getMethod()- 仅获取
public方法。 - 如果本类中没有要查找的方法,会递归向上查找父类中是否包含这个方法。
- 仅获取
getDeclaredMethod()- 查找本类所有方法,不会向上查找父类。
通过 PropertyDescriptor 处理
PropertyDescriptor 是 java.beans 包下的一个类,用于处理类的字段。可以通过 PropertyDescriptor 获取到字段的名称,字段的get方法和字段的set方法等。Spring 中的 BeanUtils 工具类就大量了用了 PropertyDescriptor 来处理字段。
public static void main(String[] args) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
User user = new User();
PropertyDescriptor propertyDescriptor = getPropertyDescriptor(User.class, "name");
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(user, "张三");
Method readMethod = propertyDescriptor.getReadMethod();
System.out.println(readMethod.invoke(user));
}
private static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String name) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
if (propertyDescriptor.getName().equals(name)) {
return propertyDescriptor;
}
}
return null;
}
通过 BeanInfo 获取的 PropertyDescriptor 是包含父类字段的。
方法调用
在 通过-getter-和-setter-方法处理字段 小节中已经介绍过方法的获取和执行,此处不再赘述。