Java反射剖析详解


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

Java 反射的主要作用包括:

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

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

一、类的反射

1. 基本介绍

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

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

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

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

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

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

// 私有类
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() 可作用于私有构造器。

动态代理可以用于实现 AOP (面向切面编程),事务处理等,如下示例中即通过 newInstance() 方式生成 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. 信息获取

在获取类对象之后即可通过其获取类的基本信息如包名、类型、作用域等等。

如下述示例即通过获取的类对象从而查询 String.class 类的基本信息。

public void basicInfo() {
    Class<?> cls = String.class;
    // 完整类名:java.lang.String
    System.out.println("Class name: " + cls.getName());
    // 类名:String
    System.out.println("Simple name: " + cls.getSimpleName());
    if (cls.getPackage() != null) {
        // 包名:java.lang
        System.out.println("Package name: " + cls.getPackage().getName());
    }
    // 是否为枚举类
    System.out.println(cls.getName() + " is enum? " + cls.isEnum());
    // 是否为接口类
    System.out.println(cls.getName() + " is interface? " + cls.isInterface());
    // 类是否私有
    System.out.println(cls.getName() + " is primitive? " + cls.isPrimitive());
}

二、属性反射

1. 基本示例

除了获取类等基本信息外通过反射也可获取其字段 (Field)、方法 (Method)、方法参数 (Parameter) 与构造器 (Constructor) 等属性信息,这里仅以获取字段举例,其它类型使用方式同理。

当上述四类属性声明为非 public 而又需要在外部实现访问时,可通过 setAccessible(true) 方法开放访问权限,这样即便定义为 private 仍可通过访问在类外部中使用。

(1) 信息获取

Java 中为每个类成员都提供 getXXXs()getDeclaredXXXs()getXXX()getDeclaredXXX() 四种方法用于获取成员属性,其中包含 Declared 表明读取的为当前类中所定义信息,否则默认读取所有(包含继承的父类),结尾为 s 表示读取所有集合,否则为指定名称进行过滤查询。

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

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

以测试类 Student.java 为例,上述表格中对应方法的基本使用示例如下:

public void fieldDemo() throws NoSuchFieldException {
    Class<Student> clazz = Student.class;
    // 获取父类
    System.out.println("getSuperclass(): " + clazz.getSuperclass());

    // 获取自身与其父类所有字段, 只能获取 public 声明字段
    Field[] fields1 = clazz.getFields();
    System.out.println("getFields(): " + Arrays.toString(fields1));

    // 获取自身所有字段
    Field[] fields2 = clazz.getDeclaredFields();
    System.out.println("getDeclaredFields(): " + Arrays.toString(fields2));

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

    // 获取字段详细信息
    int m = name.getModifiers();
    System.out.println("getField(name).getModifiers: " + m);
    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) 属性赋值

同理反射也可通过 set() 方法给实例对象赋值,若字段为私有属性时需要额外设置 setAccessible(true)

public void demo() throws Exception {
    Student std = new Student(123, 85);
    // Get field instance
    Field score = Student.class.getDeclaredField("score");
    // Set accessible.
    score.setAccessible(true);
    // Update Filed value
    score.set(std, 90);
    // Get field value
    int s1= (int) score.get(std);
    System.out.println(s1);
    System.out.println(std);
}

2. 方法调用

对象的方式反射与字段类似,同样提供了四类获取方式,这里不再重复介绍,这里重点介绍一下在通过反射获取方法对象之后如何进行调用。

在反射的 Method 对象中为方法调用提供了 invoke() 方法,即通过其实现方法的调用。

查看源码现即可看到 invoke() 定义了两个入参,具体含义如下:

  • obj: 即方法调用对象,在反射中通常通过 newInstance() 创建。
  • args: 对应目标方法的方法入参,可以为多个或零个。
public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
{
    // 省去具体实现
}

如下述示例中即通过反射调用 Foo 类中的 sayHello() 方法。

注意使用反射无论在获取字段还是方法当目标作用域为私有无法访问时需要通过 setAccessible(true) 设置允许访问,否则将会抛出 IllegalAccessException 异常。

public class Foo {
    private String sayHello() {
        return "Hello World!";
    }
}

public class FooTest {
    public static void main(String[] args) {
        // 获取类对象
        Class<?> cls = Class.forName("com.example.Foo");
        // 获取类中的方法
        Method method = cls.getMethod("sayHello");
        // 作用域为私有需要设置允许访问
        method.setAccessible(true);

        // 创建对象,等价于 Foo foo = new Foo()
        Foo foo = cls.newInstance();
        // 调用方法,等价于 foo.sayHello()
        Object invoke = method.invoke(foo);
        // 输出:Hello World!
        System.out.println(invoke);
    }
}

3. 静态反射

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

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

public class Foo {

    private static String name;

    private static String sayHello() {
        return "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);
        System.out.println(name);

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

4. 程序堆栈

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

通过 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. 基本介绍

Java 中通过 Proxy 类提供了动态代理的方式,其提供了一种运行时修改程序的功能,在远程服务调用 (RPC)AOP 切面等等中都设计到该操作。

2. 示例演示

首先定义一个接口类 ProxyService 并实现 sayHi() 进行简短的信息打印。

public interface ProxyService {
    void sayHi();
}

public class ProxyServiceImpl implements ProxyService {
    @Override
    public void sayHi() {
        System.out.println("Hi");
    }
}

动态代理的重点来了,新建类 MyInvokeHandler 并实现于 InvocationHandler

其中 invoke() 即动态代理的核心,由动态代理申请的对象在执行方法前将触发此处的 invoke(),由此即可实现切面等功能,这里我设置当执行 sayHi() 方法时打印信息将由 Hi 改为 Hello

public class MyInvokeHandler implements InvocationHandler {

    private Object target;

    public MyInvokeHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("sayHi")) {
            System.out.println("Hello");
            return null;
        }

        return method.invoke(target, args);
    }
}

通过 Proxy.newProxyInstance() 方法即可实现声明一个动态对象,该对象在执行方法时将会触发上述定义的 invoke() 方法。

运行下述示例代码即可发现打印信息由 ProxyService 中定义的 Hi 变为 Hello,即通过动态代理我们实现的不修改源程序的情况下更改的程序逻辑实现。

@Test
public void invokeDemo() throws Exception {
    Class<ProxyService> clazz = ProxyService.class;
    ProxyService service = new ProxyServiceImpl();
    MyInvokeHandler handler = new MyInvokeHandler(service);
    // Use "Proxy.newProxyInstance()" to dynamic generate a proxy instance
    ProxyService proxyService = (ProxyService) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler);
    // Call method
    proxyService.sayHi();
}

四、高级应用

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

五、Unsafe

反射一个较为常见的用法即用于获取 Unsafe 实例,因为其并不提供直接的构造方法用于初始化。Unsafe 类虽然类名为不安全,但并非表示这个类不安全,而是 Unsafe 常涉及到内存等敏感操作,因此称其非安全类。

1. 实例获取

Unsafe 无法直接通过 new 实现对象创建,只能通过反射的方式获取。

/**
 * 通过反射获取 Unsafe 实例
 */
public void init() throws NoSuchFieldException, IllegalAccessException {
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
}

2. 基本操作

在获取 unsafe 实例之后即可利用其实现一系列操作,如常见的的 CAS 等等。

public void info() throws NoSuchFieldException {
    MyEntity entity = new MyEntity("Alex");
    long offset = unsafe.objectFieldOffset(MyEntity.class.getDeclaredField("name"));
    // Get object value
    String str = (String) unsafe.getObject(entity, offset);
    System.out.println(str);

    // Put object value
    unsafe.putObject(entity, offset, "Beth");
    System.out.println(entity.getName());

    // CAS: 保证操作原子性
    unsafe.compareAndSwapObject(entity, offset, "Beth", "Jack");
    System.out.println(entity.getName());
}

public static class MyEntity {

    public String name;

    public MyEntity(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

3. 内存操作

Unsafe 之所以被称为非安全的一大重要原因即它提供了直接操作内存的能力,那就让我们看一下它究竟是怎么个操作法。

Unsafe 针对内存操作的常用方法如下:

  • allocateMemory(): 分配一块连续的直接内存块,不受堆内存与 GC 的管控,返回内存块的起始地址。
  • freeMemory(address): 根据传入的内存地址释放内存资源,在分配内存后一定要进行释放。
  • putByte(address, data): 在指定内存处写入一字节数据。
  • getByte(address, data): 在指定内存处读取一字节数据。

如下述示例即通过 unsafe 对象手动声明了一块直接内存,并存储一个 entity 对象,其中 convertObjbyteToObject 即利用 ByteArrayInputStreamByteArrayOutputStreamJava 对象进行二进制的转化,因为内存中存储的数据必须为字节数据。

public void memoryDemo() {
    MyEntity entity = new MyEntity("Alex");
    byte[] data = ObjSerialize.convertObj(entity);
    // Allocate native memory, return the memory start address
    long address = unsafe.allocateMemory(data.length);

    try{
        // Put real data to memory
        for (int i = 0; i < data.length; i++) {
            // putByte(val1, val2):
            // ==> val1: the memory address
            // ==> val2: the data
            unsafe.putByte(address + i, data[i]);
        }

        byte[] origin = new byte[data.length];
        for (int i = 0; i < origin.length; i++) {
            // Read data from native memory
            origin[i] = unsafe.getByte(address + i);
        }
        MyEntity result = ObjSerialize.byteToObject(origin, MyEntity.class);
        System.out.println("Result: " + result);
    } catch (Exception ignored) {
    } finally {
        // Release the memory, important!!!
        unsafe.freeMemory(address);
    }
}

4. 内存对齐

Unsafe 中通过 allocateMemory() 申请的内存属于直接内存,是未经任何初始化的内存空间。

在计算机系统中,有些硬件操作要求数据以特定的字节对齐方式存储,这可以提高数据读取和写入的效率。因此对于 allocateMemory() 申请的内存空间,通常为了更好的性能会利用空数据进行对齐填充操作,以确保数据存储在内存中时满足特定的对齐要求,进而提高了程序的执行效率。

如下示例即对与 unsafe 申请的内存空间进行对齐填充操作。

private static Unsafe unsafe;

private static long address;

private static byte[] data;

private void preFillData(byte[] data) {
    int position = 0;
    int dataLen = data.length;
    for (; dataLen - position >= 8; position++) {
        unsafe.putLong(address + position, 0L);
    }

    for (; dataLen - position >= 4; position++) {
        unsafe.putInt(address + position, 0);
    }

    for (; dataLen - position >= 0; position++) {
        unsafe.putByte(address + position, (byte) 0);
    }
}

六、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 对象之后,即可通过反射实现方法的执行调用。

相应的示例代码如下,其中 methodNamebean 对象中所要执行的方法, 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 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录