反射 Reflection

177 阅读15分钟

Java反射(Reflection)是Java语言中的一种特性,允许程序在运行时进行自我检查和操作。通过反射,可以在运行时获取类的结构信息(如类的成员变量、方法、构造函数等),并且可以动态地调用方法或访问字段。

动态加载类:可以在运行时加载和创建类的实例,而不是在编译时确定。使用 Class.forName() 方法可以加载指定名称的类。

访问和修改字段:可以访问类的私有字段和方法,甚至可以修改它们的值。使用 Field 类可以获取和操作字段。

调用方法:可以在运行时调用对象的方法,包括私有方法。使用 Method 类可以实现这一点。

创建实例:可以在运行时创建对象实例,而不是在编译时确定。使用 Constructor 类可以实现这一点。

动态代理:可以在运行时创建实现指定接口的代理对象。Java 的 Proxy 类和 InvocationHandler 接口配合使用可以实现动态代理。

Java 反射机制从JDK 1.1(Java Development Kit 1.1)开始引入的,也算是Java的一个基础特性了。

⨳ JDK 1.1 首次引入了反射机制,提供了基本的类、字段、方法和构造函数的反射操作API。

⨳ JDK 1.3 引入了 java.lang.reflect.Proxy 类和 InvocationHandler 接口,使得可以创建动态代理对象。

⨳ JDK 1.5 引入了注解(Annotations)和枚举(Enums),并提供了相关的反射API来处理注解和枚举。

⨳ JDK 1.7 引入了java.lang.invoke包,其中包含 MethodHandleMethodType 等类。方法句柄提供了一种新的动态语言支持机制,性能优于传统反射。

⨳ JDK 1.8 引入了对方法和构造函数参数名称的反射支持。通过在编译时添加-parameters选项,可以在运行时获取参数名称。而且增强了注解机制,允许同一个位置可以有多个相同类型的注解。

⨳ ...

反射继承体系

image.png

AnnotatedElement 注解的元素

AnnotatedElement 是所有可以包含注解的元素的顶级接口,可以在运行时获取类、方法、字段、构造函数等元素上的注解信息,从而实现更多动态化和灵活的程序行为。

AnnotatedElement 提供了几种用于访问注解的核心方法:

boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):如果该元素上存在指定类型的注解,则返回 true

<T extends Annotation> T getAnnotation(Class<T> annotationClass):返回指定类型的注解,如果注解不存在则返回 null

<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass):返回指定类型的注解(仅限于声明在该元素上的注解),如果注解不存在则返回 null

⨳ ...

具体使用详见《注解篇》

总而言之。AnnotatedElement 提供了一个统一的接口来处理各种程序元素上的注解。这意味着无论是类、方法、字段还是构造函数,都可以通过相同的方法来查询和处理它们的注解。这种统一性简化了注解处理的代码,使得反射操作更加直观和易于维护。

如果将来需要在更多类型的程序元素上支持注解,只需实现 AnnotatedElement 接口即可。这种设计提高了系统的灵活性和扩展性。

Class 类

Class 类在 Java 反射 API 中是最基础的类,它表示类和接口的运行时类型信息。

Class 类提供了一系列方法来获取有关类的详细信息:

ClassLoader getClassLoader(): 获取类的类加载器。

Package getPackage(): 获取类所在的包。

String getName(): 获取类的全限定名。

String getSimpleName(): 获取类的简单名。

Class<?> getSuperclass(): 获取直接超类的 Class 对象。

Class<?>[] getInterfaces(): 获取直接实现的接口的 Class 对象数组。

⨳ ...

更重要的是获取声明在类中成员(属性、方法和构造方法):

Field[] getFields(): 获取类的所有公有字段,包括继承的字段。

Field[] getDeclaredFields(): 获取类声明的所有字段,包括私有字段。

Field getField(String name): 获取指定名称的公有字段。 Field getDeclaredField(String name): 获取指定名称的字段(包括私有字段)。

Method[] getMethods(): 获取类的所有公有方法,包括继承的方法。

Method[] getDeclaredMethods(): 获取类声明的所有方法,包括私有方法。

Method getMethod(String name, Class<?>... parameterTypes): 获取指定名称和参数类型的公有方法。

Method getDeclaredMethod(String name, Class<?>... parameterTypes): 获取指定名称和参数类型的方法(包括私有方法)。

Constructor<?>[] getConstructors(): 获取类的所有公有构造函数。

Constructor<?>[] getDeclaredConstructors(): 获取类声明的所有构造函数。

Constructor<T> getConstructor(Class<?>... parameterTypes): 获取指定参数类型的公有构造函数。

Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes): 获取指定参数类型的构造函数(包括私有构造函数)。

⨳ ...

注意对于类中的成员,Class都提供了两套获取成员的方法 —— getXXXgetDeclaredXXX

getXXX 可以获取到当前类及其所有父类或父接口中所有公共(public)成员

getDeclaredXXX 当前类中声明的所有成员,包括公共、保护、默认(包级别)和私有成员,但不包括从父类继承的成员。

从某种角度来说,获取某个类的 Class 对象就像是把这个类“脱光了”。通过反射机制,Class 对象提供的访问类内部结构和成员的能力,可以让程序员看到类的完整定义和实现细节。

既然 Class 这么牛掰,那怎么获取类的 Class 对象呢,首先定义一个简单的类 Person,它包含两个字段、两个构造函数和两个方法,公共私有各一个:

package com.cango.reflect;

public class Person {
    public String name;
    private int age;

    public Person(){
        this.name = "Cango";
        this.age = 18;
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
    }

    private void increaseAge() {
        this.age += 1;
    }
   
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }

}

有三种方式获取某个类的类对象;

Class.forName("com.example.MyClass")

MyClass.class

myObject.getClass()

如下,获取这个 Person 类的Class对象:

Class clazz1 = null;
try {
    clazz1 = Class.forName("com.cango.reflect.Person");
} catch (ClassNotFoundException e) {
    System.err.println("类对象获取失败");
}
Class clazz2 = Person.class;
Class clazz3 = new Person().getClass();
System.out.println("Person类的类对象的类型:"+clazz1);
System.out.println(clazz1==clazz2);
System.out.println(clazz2==clazz3);

输出结果如下:

Person类的类对象的类型:class com.cango.reflect.Person
true
true

获取这个类的Class对象只是反射操作的第一步,第二步是拿到这个类的成员,进而创建对象、设置/获取属性值或调用方法了,这才是重点。

Member 成员

Member 用于表示Java反射API中的成员(如方法、字段、构造函数等)。它定义了一些通用的方法,可以在所有类型的成员上使用。

Class<?> getDeclaringClass(): 返回声明此成员的类。

String getName(): 返回成员的名称。

int getModifiers(): 返回成员的修饰符(public, private, protected, static, final, etc.),这些修饰符是 java.lang.reflect.Modifier 中定义的常量。

boolean isSynthetic(): 返回成员是否是合成的(由编译器生成的,而不是由源代码直接定义的)。

字段、方法、构造函数 都实现了 Member 接口,和 AnnotatedElement 接口一样,都是为了操作统一化。

Field 字段

Field 类表示类或接口的成员变量(即字段)。

可以在运行时获取字段的类型、名称、修饰符等信息(Member提供的能力):

Class<?> getType(): 获取字段的类型。

String Field.getName() : 返回字段的名称(继承 Member 的能力)。

int Field.getModifiers() : 返回字段的修饰符(继承 Member 的能力)。

也可以读取或修改字段的值:

Object get(Object obj): 返回指定对象obj 上此字段的值。

XXX getXXX(Object obj): 获取 obj 对象该字段的XXX类型字段的值,XXX可以是Boolean、Byte、Char、Double、Float、Int...等类型的包装类。

void set(Object obj, Object value): 设置指定对象obj上此字段的值。

void setXXX(Object obj, XXX value):设置指定对象obj上此字段指定类型XXX的值。

通过 Field 类的方法,可以在运行时获取和修改类的成员变量。比如反射获取设置 Person 对象属性:

Person person = new Person();
System.out.println("Person对象:"+person);
// 反射获取类属性
Class clazz = Class.forName("com.cango.reflect.Person");
Field field = clazz.getField("name");
// 获取 person 对象 的 name 属性值
String name = (String)field.get(person);
System.out.println("Person对象name属性值:"+name);
// 更改 person 对象 的 name 属性值
field.set(person,"张三");
System.out.println("Person对象:"+person);

输出结果如下:

Person对象:Person{name='Cango', age=18}
Person对象name属性值:Cango
Person对象:Person{name='张三', age=18}    

这对于框架开发、调试工具和动态代理等场景非常有用。

Executable 可执行的

有 UML 类图可以看到,Method 和 Constructor 的公共抽象类就是 Executable,Method 和 Constructor 本质就是方法,就可以将二者相同的代码提取到这个公共类,这也是Executable存在的原因,为方法和构造函数提供一个统一的基础类。

int getParameterCount(): 获取参数个数。

Class<?>[] getParameterTypes(): 获取参数类型数组。

boolean isVarArgs(): 判断方法或构造函数是否带有可变参数。

⨳ ...

Method 方法

Method 类用于表示类或接口的方法。

可以在运行时获取方法的信息(Member和Executable提供的能力):

Class<?> getReturnType(): 获取方法的返回类型。

Class<?>[] getExceptionTypes(): 获取方法可能抛出的异常类型。

⨳ ...

当然更重要的是动态调用方法:

Object invoke(Object obj, Object... args): 调用此方法并返回结果。

如反射调用 Person对象 的方法

Person person = new Person();
System.out.println("Person对象:"+person);
// 反射获取类方法
Class clazz = Class.forName("com.cango.reflect.Person");
Method method = clazz.getMethod("sayHello");
// 调用 person 对象 的 sayHello 方法
method.invoke(person);

输出结果如下:

Person对象:Person{name='Cango', age=18}
Hello, my name is Cango and I am 18 years old.    

Constructor 构造器

Constructor 类表示类的构造函数。

Method 一样可以在运行时获取构造函数的信息(Member和Executable提供的能力),并调用构造函数来创建类的实例。

T newInstance(Object... initargs): 使用此 Constructor 对象表示的构造函数创建类的新实例。

⨳ ...

如反射创建 Person类对象

//获取类对象
Class clazz;
try {
    clazz = Class.forName("com.cango.reflect.Person");
} catch (ClassNotFoundException e) {
    System.err.println("类对象获取失败");
    throw new RuntimeException(e);
}
// 获取构造函数
Constructor constructor;
try {
    constructor = clazz.getConstructor();
} catch (NoSuchMethodException e) {
    System.err.println("构造方法获取失败");
    throw new RuntimeException(e);
}
// 实例化
Person person ;
try {
    person = (Person)constructor.newInstance();
} catch (InstantiationException e) {
    System.err.println("实例化失败");
    throw new RuntimeException(e);
} catch (IllegalAccessException e) {
    System.err.println("非法访问");
    throw new RuntimeException(e);
} catch (InvocationTargetException e) {
    System.err.println("调用目标方法失败");
    throw new RuntimeException(e.getTargetException());
}
System.out.println("Person对象:"+person);

上述代码,先获取 Person 类的无参构造方法,后使用该构造方法实例化了一个对象出来。

可以看到反射涉及的必检异常非常多,就怕获取不到指定类,获取不到指定构造方法,调用构造方法失败,这些异常前边的《异常篇》都有讲到,这里需要注意一下 IllegalAccessException 非法访问异常。

上面用反射获取属性,更改属性,调用普通方法也会涉及这些异常,为了格式好看,就先 throws 出去了。。

访问权限问题

IllegalAccessException

前文我们知道,无论是反射创建对象,还是反射设置/获取属性值,还是反射调用方法,都显式声明了编译期异常IllegalAccessException,这是 JDK 在提醒开发者注意权限问题,如 Filed类 的设置属性方法:

// Filed 
public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException

但是 Class 又提供了getDeclaredXXX 用于获取当前类中声明的所有成员,当然也会获取到私有成员,如果私有成员只能看不能操作,那不就给人希望又让人失望了,JDK 当然不会这么不靠谱。

前文我们操作 Person 类都是使用 public 构造方法,public 属性,public 方法,现在就以 private 方法演示一下 IllegalAccessException ,以及处理方式。

Person person = new Person();
System.out.println("Person对象:"+person);
// 反射获取类的私有方法
Class clazz = Class.forName("com.cango.reflect.Person");
Method method = clazz.getDeclaredMethod("increaseAge");
// 调用 person 对象 的 increaseAge 方法
method.invoke(person);

输出结果如下:

Person对象:Person{name='Cango', age=18}
Exception in thread "main" java.lang.IllegalAccessException: class com.cango.reflect.Test cannot access a member of class com.cango.reflect.Person with modifiers "private"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:398)
	at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:709)
	at java.base/java.lang.reflect.Method.invoke(Method.java:571)
	at com.cango.reflect.Test.main(Test.java:60)

反射调用私有方法异常,很合理。解决方法也很简单,靠 AccessibleObject 提供的取消Java语言访问检查的能力就行了。

AccessibleObject 可访问对象

根据 UML 类图,可以知道 AccessibleObjectField, Method, 和 Constructor 类的超类,也就是类中所有成员都继承了它,它提供了一个机制,使得反射对象可以在运行时绕过 Java 的访问控制检查。

void setAccessible(boolean flag): 设置或取消此对象的可访问性。如果设置为 true,那么即使原本的访问级别不允许,也可以通过反射访问该对象。

boolean isAccessible(): 返回此对象的可访问性标志。

void setAccessible(AccessibleObject[] array, boolean flag): 批量设置多个 AccessibleObject 实例的可访问性

AccessibleObject 主要用于在反射中操作那些原本由于访问控制规则(如 private、protected)而无法访问的成员。通过设置可访问性,可以在运行时访问和修改这些成员。

Person person = new Person();
System.out.println("Person对象:"+person);
// 反射获取类的私有方法
Class clazz = Class.forName("com.cango.reflect.Person");
Method method = clazz.getDeclaredMethod("increaseAge");
// 设置私有方法可访问
method.setAccessible(true);
// 调用 person 对象 的 increaseAge 方法
method.invoke(person);
System.out.println("Person对象:"+person);

输出结果如下:

Person对象:Person{name='Cango', age=18}
Person对象:Person{name='Cango', age=19} 

所以说,无论是私有的Field 、私有的Method 还是 私有的Constructor,都可以通过 AccessibleObject 绕过访问控制规则。

动态代理

动态代理可以参考《代理模式 Proxy》.

注解

JDK 1.7 同时引入了注解和枚举,枚举即便不依赖反射机制也很好用,但注解如果不使用反射,那注解就废了大部分功能。

注解可以参考《注解》

MethodHandle

java.lang.invoke 包是在 Java 7 中引入的,旨在为动态语言支持、优化反射调用以及实现 Lambda 表达式等提供基础设施。它包含一系列用于字节码级别方法操作的类和接口,能够实现更灵活、高效的动态方法调用机制。

MethodHandle :该包的核心类,代表对字段、方法或构造函数的轻量级、直接引用。它类似于反射中的 MethodFieldConstructor,但更加高效。可以动态地调用方法、访问字段或创建对象。

MethodHandles:这是一个实用类,包含工厂方法来创建 MethodHandle 对象。

MethodType:用于描述方法的签名,包括返回类型和参数类型。在创建 MethodHandle 时,必须指定正确的 MethodType

VarHandle: 是在 Java 9 中引入的,它提供了对字段和数组元素的低级别访问,类似于 MethodHandle 访问方法。

LambdaMetafactory:这是一个工厂类,是 Lambda 表达式的的底层实现。

CallSite :是一个抽象类,用于动态语言和框架实现中的动态方法调用点。它为一些动态语言(如 Groovy、JRuby)在 JVM 上运行提供了更好的支持。

⨳ ...

下面看一下MethodHandle对实例方法的调用:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleExample {
    public void instanceMethod() {
        System.out.println("Instance Method Invoked");
    }

    public static void main(String[] args) throws Throwable {
        MethodHandleExample example = new MethodHandleExample();

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(void.class);
        MethodHandle methodHandle = lookup.findVirtual(MethodHandleExample.class, "instanceMethod", methodType);
        // 调用实例方法
        methodHandle.invoke(example);  // Instance Method Invoked
        
    }
}

使用方式和 reflect反射也没差多少,区别就是MethodHandle 是通过 MethodHandles.Lookup 类来获取的。查找方法、字段和构造函数都可以使用 MethodHandles.Lookup 来查找,就不赘述了。

reflect反射虽然功能强大,但其性能相对较低,因为反射涉及到大量的运行时检查(安全检查、类型匹配等检查)。而 MethodHandle 是通过字节码级别的直接调用,类似于直接的 Java 方法调用,因此性能更高。

参数名称反射

在 Java 8 及以后的版本中,Java 引入了一项新特性,可以通过反射来获取方法或构造函数的参数名称。这一特性依赖于编译时的 -parameters 标志。如果编译时不加这个标志,参数名称在编译时会被擦除,无法通过反射获取。

具体使用的是 MethodConstructor 类中的 getParameters() 方法,该方法返回一个 Parameter 数组,Parameter 对象中包含了参数的名称。

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ParameterNameExample {
    public static void main(String[] args) throws NoSuchMethodException {
        // 获取 ExampleClass 的 testMethod 方法
        Method method = ExampleClass.class.getMethod("testMethod", String.class, int.class);

        // 获取参数数组
        Parameter[] parameters = method.getParameters();

        // 遍历并打印参数名称
        for (Parameter parameter : parameters) {
            System.out.println("参数名称: " + parameter.getName());
        }
    }
}

class ExampleClass {
    public void testMethod(String name, int age) {
        // 一些代码
    }
}

输出结果如下:

参数名称: name
参数名称: age

如果没有添加 -parameters 编译选项,输出将是默认的形式:

参数名称: arg0
参数名称: arg1

在设计框架时,通过获取参数名称可以实现更加智能的参数绑定。例如,Web 框架可以自动将 HTTP 请求中的参数映射到方法参数上。

总结

总而言之,反射(Reflection)是 Java 语言的一项强大特性,它允许程序在运行时动态地访问和操作类、方法、字段等元素。

优点如下:

灵活性:反射允许我们在运行时动态加载类、调用方法和访问字段,这对开发通用框架、库、工具特别有用,例如 ORM 框架、依赖注入框架等。

动态代理:反射是 Java 动态代理机制的基础,通过反射可以实现运行时的代理对象和方法拦截。

操作私有成员:反射能够访问类的私有方法和字段,这为调试和测试代码提供了很大便利。

缺点如下:

性能损耗:反射会绕过 JVM 的某些优化机制,频繁使用反射会导致性能下降。反射涉及的方法调用比直接调用慢。

安全性问题:反射可以绕过 Java 的访问控制检查,访问私有方法和字段,可能导致安全隐患。因此,反射在一些环境中可能会受到限制(如在某些安全管理器中)。

可维护性差:反射代码较为复杂,且由于它是动态的,编译器无法在编译期检查其正确性,可能导致运行时错误,更难调试和维护。

不管怎么说,优点大于缺点,基本上Java开发需要的框架里面都有反射的影子。如 Spring 使用反射在运行时注入依赖,如MyBatis 通过反射将数据库表映射到 Java 对象...