万字长文深入理解Java反射机制

本文是我和武哥联合创作,已收录至我们的GitHub,欢迎大家给个Star:https://github.com/nxJava/nx_java

微信搜索:Java学习指南,关注这个专注于分享Java干货的公众号~


1. 什么是反射?

1.1 反射概念

我们先看下反射的概念:

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。

简单的来说,反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的全限定名称(例如 com.example.Student),那么就可以通过反射机制来获得类的所有信息。

1.2 举例说明反射机制

那么既然有“反”,就应该有“正”,我们没学反射之前是怎么通过正向过程创建对象的呢?

例如我们想创建一个Student的对象,在这之前我们是不是应该创建Student类:

public class Student {
    private String name; // 姓名
    private Integer age;  // 年龄
    private String address;  // 地址

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

然后我们通过new的方式创建对象,并调用其方法:

Student student = new Student();  // 创建对象
student.setAge(19);  // 执行setAge方法
Integer age = student.getAge();  // 获取学生的年龄
System.out.println(age);

好,到此为止我们通过正向的方式创建了一个Student对象,那么通过反射怎么创建Student对象?我们来看下:

Class<?> clazz = Class.forName("Student");  //获取Student类
Object o = clazz.newInstance();    // 通过类创建对象
Method setAgeMethod = clazz.getMethod("setAge", Integer.class);  // 获取setAge方法。第一个参数是方法名称,第二个参数是方法的参数类型列表,如果有多个参数可以传多个参数的类型
setAgeMethod.invoke(o, 19);  //  执行setAge()方法
Method getAgeMethod = clazz.getMethod("getAge");  // 获取getAge方法
Object age = getAgeMethod.invoke(o);  // 执行getAge方法
System.out.println(age);

以上两种方法都可以打印年龄19:

这就是反射,在程序运行的时候动态创建类的实例,并通过实例调用类的方法。

1.3 更深入地看下反射机制

java源文件从创建到运行会经历3个阶段,分别是源码阶段、Class类对象阶段和运行阶段。在java文件编译成class文件后,通过类加载器将class的信息加载进内存,在内存中class字节码文件被描述成3种对象信息,分别是成员变量对象、构造方法对象、成员方法对象,它们都是多个,所以是数组。这个Class对象阶段就是反射机制,我们通过Class可以得到类的所有信息,创建对象和调用其成员方法。

2. 为什么学反射?

2.1 利用反射分工协作

从上面的例子我们可以看出,如果你事先创建好了Student类,那么你可以直接创建Student对象并调用其方法,那么如果我和另外一位老铁分开开发程序,这个Student类是他写的,我们彼此开发完全独立,我需要在我的程序中创建Studnet对象,然后调用Student方法获取学生的年龄,这个时候可能这位老铁还没有完成Student类的开发,这怎么办?

这就用到了反射了,我在开发程序的时候只需要跟他约定好Studnet类的信息,例如类名称,字段名称、方法名称,我就可以通过反射的机制在他还未写Studnet类的时候动态创建Studnet对象并调用其方法了。等他的Studnet类完成创建之后,我们的代码合并部署运行,我就可以调用到Student的方法了。

2.2 Spring中的反射

其实在框架内部处处都是反射的应用,反射是框架的灵魂。比如我们最常用的Spring框架,在定义一个bean的时候我们可能会这么做:

<bean id="studentBean" class="com.example.Student"/>

在xml文件中定义一个bean,告诉Spring我需要一个Student对象,这个对象的名称是com.example.Student,那么Spring就会通过我们上面那种方式去生成对象:

Class clazz = Class.forName("com.example.Student");  // 通过名称得到Class
Object stuObj = clazz.newInstance();   // 生成对象

生成完对象之后,Spring会把这个对象放在一个容器中,在实际需要的时候通过@Autoware或者@Resource等注解的方式去获取对象,或者通过ApplicationContext获取bean对象。

2.3 Dubbo中的反射

我们常用的RPC框架,例如DUBBO,在接口调用的时候,也是应用反射机制。

RPC框架根据客户端的请求 :接口名称(interface)、方法名称(method)、参数类型(paramtype)、参数(params)等进行反射,dubbo接口之间通信的机制大概是这样的:

通过反射我们可以实现上面的流程:

String className = request.getClassName();  // 获取类名称

Class<?> c = Class.forName(className);  // 得到Class信息

Object serviceBean = c.newInstance();   // 创建对象

String methodName = request.getMethodName();  // 得到调用的接口名称

Class<?> paramTypes = request.getParamTypes();  // 获取接口的参数类型

Object[] params = request.getParams();   // 得到消费方调用接口的参数

Method method = c.getMethod(methodName, paramTypes); // 得到接口

method.invoke(serviceBean , params);  // 执行接口调用

2.4 IDEA中的反射

再比如我们在写代码的时候,创建一个字符串对象,调用其方法,我们是不是通过 ".“来获取方法的?

你看通过”."我们可以看到String对象的所有方法信息,这是怎么做到的?其实这也是利用了Java的反射机制。

idea在运行期间,通过反射获取到了String对象的全部方法,然后列举成一个列表,描述了方法的名称、参数和返回值,我们在使用的时候可以直接选择想要的方法,非常方便。

2.5 反射在动态代理中的应用

2.5.1 JDK动态代理

Java动态代理类位于Java.lang.reflect包下,一般主要涉及到以下两个类:

  1. Interface InvocationHandler:该接口中仅定义了一个方法Object,invoke(Object obj,Method method, Object[] args)。在实际使用时,第一个参数obj一般是指代理类,method是被代理的方法,如上例中的request(),args为该方法的参数数组。这个抽象方法在代理类中动态实现。
  2. Proxy:该类即为动态代理类。

2.5.2 Cglib动态代理

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。在CGLIB底层,其实是借助了ASM这个非常强大的Java字节码生成框架。

2.5.3 Javassist代理

一种是使用代理工厂创建,另一种通过使用动态代码创建。使用代理工厂创建时,方法与CGLIB类似,也需要实现一个用于代理逻辑处理的Handler;使用动态代码创建,生成字节码,这种方式可以非常灵活,甚至可以在运行时生成业务逻辑。

3. 怎么学反射?

最简单粗暴的方法就是对着API撸!


3.1 class对象

获取class对象的三种方式

  • Class.forName("全类名"),将字节码文件加载进内存,获取class对象;
  • 类名.class,通过类的class属性获取;
  • 对象.getClass(),getClass方法在Object类中定义的,所以对象都有这个方法。
Class clazz = Class.forName("Student");   // 可能会抛出ClassNotFoundException异常
Class clazz1 = Student.class;  
Class<?> clazz2 = new Student().getClass();

注意,同一个字节码.class文件只会被加载一次,无论哪种方式获取的Class对象都是同一个。

3.2 成员变量

  • getFields(), 返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问公共字段
  • getField(String name),返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段
  • getDeclaredFields(),返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段
  • getDeclaredField(String name), 返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段

我们来具体演示下。

为了达到演示效果,Studnet类稍作修改,有四个属性,分别使用public、protected、default、private来修饰。

public class Student {
    public String name;
    protected Integer age;
    String address;
    private String phone;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getPhone() {
        return phone;
    }

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

先看下getFieldsgetField方法

Class clazz = Student.class;
 Field[] fields = clazz.getFields();
 for (Field field : fields) {
     System.out.println("getFields======" + field);
 }
 Field field = clazz.getField("name");
 System.out.println("getField name======" + field);
 Field field1 = clazz.getField("age");
 System.out.println("getField age======" + field1);

运行结果:

我们可以看到,getFields只获得了Student的name属性,由于age属性是protected修饰的,所以getField(“age”)抛出了NoSuchFieldException的异常。

那么怎么获得非public修饰的属性?我们可以通过getDeclaredFields和getDeclaredField(‘name’)两个方法来获取:

Field[] declaredFields = clazz.getDeclaredFields();  // 获取所有已声明的属性
for (Field declaredField : declaredFields) {
    System.out.println("getDeclaredFields======" + declaredField);
}
Field ageField = clazz.getDeclaredField("age"); // 获取指定名称的已声明的属性
System.out.println("getDeclaredField age=========" + ageField);

运行结果

可以看到我们拿到了类的所有已声明字段,那么拿到了这些字段我们就可以获取对象对应字段的值了,来试下:

Student student = new Student();   // new一个Student对象
student.setName("小明");   // 设置属性值
student.setAge(20);
student.setAddress("上海市");
student.setPhone("13011220099");
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
    System.out.println("field=" + declaredField.getName() +  ",value=" + declaredField.get(student));  // 获取属性值
}

那么结果会是什么样子的?

可以看到,前3个属性的值打印出来了,也就是说非private修饰的属性可以通过declaredField.get(obj)的方式从对象中获取其属性值,但是private修饰的属性phone在获取属性值的时候会抛出java.lang.IllegalAccessException异常,无法访问。

那么private修饰的属性我们如何从对象中获取其属性值?我们需要加上这一句:

// 忽略访问权限修饰符的安全检查
declaredField.setAccessible(true);

忽略了访问权限修饰符的权限限制,我们就可以通过反射获取任意属性的属性值了。

注意:getDeclared***同样适用于Constructor和Method,分别是getDeclaredConstructor()和getDeclaredMethod(),通过setAccessible(true)这种方式可以忽略其访问限制,从而可以使用类私有的属性、构造器和方法。

3.3 构造方法

  • getConstructor(Class<?>… parameterTypes),返回一个 Constructor对象Constructor,该对象反映 Constructor对象表示的类的指定的公共 类函数。
  • getConstructors(),返回包含一个数组 Constructor对象Constructor<?>[],反射由此表示的类的所有公共构造类对象。
  • getDeclaredConstructor(类<?>… parameterTypes),返回一个 Constructor对象Constructor,该对象反映 Constructor对象表示的类或接口的指定 类函数。
  • getDeclaredConstructors(),返回一个反映 Constructor对象表示的类声明的所有 Constructor对象的数组类Constructor<?>[]

获取构造器的作用是为了new对象,可以通过有参、无参的构造器创建对象,无参构造器创建的对象等同于Class提供的newInstance方式创建对象:

Constructor constructor = clazz.getConstructor(String.class, Integer.class);  // Constructor 有参构造
Object stu = constructor.newInstance("张三", 12);  // 创建对象
System.out.println("Constructor有参构造对象======" + stu);

Constructor constructor1 = clazz.getConstructor(); // Constructor 无参构造
Object stu1 = constructor1.newInstance();  // 创建对象
System.out.println("Constructor无参构造======" + stu1);

Object stu2 = clazz.newInstance();  // Constructor无参构造创建对象简写形式:Class创建对象
System.out.println("Class无参构造======" + stu2); 

运行结果:

3.4 成员方法

  • getMethod(String name, Class<?>… parameterTypes),获取一个指定名称和参数类型的public修饰的方法对象。
  • getMethods(),获取所有的public修饰的方法列表。
  • getDeclaredMethod(String name, Class<?>… parameterTypes),获取已声明的指定名称和参数类型的的成员方法。
  • getDeclaredMethods()),获取已声明的所有成员方法。

我们还是用Student类来演示,先给Student加上两个成员方法:

public void eat() {
    System.out.println("eat");
}

public void eat(String food) {
    System.out.println("eat " + food);
}

现在来利用反射调用下这两个方法:

Student student = new Student();  // 创建Student对象
Method eatMethod = clazz.getMethod("eat");  // 获取eat方法对象
eatMethod.invoke(student);    // 执行eat方法

Method eatMethod1 = clazz.getMethod("eat", String.class);  // 获取有参eat方法对象
eatMethod1.invoke(student, "香蕉");  // 执行有参eat方法

运行结果:

那么getMethods是不是获取Studnet定义的成员方法呢?

Method[] methods = clazz.getMethods();
for (Method method : methods) {
    System.out.println(method);
}

运行结果:

可以看到Object类定义的成员方法也被打印出来了,所以这里我们要注意getMethods()方法会获取类自身以及Object类中定义的所有成员方法。

4. 反射的优缺点

4.1 优点

反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创建和控制任何类的对象,无需提前硬编码目标类;

4.2 缺点

  1. 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。
  2. 使用反射会模糊程序内内部逻辑:程序员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。

创作不易,如果您喜欢这篇文章的话,请你 点赞 + 评论 支持一下作者好吗?您的支持是我创作的源泉哦!喜欢Java,热衷学习的小伙伴可以加我微信: xia_qing2012 ,私聊我可以获取最新Java基础到进阶的全套学习资料。大家一起学习进步,成为大佬!

猜你喜欢

转载自blog.csdn.net/xqnode/article/details/106750384