Java反射机制详解
反射机制
基础介绍
什么是反射机制?
Java 的反射机制是指在程序运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。
简单来说,反射允许你在程序运行时“解剖”一个类,并操作它,即使你在编写代码时并不知道这个类的具体信息。
反射的核心功能
反射机制主要提供了以下功能:
- 在运行时判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的方法。
- 生成动态代理。
反射的核心 API
反射相关的类主要位于 java.lang.reflect包中,但核心的入口是 java.lang.Class类。
java.lang.Class: 代表一个类本身。它是反射的源头。你只有先获取到了类的Class对象,才能进行后续操作。java.lang.reflect.Constructor: 代表类的构造方法,用于创建对象。java.lang.reflect.Field: 代表类的成员变量(属性)。java.lang.reflect.Method: 代表类的方法。
使用反射的基本步骤
第一步:获取 Class 对象
这是最关键的一步。有四种常见方式:
Class.forName("全限定类名")(最常用)适用于在编译时完全未知的类名,通常来自配置文件。
Class<?> clazz = Class.forName("java.lang.String");类名.class适用于编译时已知的类,最安全,性能最好。
Class<String> clazz = String.class;对象.getClass()适用于你已有一个该类的实例对象。
String s = "Hello"; Class<? extends String> clazz = s.getClass();通过类加载器 (不常用)
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)都依赖反射链来构造。
- 攻击流程:
- 入口点: 攻击者找到一个接受外部输入的反序列化端点(如读取文件、接收网络数据)。
- 构造利用链(Gadget Chain): 攻击者精心构造一串序列化后的对象。这些对象在反序列化时,会通过一系列的反射调用(如调用
Runtime.exec()或ProcessBuilder.start())最终执行系统命令。 - 动态调用: 利用链中的关键一步往往是
Method.invoke(),它动态地调用了一个危险的方法。如果没有反射,这种动态调用链几乎无法实现。
3. 动态加载和执行恶意代码
反射允许程序在运行时加载并实例化未知的类。攻击者可以利用这一点来加载恶意类。
- 场景举例:
- WebShell: 一个上传的 JSP WebShell 可能会使用
ClassLoader.defineClass()或Unsafe.defineClass()(通过反射获取Unsafe实例)来加载一个字节数组(即恶意类的字节码),然后实例化并执行它,从而实现内存马(Memory Shell),无需写入文件,更难检测。 - 规避静态检测: 恶意代码的类名、方法名可以通过字符串拼接,从而规避基于特征码的静态杀毒软件或IDS/IPS的检测。
- WebShell: 一个上传的 JSP WebShell 可能会使用
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);