Java反射机制详解

约 30 分钟读完

反射机制

基础介绍

什么是反射机制?

Java 的反射机制是指在程序运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

简单来说,反射允许你在程序运行时“解剖”一个类,并操作它,即使你在编写代码时并不知道这个类的具体信息。


反射的核心功能

反射机制主要提供了以下功能:

  1. 在运行时判断任意一个对象所属的类。
  2. 在运行时构造任意一个类的对象。
  3. 在运行时判断任意一个类所具有的成员变量和方法。
  4. 在运行时调用任意一个对象的方法。
  5. 生成动态代理。

反射的核心 API

反射相关的类主要位于 java.lang.reflect包中,但核心的入口是 java.lang.Class类。

  1. java.lang.Class: 代表一个类本身。它是反射的源头。你只有先获取到了类的 Class对象,才能进行后续操作。
  2. java.lang.reflect.Constructor: 代表类的构造方法,用于创建对象。
  3. java.lang.reflect.Field: 代表类的成员变量(属性)。
  4. java.lang.reflect.Method: 代表类的方法。

使用反射的基本步骤

第一步:获取 Class 对象

这是最关键的一步。有四种常见方式:

  1. Class.forName("全限定类名") (最常用)

    适用于在编译时完全未知的类名,通常来自配置文件。

    Class<?> clazz = Class.forName("java.lang.String");
  2. 类名.class

    适用于编译时已知的类,最安全,性能最好。

    Class<String> clazz = String.class;
  3. 对象.getClass()

    适用于你已有一个该类的实例对象。

    String s = "Hello";
    Class<? extends String> clazz = s.getClass();
  4. 通过类加载器 (不常用)

    ClassLoader cl = this.getClass().getClassLoader();
    Class<?> clazz = cl.loadClass("java.lang.String");

第二步:进行反射操作

1. 创建对象

通过 Constructor对象来创建实例,尤其是无参和有参构造。

// 获取无参构造并创建对象
Class<?> clazz = Class.forName("com.example.Person");
Constructor<?> constructor = clazz.getConstructor(); // 获取无参构造
Object obj = constructor.newInstance();

// 获取有参构造并创建对象
Constructor<?> cons = clazz.getConstructor(String.class, int.class); // 参数类型
Object objWithParams = cons.newInstance("Alice", 30);

2. 操作字段 (Field)

获取和设置对象的属性值。

// 获取 public 字段(包括父类的 public)
Field nameField = clazz.getField("name"); 
nameField.set(obj, "Bob"); // 为 obj 对象的 name 属性设置值 "Bob"

// 获取所有字段(包括 private,但不包括继承的)
Field ageField = clazz.getDeclaredField("age");
// 解除私有权限限制(暴力反射)
ageField.setAccessible(true); 
int age = (int) ageField.get(obj); // 获取 obj 对象的 age 属性值

3. 调用方法 (Method)

动态调用对象的方法。

// 获取 public 方法(包括父类的)
Method sayHelloMethod = clazz.getMethod("sayHello");
sayHelloMethod.invoke(obj); // 调用 obj 对象的 sayHello 方法

// 获取声明的方法(包括 private)
Method privateMethod = clazz.getDeclaredMethod("privateMethod", String.class); // 方法名 + 参数类型
privateMethod.setAccessible(true);
Object returnValue = privateMethod.invoke(obj, "argument"); // 调用并接收返回值

反射的优缺点

优点:

  • 灵活性极高: 极大地提高了程序的灵活性和扩展性。很多框架(如 Spring, Hibernate, MyBatis, JUnit)都大量使用反射来实现解耦和动态行为。
  • 实现通用代码: 可以编写出非常通用的代码,适用于不同类型的对象。

缺点:

  • 性能开销: 反射涉及动态类型解析,JVM 无法对其进行优化,因此速度比直接调用慢。在性能敏感的应用中需要谨慎使用。
  • 安全限制: 反射可以突破权限控制,例如访问私有成员,可能会带来安全问题。
  • 内部暴露: 反射打破了封装性,可能导致意想不到的副作用,使得代码行为不稳定且难以调试。
  • 代码复杂性: 反射代码比普通代码更复杂,可读性较差。

安全场景


一、 攻击面:恶意利用场景

攻击者利用反射来绕过安全限制、执行恶意代码和进行侦察。

1. 绕过访问控制(Access Control)

这是反射最直接的危险。通过 setAccessible(true),攻击者可以禁用 Java 的访问检查,从而访问和修改类的私有字段、调用私有方法。

  • 场景举例:
    • 修改关键配置: 许多框架(如 Spring)将配置存储在私有字段中。攻击者可以通过反射获取这些字段并修改,从而改变程序行为。
    • 绕过权限检查: 如果某个安全检查逻辑封装在一个私有方法中,攻击者可能直接调用它后面的方法,绕过检查。
    • 篡改数据: 访问并修改敏感数据,如会话令牌、用户权限标志位等。

2. 实现反序列化漏洞利用

这是反射在攻击中最经典、最危险的应用。许多著名的 Java 反序列化漏洞(如 Apache Commons Collections、Spring Framework RCE)都依赖反射链来构造。

  • 攻击流程:
    1. 入口点: 攻击者找到一个接受外部输入的反序列化端点(如读取文件、接收网络数据)。
    2. 构造利用链(Gadget Chain): 攻击者精心构造一串序列化后的对象。这些对象在反序列化时,会通过一系列的反射调用(如调用 Runtime.exec()ProcessBuilder.start())最终执行系统命令。
    3. 动态调用: 利用链中的关键一步往往是 Method.invoke(),它动态地调用了一个危险的方法。如果没有反射,这种动态调用链几乎无法实现。

3. 动态加载和执行恶意代码

反射允许程序在运行时加载并实例化未知的类。攻击者可以利用这一点来加载恶意类。

  • 场景举例:
    • WebShell: 一个上传的 JSP WebShell 可能会使用 ClassLoader.defineClass()Unsafe.defineClass()(通过反射获取 Unsafe实例)来加载一个字节数组(即恶意类的字节码),然后实例化并执行它,从而实现内存马(Memory Shell),无需写入文件,更难检测。
    • 规避静态检测: 恶意代码的类名、方法名可以通过字符串拼接,从而规避基于特征码的静态杀毒软件或IDS/IPS的检测。

4. 侦察与信息收集

在攻击的初步阶段,攻击者可以利用反射来探测应用程序的内部结构。

  • 场景举例:
    • 通过 getDeclaredClasses(), getDeclaredMethods(), getDeclaredFields()等,攻击者可以枚举出当前类加载器中的所有类、方法、字段,寻找潜在的可利用组件或敏感信息。

5. 绕过 RASP/IAST 等运行时防护

一些高级的运行时应用自我保护(RASP)解决方案会 Hook 关键的危险方法(如 Runtime.exec)。攻击者可能会使用反射来寻找并调用这些方法的“原始”版本,或者通过更迂回的反射链来绕过 Hook 点。

代码示例

创建类User

package com.user;

public class User {
    public String name;
    private int age;
    public String  gender;
    protected String phone;

    public User(String name, int age, String gender, String phone) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.phone = phone;
    }

    public User(String gender,  String name) {
        this.gender = gender;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}

获取类

创建一个GetClassUser.java

    import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import com.user.User;

import java.lang.Class;
public class GetClassUser {
    public static void main(String[] args) throws ClassNotFoundException {
        //第一种获取类的方式
        Class<?> aClass = Class.forName("com.user.User");
        System.out.println(aClass);

        //第二种获取类的方式
        User user=new User("张三",18,"男","12345678901");
        Class<? extends User> aClass1 = user.getClass();
        System.out.println(aClass1);

        //第三种获取类方式
        Class<?> aClass2 = User.class;
        System.out.println(aClass2);

        //第四种获取类方式
        ClassLoader classLoader = GetClassUser.class.getClassLoader();
        Class<?> aClass3 = classLoader.loadClass("com.user.User");
        System.out.println(aClass3);
    }
}

获取属性

核心 API:java.lang.reflect.Field

Field类提供了获取和设置某个类的静态或实例字段的值的能力。

获取 Field 对象的方法

主要通过 Class对象来获取 Field,有以下几种方法:

1. getField(String name)

  • 功能:获取指定的 public 成员变量(包括从父类继承的 public 变量)。
  • 注意:只能获取 public修饰的字段。如果找不到,会抛出 NoSuchFieldException

2. getDeclaredField(String name)

  • 功能:获取当前类中 声明的 指定成员变量。
  • 注意可以获取所有访问权限的字段public, protected, default, private),但不包括从父类继承的字段

3. getFields()

  • 功能:获取所有 public 成员变量(包括从父类继承的 public 变量)的数组。
  • 返回Field[]

4. getDeclaredFields()

  • 功能:获取当前类中 声明的所有 成员变量。
  • 注意可以获取所有访问权限的字段,但不包括从父类继承的字段
  • 返回Field[]

代码示例

import com.user.User;

import java.lang.reflect.Field;

public class GetFiledUser {
    public static void main(String[] args) throws NoSuchFieldException {
        //获取类
        Class<?> clazz = User.class;

        //获取所有属性
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field);
        }
        System.out.println("------------");
        //获取所有公共属性
        Field[] fields1 = clazz.getFields();
        for (Field field : fields1) {
            System.out.println(field);
        }
        System.out.println("------------");
        //获取单个公共属性
        Field field = clazz.getField("name");
        System.out.println(field);
        System.out.println("------------");
        //获取单个属性
        Field field1 = clazz.getDeclaredField("age");
        System.out.println(field1);
    }
}

获取和赋值get()和set()

创建类ControlFieldjava

import com.user.User;

import java.lang.reflect.Field;

public class ControlField {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        User user=new User("张三",18,"男","12345678901");
        Class<? extends User> aClass = user.getClass();
        
        //获取name的值
        Field name1 = aClass.getField("name");
        Object o = name1.get(user);
        System.out.println(o);
        
        //赋值
        name1.set(user,"李四");
        Object o2 = name1.get(user);
        System.out.println(o2);

    }
}

获取构造方法

核心 API:java.lang.reflect.Constructor

Constructor类提供了关于类的单个构造方法的信息以及访问权限。


获取 Constructor 对象的方法

主要通过 Class对象来获取 Constructor,有以下几种方法:

1. getConstructor(Class<?>... parameterTypes)

  • 功能:获取指定的 public 构造方法。
  • 参数parameterTypes是一个可变参数,指定构造方法的参数类型(如 String.class, int.class)。
  • 注意:只能获取 public修饰的构造方法。如果找不到,会抛出 NoSuchMethodException

2. getDeclaredConstructor(Class<?>... parameterTypes)

  • 功能:获取当前类中 声明的 指定构造方法。
  • 参数parameterTypes指定构造方法的参数类型。
  • 注意可以获取所有访问权限的构造方法public, protected, default, private)。

3. getConstructors()

  • 功能:获取所有 public 构造方法的数组。
  • 返回Constructor<?>[]

4. getDeclaredConstructors()

  • 功能:获取当前类中 声明的所有 构造方法。
  • 注意可以获取所有访问权限的构造方法
  • 返回Constructor<?>[]

setAccessible(true);:这行代码的作用是关闭对 con2所代表的构造方法的 Java 语言访问检查

newInstance()方法用于通过反射动态创建对象实例。

代码示例

import com.user.User;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class GetConstructor {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        User user = new User();
        Class<? extends User> aClass = user.getClass();
        //获取单个构造方法
        Constructor<? extends User> constructor = aClass.getConstructor(String.class, int.class,String.class,String.class);
        System.out.println(constructor);
        //获取所有公共构造方法
        Constructor<?>[] constructors = aClass.getConstructors();
        for (Constructor<?> constructor2 : constructors) {
            System.out.println(constructor2);
        }
        //获取所有构造方法
        Constructor<?>[] constructors1 = aClass.getDeclaredConstructors();
        for (Constructor<?> constructor2 : constructors1) {
            System.out.println(constructor2);
        }
        //获取单个私有构造方法
        Constructor<? extends User> declaredConstructor = aClass.getDeclaredConstructor(String.class, String.class);
        System.out.println(declaredConstructor);

        //修改构造方法的值
        Constructor<? extends User> constructor1 = aClass.getConstructor(String.class, int.class,String.class,String.class);
        constructor1.setAccessible(true);
        User user1 = constructor1.newInstance("zs", 20, "女", "22222456789");
        System.out.println(user1.getName());
        System.out.println(user1.getGender());
        System.out.println(user1.getAge());
        System.out.println(user1.getPhone());


    }
}

获取方法

方法名 作用描述 包含父类方法 包含私有方法
getMethods() 获取所有 public 方法(包括从父类继承的)
getDeclaredMethods() 获取本类中声明的所有方法(包括 public, protected, 默认, private
getMethod() 获取指定的 public 方法(可包括父类的)
getDeclaredMethod() 获取本类中声明的指定方法(包括私有方法)

invoke()方法的作用

invoke()方法的主要作用是在运行时动态地调用指定对象的方法。这意味着你可以在编译时不确定具体调用哪个方法,而是在程序运行期间根据条件或配置来决定。这为Java程序带来了极大的灵活性,是许多框架(如Spring、Hibernate)实现的基础

使用 invoke()的一般步骤是:

1.获取类的 Class对象:例如通过 Class.forName("全限定类名")或 类名.class。

2.获取 Method对象:

使用 getMethod("方法名", 参数类型列表)获取公共方法(包括继承的)。

使用 getDeclaredMethod("方法名", 参数类型列表)获取本类声明的任何方法(包括私有方法)。

3.(可选)设置访问权限:如果要调用的方法是私有的,需要先调用 method.setAccessible(true)来绕过访问控制检查。

4.调用 invoke()方法:传入目标对象实例(静态方法传 null)和参数。

代码示例

import com.user.User;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class GetMethod {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
       User user = new User();
        Class<? extends User> aClass = user.getClass();
        System.out.println("获取单个公有方法");
        Method method = aClass.getMethod("getName");
        System.out.println(method);
        System.out.println("获取所有公有方法");
        Method[] methods = aClass.getMethods();
        for (Method method1 : methods) {
            System.out.println(method1);
        }
        System.out.println("获取所有方法");
        Method[] declaredMethods = aClass.getDeclaredMethods();
        for (Method method1 : declaredMethods) {
            System.out.println(method1);
        }
        System.out.println("获取单个私有方法");
        Method declaredMethod = aClass.getDeclaredMethod("setName", String.class);
        System.out.println(declaredMethod);
        System.out.println("修改值");
        Method setName = aClass.getMethod("setName", String.class);
        setName.setAccessible(true);
        setName.invoke(user,"麻瓜");
        System.out.println(user.getName());

    }
}

总结

反射的核心 API 与操作

1. 操作字段 (Field)
  • 获取 Field:
    • getField("name"): 获取指定 public 字段(包括父类)。
    • getDeclaredField("name"): 获取本类声明的指定字段(包括 private)。
    • getFields(): 获取所有 public 字段(包括父类)。
    • getDeclaredFields(): 获取本类声明的所有字段(包括 private)。
  • 操作字段值:
    • get(Object obj): 获取指定对象 obj的该字段值。
    • set(Object obj, Object value): 为指定对象 obj的该字段设置值。
    • 访问私有字段前必须field.setAccessible(true);(暴力反射)
2. 操作构造方法 (Constructor)
  • 获取 Constructor:
    • getConstructor(Class<?>... paramTypes): 获取指定 public 构造。
    • getDeclaredConstructor(Class<?>... paramTypes): 获取本类声明的指定构造(包括 private)。
    • getConstructors(): 获取所有 public 构造。
    • getDeclaredConstructors(): 获取本类声明的所有构造(包括 private)。
  • 创建对象实例:
    • constructor.newInstance(Object... initargs): 传入参数,创建对象。
    • 访问私有构造前必须constructor.setAccessible(true);
3. 操作方法 (Method)
  • 获取 Method:
    • getMethod("name", Class<?>... paramTypes): 获取指定 public 方法(包括父类)。
    • getDeclaredMethod("name", Class<?>... paramTypes): 获取本类声明的指定方法(包括 private)。
    • getMethods(): 获取所有 public 方法(包括父类)。
    • getDeclaredMethods(): 获取本类声明的所有方法(包括 private)。
  • 调用方法:
    • method.invoke(Object obj, Object... args): 对对象 obj调用此方法,并传入参数 args
      • 如果方法是静态的,obj参数可为 null
    • 调用私有方法前必须method.setAccessible(true);
← ORM框架:JDBC与MyBatis安全 Java类加载机制详解 →