本文是我和武哥联合创作,已收录至我们的GitHub,欢迎大家给个Star:https://github.com/nxJava/nx_java
微信搜索:Java学习指南,关注这个专注于分享Java干货的公众号~
我们先看下反射的概念:
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
简单的来说,反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的全限定名称(例如 com.example.Student
),那么就可以通过反射机制来获得类的所有信息。
那么既然有“反”,就应该有“正”,我们没学反射之前是怎么通过正向过程创建对象的呢?
例如我们想创建一个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:
这就是反射,在程序运行的时候动态创建类的实例,并通过实例调用类的方法。
java源文件从创建到运行会经历3个阶段,分别是源码阶段、Class类对象阶段和运行阶段。在java文件编译成class文件后,通过类加载器将class的信息加载进内存,在内存中class字节码文件被描述成3种对象信息,分别是成员变量对象、构造方法对象、成员方法对象,它们都是多个,所以是数组。这个Class对象阶段就是反射机制,我们通过Class可以得到类的所有信息,创建对象和调用其成员方法。
从上面的例子我们可以看出,如果你事先创建好了Student类,那么你可以直接创建Student对象并调用其方法,那么如果我和另外一位老铁分开开发程序,这个Student类是他写的,我们彼此开发完全独立,我需要在我的程序中创建Studnet对象,然后调用Student方法获取学生的年龄,这个时候可能这位老铁还没有完成Student类的开发,这怎么办?
这就用到了反射了,我在开发程序的时候只需要跟他约定好Studnet类的信息,例如类名称,字段名称、方法名称,我就可以通过反射的机制在他还未写Studnet类的时候动态创建Studnet对象并调用其方法了。等他的Studnet类完成创建之后,我们的代码合并部署运行,我就可以调用到Student的方法了。
其实在框架内部处处都是反射的应用,反射是框架的灵魂。比如我们最常用的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对象。
我们常用的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); // 执行接口调用
再比如我们在写代码的时候,创建一个字符串对象,调用其方法,我们是不是通过 ".“来获取方法的?
你看通过”."我们可以看到String对象的所有方法信息,这是怎么做到的?其实这也是利用了Java的反射机制。
idea在运行期间,通过反射获取到了String对象的全部方法,然后列举成一个列表,描述了方法的名称、参数和返回值,我们在使用的时候可以直接选择想要的方法,非常方便。
Java动态代理类位于Java.lang.reflect包下,一般主要涉及到以下两个类:
JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。在CGLIB底层,其实是借助了ASM这个非常强大的Java字节码生成框架。
一种是使用代理工厂创建,另一种通过使用动态代码创建。使用代理工厂创建时,方法与CGLIB类似,也需要实现一个用于代理逻辑处理的Handler;使用动态代码创建,生成字节码,这种方式可以非常灵活,甚至可以在运行时生成业务逻辑。
最简单粗暴的方法就是对着API撸!
获取class对象的三种方式
Class.forName("全类名")
,将字节码文件加载进内存,获取class对象;Class clazz = Class.forName("Student"); // 可能会抛出ClassNotFoundException异常
Class clazz1 = Student.class;
Class<?> clazz2 = new Student().getClass();
注意,同一个字节码.class文件只会被加载一次,无论哪种方式获取的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)这种方式可以忽略其访问限制,从而可以使用类私有的属性、构造器和方法。
获取构造器的作用是为了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);
我们还是用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类中定义的所有成员方法。
反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创建和控制任何类的对象,无需提前硬编码目标类;
创作不易,如果您喜欢这篇文章的话,请你 点赞 + 评论 支持一下作者好吗?您的支持是我创作的源泉哦!喜欢Java,热衷学习的小伙伴可以加我微信: xia_qing2012 ,私聊我可以获取最新Java基础到进阶的全套学习资料。大家一起学习进步,成为大佬!