大家好,我是程序员青戈,一个被Bug耽误了才艺的程序员👦专注于Java领域的知识分享和技术交流,每天会给大家带来Java学习的干货教程📚
微信搜索公众号 Java学习指南,回复 面试 领取一线大厂面试题一套可以进技术交流学习群一起共同进步哦😊
文章目录
- 1. 什么是反射?
- 1.1 反射概念
- 1.2 举例说明反射机制
- 1.3 更深入地看下反射机制
- 2. 为什么学反射?
- 2.1 利用反射分工协作
- 2.2 Spring中的反射
- 2.3 Dubbo中的反射
- 2.4 IDEA中的反射
- 2.5 反射在动态代理中的应用
- 2.5.1 JDK动态代理
- 2.5.2 Cglib动态代理
- 2.5.3 Javassist代理
- 3. 怎么学反射?
- 3.1 class对象
- 3.2 成员变量
- 3.3 构造方法
- 3.4 成员方法
- 4. 反射的优缺点
- 4.1 优点
- 4.2 缺点
- 关于作者
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包下,一般主要涉及到以下两个类:
- Interface InvocationHandler:该接口中仅定义了一个方法Object,invoke(Object obj,Method method, Object[] args)。在实际使用时,第一个参数obj一般是指代理类,method是被代理的方法,如上例中的request(),args为该方法的参数数组。这个抽象方法在代理类中动态实现。
- 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;
}
}
先看下getFields和getField方法
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 缺点
- 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。
- 使用反射会模糊程序内内部逻辑:程序员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
关于作者
程序员青戈
,5年一线Java开发经验,先后在IBM、阿里、科大讯飞踩坑~
微信搜索:Java学习指南
关注我的原创公众号感谢大家的阅读,创作不易,能否请您小手点一点下方的
一键三连
支持一下作者呢😊谢谢~