反射

反射

如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。

反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。

通过反射可以去构造任意一个类对象获取任意一个类的成员变量,成员方法,属性,可以在程序运行时动态的获取类的信息,以及动态的调用方法,通过java.lang.reflect实现反射相关的类库,包括Construct、Field、Method这样的类

谈谈反射机制的优缺点

优点:可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利

  • 增加程序的灵活性,可在运行的过程中动态对类进行修改和操作
  • 提高代码的复用率,比如动态代理,就是用到了反射来实现
  • 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用

缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。相关阅读:Java Reflection: Why is it so slow?

  • 反射会涉及到运行时动态类型的解析,所以JVM无法对这些代码进行优化,导致性能要比非反前调用更低。
  • 使用反射以后,代码的可读性会下降
  • 反射可以绕过一些限制访问的属性或者方法,可能会导致破坏了代码本身的抽象性

反射的使用

创建学生类

1
2
3
4
5
public class Student {
public void sleep(){
System.out.println("sleep...");
}
}

在src下创建配置文件pro.properties

1
2
#全类名className=cn.itcast.domain.Student
#方法名methodName=sleep

框架测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ReflectTest {
public static void main(String[] args) throws Exception {
/* 不使用框架创建对象,new对象->调用方法
Student stu = new Student();
stu.sleep();*/

//1.加载配置文件
//1.1创建Properties对象
Properties pro = new Properties();
//1.2加载配置文件,转换为一个集合
//1.2.1获取class目录下的配置文件,使用类加载器下的getClassLoader()方法
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("pro.properties");
pro.load(is);

//2.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");

//3.根据全类名获取Class对象加载该类进内存
Class cls = Class.forName(className);
//4.空参创建对象,通过对象cls加载类cn.itcast.domain.Student的对象实例
Object obj = cls.newInstance();
//5.获取方法对象,该方法为通过cls加载的类cn.itcast.domain.Student的methodName的方法对象
Method method = cls.getMethod(methodName);
//6.执行方法 传统方法:对象.方法(),反射机制:方法.invoke(对象)
method.invoke(obj);
}
}

反射的应用场景

动态代理

像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。

但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。

这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。

比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法。

实现 InvocationHandler 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ServiceInvocationHandler implements InvocationHandler {
private final Object target;

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After method: " + method.getName());
return result;
}
}

目标类和其实现的接口

1
2
3
4
5
6
7
8
9
10
11
public interface Service {
void performTask();
}

public class ServiceImpl implements Service {
@Override
public void performTask() {
System.out.println("Executing task...");
}
}

客户端调用代理类执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.lang.reflect.Proxy;

public class DynamicProxyDemo {
public static void main(String[] args) {
// 实例化目标对象
Service service = new ServiceImpl();

// 创建 InvocationHandler
ServiceInvocationHandler handler = new ServiceInvocationHandler(service);

// 创建代理实例
Service proxyInstance = (Service) Proxy.newProxyInstance(
service.getClass().getClassLoader(),
new Class[]{Service.class},
handler
);

// 调用代理实例的方法
proxyInstance.performTask();
}
}

Before method: performTask
Executing task…
After method: performTask

动态代理的好处

减少代码冗余:动态代理不需要为每个接口手动编写代理类,避免了代码重复。只需编写一个通用的代理处理器,减少了类的数量和代码冗余。

提高灵活性:动态代理允许在运行时动态决定代理哪个对象和方法。可以根据配置文件或运行时条件动态生成代理,具有更高的灵活性和扩展性。

支持更多的接口:动态代理可以在运行时为多个接口创建代理实例,而不需要为每个接口创建单独的代理类。

减少维护成本:因为动态代理不需要手动创建代理类,代码量减少,修改和维护成本也相对降低。

适用于AOP(面向切面编程):动态代理非常适合实现 AOP,比如 Spring AOP 就大量使用动态代理来实现切面功能。可以在运行时为目标对象添加横切关注点(如日志记录、事务管理等)。

搭配注解使用

另外,像 Java 中的一大利器 注解 的实现也用到了反射。

为什么你使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?

这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。

现有一个类和方法

1
2
3
4
5
public class Demo1 {
public void show(){
System.out.println("demo1...show");
}
}

写一个自定义注解

1
2
3
4
5
6
7
8
9
/**
* 描述需要执行的类名和方法名
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
String className();
String methodName();
}

写一个需要使用注解的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//指定注解的属性
@Pro(className = "annotation.Demo1",methodName = "show")
public class ReflectTest {
public static void main(String[] args) throws Exception {
/*
前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
*/

//1.解析注解
//1.1获取被注解作用的类的字节码文件(Class)对象
Class<ReflectTest> reflectTestClass = ReflectTest.class;


//2,获取上面的注解对象
//其实就是就是在内存中生成一个该注解接口的子类实现对象
Pro proimpl = reflectTestClass.getAnnotation(Pro.class);

//3.调用注解对象中定义的抽象方法,获取返回值
String className = proimpl.className();
String methodName = proimpl.methodName();
System.out.println(className); //annotation.Demo1
System.out.println(methodName); //show


//3.加载该类进内存
Class cls = Class.forName(className);
//4.创建对象
Object obj = cls.newInstance();
//5.获取方法对象
Method method = cls.getMethod(methodName);
//6.执行方法
method.invoke(obj); //demo1...show
}
}

其实就是在内存中生成了一个该注解接口的子类实现对象

1
2
3
4
5
6
7
8
public class ProImpl implements Pro{
public String className(){
return "cn.itcast.annotation.Demo1";
}
public String methodName(){
return "show";
}
}

利用反射实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Animal {
void makeSound();
}

public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof");
}
}

public class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.reflect.Method;

public class ReflectionPolymorphism {
public static void main(String[] args) {
try {
// 动态加载类名,可以从配置文件加载,这里简化了
String className = "Dog"; // 或者 "Cat"
Class<?> clazz = Class.forName(className);

// 创建实例
Animal animal = (Animal) clazz.getDeclaredConstructor().newInstance();

// 获取方法并调用
Method method = clazz.getMethod("makeSound");
method.invoke(animal);
} catch (Exception e) {
e.printStackTrace();
}
}
}

获取Class对象的方式

1. Class.forName(“全类名”):将字节码文件加载进内存,返回Class对象

​ 多用于配置文件,将类名定义在配置文件 载类

2. 类名.class:通过类名的属性class获取

​ 多用于参数的传递

3. **对象.getClass()**:getClass()方法在Object类中定义了。

​ 多用于对象的获取字节码的方式,可以获取该类运行时的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1、Class.for("全类名")
Class cls1 = Class.forName("cn.itcast.reflect.Person");
System.out.println(cls1); //class cn.itcast.reflect.Person

//2、类名.class
Class cls2 = Person.class;
System.out.println(cls2); //class cn.itcast.reflect.Person

//3、对象.getClass()
Person p = new Person();
Class cls3 = p.getClass();
System.out.println(cls3); //class cn.itcast.reflect.Person

//比较三个对象的地址
System.out.println(cls1==cls2); //true
System.out.println(cls2==cls3); //true

结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个

Class类常用方法

image-20240309173544259

获取成员变量们

Field[] getFields() :获取所有public修饰的成员变量

Field getField(String name) :获取指定名称的 public修饰的成员变量

Field[] getDeclaredFields() :获取所有的成员变量,不考虑修饰符

Field getDeclaredField(String name) :获取指定名称成员变量,不考虑修饰符

Field对象的方法

void set(Object obj, Object value):设置成员便变量对象的值

Object get(Object obj):获取成员变量对象

创建一个Person类

1
2
3
4
5
6
7
8
public class Person {
private String name;
private int age;
public String a;
protected String b;
String c;
private String d;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//获取Person的Class对象
Class personClass = Person.class;

//获取所有public修饰的成员变量
Field[] fields = personClass.getFields();
for(Field field :fields)
{
System.out.println(field); //public java.lang.String cn.itcast.reflect.Person.a
}

//获取所有的成员变量,不考虑修饰符
Field[] declaredFields = personClass.getDeclaredFields();
for(Field declaredField :declaredFields)
{
System.out.println(declaredField); //name age a b c d
}

Field a = personClass.getField("a"); //获取指定名称public成员变量
//获取成员变量a的值
Person p = new Person();
Object value = a.get(p);
System.out.println(value);//null
//设置成员变量a的值
a.set(p,"张三");
Object value1 = a.get(p); //张三
System.out.println(p); //Person{name='null', age=0, a='张三', b='null', c='null', d='null'}

获取构造方法们

Constructor<?>[] getConstructors()

Constructor< T> getConstructor(类<?>… parameterTypes)

Constructor< T> getDeclaredConstructor(类<?>… parameterTypes)

Constructor<?>[] getDeclaredConstructors()

Constructor对象的方法

**T newInstance(Object… initargs) **:通过构造器创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
//获取Person的Class对象
Class personClass = Person.class;

//按指定参数获取构造方法
Constructor constructor = personClass.getConstructor(String.class, int.class);
System.out.println(constructor);//public cn.itcast.domain.Person(java.lang.String,int)

//利用构造器方法创建对象
Object person = constructor.newInstance("张三", 23);
System.out.println(person); //Person{name='张三', age=23, a='null', b='null', c='null', d='null'}

如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法

1
2
3
4
5
6
7
//空参构造方法
Constructor constructor1 = personClass.getConstructor();
System.out.println(constructor1); //public cn.itcast.domain.Person()

//利用构造器方法创建对象
Object person2 = personClass.newInstance(); //Class对象.newInstance()可直接创建对象
System.out.println(person2); //Person{name='null', age=0, a='null', b='null', c='null', d='null'}

获取成员方法们

Method[] getMethods()

Method getMethod(String name, 类<?>… parameterTypes)

Method[] getDeclaredMethods()

Method getDeclaredMethod(String name, 类<?>… parameterTypes)

Method对象的方法

Object invoke(Object obj, Object… args) :执行方法对象的方法

String getName(): 获取方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person {
public void eat(){
System.out.println("eat...");
}
public void eat(String food){
System.out.println("eat..."+food);
}
}

//获取Person的Class对象
Class personClass = Person.class;
//获取空参Method对象
Method eat_method = personClass.getMethod("eat");
Person p = new Person();
//执行方法
eat_method.invoke(p); //吃...

//获取指定名称和指定参数列表的Method对象
Method eat_method2 = personClass.getMethod("eat", String.class);
//执行方法
eat_method2.invoke(p,"饭"); //吃...饭

//获取所有public修饰的方法
Method[] methods = personClass.getMethods();
for (Method method : methods) {
System.out.println(method);
String name = method.getName();
System.out.println(name); //输出所有public方法的名称
}

反射会产生的安全性问题

Java反射机制是一种强大的工具,但同时也可能引发一些安全性问题,主要包括以下几个方面:

  1. 破坏封装性:通过反射,可以访问和修改类的私有成员变量、方法和构造函数,从而破坏了类的封装性。这可能导致程序的不稳定性和安全隐患,使得不恰当的访问可能产生意外的结果。
  2. 绕过访问控制:通过反射,可以绕过Java语言的访问控制机制,访问和修改类的私有成员变量和方法。这可能导致未经授权的访问,破坏了程序的安全性。
  3. 执行恶意代码:恶意用户可以利用反射机制执行恶意代码,例如在运行时动态加载恶意类并执行其中的恶意方法,从而实现攻击目标系统的目的。
  4. 潜在的性能问题:使用反射可能会导致性能下降,因为反射调用通常比直接调用方法或访问变量要慢,而且会消耗更多的资源。因此,过度使用反射可能会影响程序的性能。

比如以下这个类,有一个final修饰的num变量,final修饰的变量只能在类初始化时赋值,并且不可改变

1
2
3
4
5
6
public class MyClass {
public final int num;
public MyClass(int num){
this.num = num;
}
}

通过setAccessible方法可以编译期的安全检测,修改final变量

setAccessible(boolean flag) 方法是 Java 反射机制中的一个方法,用于设置 Field、Method 或 Constructor 对象的可访问性。通过调用该方法并传入 true 参数,可以关闭 Java 语言的访问控制检查,使得无论访问修饰符是什么,都可以访问该对象。

通常情况下,如果尝试访问一个私有成员(字段、方法或构造函数),或者是对于受保护或默认访问权限的成员,Java 编译器会执行访问控制检查,防止直接访问这些成员。但是,通过调用 setAccessible(true) 方法,可以绕过这种访问控制检查,从而获得对这些私有成员的访问权。

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
MyClass obj = new MyClass(10);
Class<MyClass> myClass = MyClass.class;
Field num = myClass.getField("num");
num.setAccessible(true);
num.set(obj, 20); //IllegalAccessException
System.out.println(obj.num);
}
}

反射
http://example.com/2023/06/26/Java/Java进阶/反射/
作者
PALE13
发布于
2023年6月26日
许可协议