Java反射剖析详解


反射作为 Java 的三大特性之一其重要性不言而喻,它允许在运行时分析、检查和修改类、接口、字段和方法等程序构件的行为。

Java 反射的主要作用包括:

  • 在运行时获取对象的类型信息,包括类名、父类、接口、字段、方法等信息,从而可以实现动态实例化对象、动态调用方法等功能。
  • 可以在运行时访问和操作对象的私有属性和方法,从而实现一些 Java 语言本身无法实现的功能。
  • 可以实现动态代理,可以在程序运行时动态生成代理类,从而实现对目标对象的代理操作。
  • 可以实现工具类、框架和库等高级应用,如 Spring 框架中的 Bean 管理、 AOP 等功能,还可以实现 ORM 框架的数据映射等功能。

Java 反射的意义在于提高了程序的灵活性和可扩展性,使得程序在运行时可以动态地适应不同的需求和环境,从而增强了程序的适应性和可维护性。

一、类的反射

1. 基本介绍

在开发中最常见的场景就是通过已由的属性信息从而初始化实例对象,而反射则恰恰相反,通俗一点的讲就是通过类从而获取类中的所具备的属性。

在进行类反射之前首先需要获取对应的类对象,即通过类名或类的完整限定名获取对象,如字符串对应的 String.class 或 java.lang.String。

针对类对象信息的获取 Java 中提供了三类方式:

  • xxx.class: 最常见的方式,但缺点在于无法作用于私有类等访问受限类。
  • Class.forName(): 通过类加载实现,默认会在加载时同时初始化对象,需要耗费一定性能。
  • loadClass(): 通过类加载实现,与 forName() 类似,但默认不初始化对象,当使用到才会初始化。

如下述示例中创建了一个私有测试类 Foo,但是由于 Foo 类是私有的,而通过 XXX.class 方式是基于导入实现此处显然是非法的。

因此在实际应用中通常是基于类的完整限定名来获取类对象,同时注意 cls2 中获取的方式会默认初始化对象,而对象在初始化时需要为属性字段等信息设置默认值等操作造成性能损耗。因此,在实际应用中更多推荐 cls3 与 cls4 的声明方式,即默认不初始化对象,只有当程序需要用到该对象时才会为对象初始化。

// 私有类
private class Foo {
}

// 测试类
public class FooTest {
    public static void main(String[] args) {
        Class<?> cls1 = Foo.class;      // 非法,Foo 为私有类将无法导入类引用
        Class<?> cls2 = Class.forName("com.example.Foo");       // 默认初始化对象

        // 获取当前上下文类加载
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // 更推荐下述两种方式,不直接初始实例化对象
        Class<?> cls3 = Class.forName("com.example.Foo", false, classLoader);
        Class<?> cls4 = classLoader.loadClass("com.example.Foo")
    }
}

2. 对象实例

在应用程序中若需要使用对象通常利用 new 关键字实现,再编译为可执行的二进制 class 文件,而反射则无法简单的利用 new 关键字。因此,在反射中提供了动态代理的方式初始化,即在运行时生成代理对象,实现对原对象的增强。

在 Java 反射中为对象初始化提供了两种方式,第一种即利用获取 Class<?> 对象调用其 newInstance() 方法,第二类即通过反射获取其构造函数再执行其 newInstance()。

clazz.newInstance() 与 constructor.newInstance() 区别如下:

  • clazz.newInstance():只能生成无参的代理对象,且无法对私有构造函数触发,新版 JDK 中已标记弃用。
  • constructor.newInstance():可以指定代理对象构造参数,通过 setAccessible() 可作用于私有构造器。

如下示例中分别通过上述两种方式生成 Student 的实例对象,而非通过 new 关键字构造。

public class Student {
    public Student() { }
}

public void instanceDemo() throws Exception {
    Class<?> clazz = Class.forName("com.example.Student");
    Student student1 = (Student) clazz.newInstance();
    System.out.println("student-1: " + student1);

    // 获取 Student.class 无参构造器
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Student student2 = (Student) constructor.newInstance();
    System.out.println("student-2: " + student2);
}

3. 信息获取

在获取 Class<?> 实例对象后便可获取其包名与类名等各项信息,参考下表:

方法 作用
getPackage() 获取包实例。
getName() 获取完整限定名,含包名。
getSimpleName() 获取类名称,不含包名。

而除上表中信息外,反射同时提供了一系列方式判断实例类型属性,这里列举常用方法:

方法 作用
isPrimitive() 是否私有类。
isEnum() 是否为枚举。
isInterface() 是否为接口。
isAnnotation() 是否为注解。

二、属性反射

除了获取类等基本信息外通过反射也可获取其字段 (Field)、方法 (Method)、方法参数 (Parameter) 与构造器 (Constructor) 等属性信息,下面让我们来看一下具体方式。

1. 访问方式

针对获取的 Class<?> 实例可由 getXXX() 和 getDeclaredXXX() 方式获取字段与方法等属性。

其中 Declared 则表明读取的为当前类中所有属性,包含 private 等私有属性。而 getXXX() 仅读取当前类及其继承的父类中 public 的属性,更侧重于对外可见的属性。

这里以 Field 字段属性为例,其方法描述信息如下。

方法 作用
getFields() 获取当前类及其父类中所有 public 字段。
getDeclaredFields() 获取当前类的所有属性 field(不包括父类)。
getField(name) 根据字段名获取当前类及其父类中指定的 public 字段。
getDeclaredField(name) 根据字段名获取当前类的指定属性 field(不包括父类)。

此处以 Foo.java 测试类为例,上述表格中对应方法的示例如下:

public class Foo {
    private int name;
}

public void fieldDemo() throws Exception {
    Class<Foo> clazz = Foo.class;

    // 获取自身所有与其父类中 public 字段
    Field[] fields1 = clazz.getFields();

    // 获取自身所有字段
    Field[] fields2 = clazz.getDeclaredFields();

    // 获取 Student 的指定字段信息
    Field name = clazz.getField("name");

    // 设置私有也允许访问
    name.setAccessible(true);

    // 获取字段描述
    int m = name.getModifiers();
    System.out.println("Is Final: " + Modifier.isFinal(m));
    System.out.println("Is Public: " + Modifier.isPublic(m));
    System.out.println("Is Static: " + Modifier.isStatic(m));
}

2. 访问权限

在上一点的代码示例中可以看到在访问资源前通过 setAccessible(true) 设置了可见性。这是因为通过 getDeclaredXXX() 虽能访问类中的私有属性,但直接访问仍会抛出 IllegalAccessException 异常。

需注意对于私有属性并不是所有操作都需通过 setAccessible(true) 获取访问权限,如 Field#getType() 等只是返回编译期的类型信息,若不涉及字段值的访问则无需。

只有当涉及数据内容的获取与变更时,为了解决运行时访问问题才需要,在反射中涉及数据内容的操作如下:

描述 操作
获取字段的值 field.get(obj)
修改字段的值 field.set(obj, value)
调用私有方法 method.invoke(obj, args...)
访问构造器 constructor.newInstance(...)

3. 属性调用

当获取字段与方法等实例信息后,便可通过反射方式操作实例调用。

先以 Field 字段为例,获取相应实例后便可操作 get() 与 set() 实现对象属性的读取与赋值。

public class Foo {
    private int score;
}

public void demo() throws Exception {
    Foo foo = new Foo(100);

    // Get field instance
    Field score = Foo.class.getDeclaredField("score");
    score.setAccessible(true);

    // Get value
    int scoreValue = (int) score.get(foo);
    // Update value
    score.set(foo, 90);
}

同理的,基于反射可以获取 Method 方法属性,并通过 invoke() 实现方法的调用,其中 invoke() 方法定义了两个入参,具体信息如下:

参数 描述
obj 对应的实例对象。
args 对应方法入参,可以为零个或多个。

如下代码中通过反射调用 Foo 的 sayHello() 方法,注意私有方法同样需设置 setAccessible(true) 。

public class Foo {
    private void sayHello() {
        System.out.println("Hello World!");
    }
}

public class FooTest {
    public static void main(String[] args) {
        Foo foo = new Foo();

        // 获取类中的方法
        Method method = cls.getMethod("sayHello");
        method.setAccessible(true);

        // 调用方法
        method.invoke(foo)
    }
}

4. 静态反射

通过反射操作静态成员时与普通成员有所差异,上述提到的操作中无论是在获取字段 Field 还是在执行 Method 时都需要提前通过 newInstance() 初始化对象再实现调用。

而我们知道通过 static 声明的成员属于类的共有属性,因此在反射时无需初始化对象调用,直接传入 null 即可,如之前的提到的反射示例其对象的静态反射示例如下,分别通过反射获取了类的静态成员属性与静态方法。

public class Foo {

    private static String name;

    private static void sayHello() {
        System.out.println("Hello World!");
    }
}

public class FooTest {
    public static void main(String[] args) {
        Class<?> cls = Class.forName("com.example.Foo");

        // 静态属性
        Field field = cls.getDeclaredField("name");
        field.setAccessible(true);
        String name = (String) field.get(null);

        // 静态方法
        Method method = cls.getMethod("sayHello");
        method.setAccessible(true);
        method.invoke(null);
    }
}

5. 程序堆栈

在上面的示例中介绍了在定义时通过反射获取类信息,下面介绍一下如何在运行中获取方法的堆栈信息。

通过 Thread.currentThread().getStackTrace() 即可获取当前方法调用的堆栈信息 StackTraceElement ,返回的类型为数组对象。 StackTraceElement 类包含了如下基本信息:

属性 描述
ClassName 方法所有类的完成限定名,如 xyz.ibudai.bean.User。
MethodName 方法的名称。
FileName 方法所在类的文件名称,如 User.java。

返回的数组中第二个对象(下标为 1)为当前方法的信息,第三个对象(下标为 2)为当前方法调用者的信息,具体示例如下:

public void call() {
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    // 当前方法堆栈信息
    StackTraceElement current = stackTrace[1];
    // 调用者方法堆栈信息
    StackTraceElement caller = stackTrace[2];
    System.out.println("Current info: " + String.format("Class:[%s], Method:[%s], File:[%s]",
            current.getClassName(), current.getMethodName(), current.getFileName()));
    System.out.println("Caller info: " + String.format("Class:[%s], Method:[%s], File:[%s]",
            caller.getClassName(), caller.getMethodName(), caller.getFileName()));
}

三、反射工具

1. 依赖集成

在默认的 JDK 中可利用反射从而实现类的动态获取或创建等操作,而 org.reflections 类库在此基础上提供更丰富的操作。如虽然默认的 JDK 中提供 getInterfaces() 与 getSuperclass() 可以获取当前类实现的接口与父类,却无法查询根据接口或类查询哪些类实现或继承于其。

在开始之前现在工程中导入下述 Maven 依赖。

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>

2. 对象实例

org.reflections 类库的核心处理基于 Reflections 对象,通过其指定反射作用对象。

在传统的反射中通过 Class<?> 针对的是单个类的反射操作,而 Reflections 则更多的应用场景为针对包 package 而言,如下实例中创建了 Reflections 作用于 xyz.ibudai.service 包路径。

public void demo() {
    String packageName = "xyz.ibudai.service";
    Reflections reflections = new Reflections(packageName);
}

Reflections 对象的常用操作接口方法参考下表,同样的 Reflections 针对构造器(Constructor)、字段(Field)与方法(Method)提供了一系列反射方法这里不作详细阐述。

方法 作用
getSubTypesOf() 获取 Reflections 作用域下指定类的所有实现类或继承类。
getTypesAnnotatedWith() 获取 Reflections 作用域下使用指定的注解的所有类。

3. 示例分析

在开始之前先创建如下测试接口类 HelloService 及其相应的两个测试实现类用于后续操作。

package xyz.ibudai.service;

public interface HelloService {
    void sayHello();
}

public class StudentHelloImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Student say hello.");
    }
}

public class TeacherHelloImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Teacher say hello.");
    }
}

完成上述测试类创建之后即可实现相应的操作,下述中提供了两个实例作用如下:

  • demo1(): 查询 xyz.ibudai.service 包路径下所有包含 @Producer 注解的类。
  • demo2(): 查询 xyz.ibudai.service 包路径下所有 HelloService 类的实现类或继承类。

完整的 Reflections 示例代码如下:

public class ReflectionTest {
    @Test
    public void demo1() {
        String packageName = "xyz.ibudai.service";
        Reflections reflections = new Reflections(packageName);
        // getTypesAnnotatedWith(): Get class with specify annotation
        Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(Producer.class);
        for (Class<?> cls : classSet) {
            System.out.println(cls.getName());
        }
    }

    @Test
    public void demo2() {
        String packageName = "xyz.ibudai.service";
        Reflections reflections = new Reflections(packageName);
        Class<HelloService> parent = HelloService.class;
        // getSubTypesOf(): Get class extends and implementation source.
        Set<Class<? extends HelloService>> classSet = reflections.getSubTypesOf(parent);
        for (Class<? extends HelloService> cls : classSet) {
            System.out.println("Name: " + cls.getName());
        }
    }
}

四、Spring反射

1. bean获取

在 Spring 工程中获取已注入的 Bean 对象可通过 @Autowired 获取应用上下文对象 applicationContext,再通过其 getBean() 方式即可获取相应的 Bean 对象。

具体的示例如下:

public class BeanService {
    /**
     * 通过装配获取上下文对象
     */
    @Autowired
    private ApplicationContext applicationContext;
        
    /**
     * 通过名称获取 bean 对象
     */
    public Object getBean(String bean) {
        return applicationContext.getBean(bean);
    }
}

2. 反射代理

在获取 Bean 对象之后,即可通过反射实现方法的执行调用。

相应的示例代码如下,其中 methodName 为 bean 对象中所要执行的方法, params 为方法的参数。

public static Object springInvokeMethod(String serviceName, String methodName, Object[] params) {
    Object service = getBean(serviceName);
    Class<?>[] paramClass = null;
    if (params != null) {
        int length = params.length;
        paramClass = new Class[length];
        for (int i = 0; i < length; i++) {
            paramClass[i] = params[i].getClass();
        }
    }

    // Find target method
    Method method = ReflectionUtils.findMethod(service.getClass(), methodName, paramClass);
    // Executed method
    assert method != null;
    return ReflectionUtils.invokeMethod(method, service, params);
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
赏
  目录