在正式进入反射之前,我先通过一些例子来尽可能阐述清楚Java反射的作用。
在刚接触Java反射的时候,我被这个名字就给弄糊涂了。反射的英文单词为Reflect
,翻译为反射,但是你要反射给什么鬼?
一提到反射这个词,给我的印象是这样的:
而从反射
这个词我得不出任何信息,因此从开始学便充满了困惑。
怀揣着好奇心,我来看一下反射的英文单词Reflect
。这个单词有反射
的意思,但是仅仅当成反射
来理解,就有点不恰当了。
我们来看看反射的其它解释,其中的一个英文解释为:
sb/sth (in sth) to show the image of sb/sth on the surface of sth such as a mirror, water or glass
【通过镜子或者是水面来显现出某些东西】
为什么要通过镜子或者水面呢?直接显示或者直接告诉你不好吗?
直接有直接的好处,拐弯抹角也有含蓄的优美。小学语文我们可能就学过了两种写作手法,一种是直接描写,另一种是衬托,也叫侧面描写。
举个例子:期末考试你考了第一名,回家的路上你非常高兴,该怎么描述呢?
第一种,直接表达:我期末考试考了第一名,我好高兴,我真高兴!
第二种,衬托:期末考试考了第一名,回家的路上感觉太阳公公在对着我微笑,小鸟在对着我唱歌,花儿在对着我舞蹈。
如果很不幸,你考了倒数第一名,想到爸爸妈妈对你那么殷切的目光,你感觉很对不起他们,也很对不起自己,那还可以这么描述:
第三种,侧面衬托:天空下着小雨,淅淅沥沥,其它同学撑着伞,有说有笑地走着,但是我一抬头,发现天空中只有挥之不去的乌云。
一种是直接表达,另一种则是间接表达,这里的Reflect
便可以理解为是间接表达的含义。直接表达可能太生硬,间接表达有时候更符合我们的行为。
Reflect的另一个英文解释,这便是我们所熟悉的反射了:
to throw back light, heat, sound, etc. from a surface
谈到反射,我们再来看看这个图,光线通过反射可以以各种角度照射出去。
通过上面对Reflect这个单词的理解,我们或许就能体会到Java的反射机制的作用。
1、间接执行:
对比于Reflect
的作用1
我们执行某个类的某一个方法,往往都是先new出来一个对象,然后用点去引用该对象对应的方法,然后用反射却不是这样来执行方法的,反射的执行过程比较绕,具体我们可以看后面的代码。
2、动态执行:
对比于Reflect
的作用2
我们new出来一个对象,那我们就只能调用该对象的方法,或者是充其量调用其父类或者是子类的方法。但是使用Reflect,我们能使用哪个对象,能调用哪个方法,这都是不固定的,Java代码可以在运行时做出调整。就像那个镜子反射图一样,我们只需要调整镜子的位置,我们就能看到更多的内容。
到这里,我们就正式进入反射了。
虽然你可能已经很了解反射了,但是我还是要在这里啰嗦一下。
首先我们要明白这两个概念:编译
和运行
。
对于Java代码而言:
编译
是将你写的代码弄成Java虚拟机可以执行的字节码。
运行
是Java虚拟机运行你写的代码(编译后的字节码文件),然后显示运行结果
Java的反射机制提供了一套API,使用反射可以做到在程序运行期间,在不知道有哪些类,方法,接口,方法和成员变量的情况下,查看或者修改这些类,接口,方法或者是属性。
有人说反射就像是一种魔法,引入了运行时自省
(自省:官方用语,个人觉得这个翻译很好)能力,赋予了 Java 语言令人意外的活力,通过运行时操作元数据或对象,Java 可以灵活地操作运行时才能确定的信息。
如果觉得我前面的的解释不太清楚,我那来继续以大白话的方式说说Java反射的作用。
我们普通人是怎么获得信息的?大部份人都是通过眼睛直接看见,比如张三的双眼直勾勾地看到李四走过来了,那张三便知道李四朝他走来了。如果是一个间谍呢,他们可能通过某个反光镜或者是一块小的反光的玻璃便知道有人朝他走过来,还能以此观察自己的行踪是否暴露了。
两者获取到的信息大体上都差不多,但是方式上却有很大不同,一个是直接的,另一个是间接的在,采用间接的方式无疑会绕很多弯路。
我们直接在程序中new 出一个对象A,然后使用对象A去调用某个方法B,写作:A.B,你将这样的代码以写出来,Java在编译期,也就是当Java把你写的程序翻译成字节码的时候,它就知道你在这个地方要new出一个A对象,然后要调用B方法。
而采用反射的做法,程序在编译的时候并不知道你这个地方使用A对象还是用C对象,使用B方法还是使用D方法。等到运行的时候才会知道具体该怎么走。
Java执行反射的过程就像是你要去某个地方W,第一个知道它的具体经纬度,然后叫了一辆直升飞机,直接就飞过去了。而另一个人却不知道该怎么走,于是他就边走边问,一路上不断有人给他指点,往这里走,往那里走,终于经过一番摸索,走到了W这个地方。很显然,第一种方法快,没有这么么多绕弯,不想麻烦,任谁都会选择第一种方法吧。但是第二种方法有什么优点呢?我们边走边问,不仅可以到达W,我还可以到达任何一个地方。如果采用第一种方法,如果我们不知道W的经纬度或者是告诉了经纬度又忘记的话,那就无法到达目的地了,因为第一个人没有边走边问的能力。
我定义了一个Car类
public class Car {
public Long price;
public String brand;
public Car(Long price, String brand) {
this.price = price;
this.brand = brand;
}
public Car() {
}
}
现在我要获取其成员变量price和brand的值
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//创建一个car的对象
Car porsche911 = new Car(1100000L,"Porsche ");
Class clazz = porsche911.getClass();
Field priceField = clazz.getField("price");
// 相当于是执行:porsche911.getPrice()
System.out.println("通过反射获得price,porsche911.price:" + priceField.get(porsche911));
}
打印结果:
通过反射获得price,porsche911.price:1100000
如果成员变量为private类型的,比如我们把Car类中的price设置为private类型的变量,那又该怎么访问呢?
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Car porsche911 = new Car(1100000L,"Porsche ");
Class clazz = porsche911.getClass();
Field priceField = clazz.getDeclaredField("price");
priceField.setAccessible(true);
System.out.println("通过反射获得price,porsche911.price:" + priceField.get(porsche911));
}
}
只需要将相应的Field的setAccessible方法设置true即可,如代码所示,就多了一句priceField.setAccessible(true)
代码。然后就可以查看private的属性值了,打印出来的值和上面的一样。到这里的确是有些不可思议,private属性就是为了封装起来不让外界直接访问,但是反射就能走后门。
然后我们还可以通过Field的set方法,修改相应对象相应的属性的值。比如下面这句代码就能将porsche911对象的price属性的值修改为1200000L。
priceField.set(porsche911,1200000L);
为了演示效果:我又在Car中增加了如下几个成员变量,Car中的成员变量现在为:
private Long price;
public String brand;
// 轮胎的数量
public static int TYRE_NUMS = 4;
public int speed;
public String color;
运行Test类
public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Car porsche911 = new Car(1100000L, "Porsche");
Class clazz = porsche911.getClass();
// 不能取出private类型的变量
for (Field field : clazz.getFields()) {
System.out.println(field.getType() + " " + field.getName());
}
}
打印结果:
int TYRE_NUMS
class java.lang.String brand
int speed
class java.lang.String color
需要注意的是这样不能打印出private类型的变量。
我在Car类中添加了一个run方法
public void run(){
System.out.println("我是:" + this.brand + ",我百公里加速只要三秒");
}
运行Test类
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Car porsche911 = new Car(1100000L, "Porsche");
Class clazz = porsche911.getClass();
Method runMethod = clazz.getMethod("run");
runMethod.invoke(porsche911);
}
打印结果:
我是:Porsche,我百公里加速只要三秒
我在Car中添加了一个带参数的方法
public void buy(long price) {
if (price >= this.price) {
System.out.println("购买成功");
} else {
System.out.println("购买失败");
}
}
Test类执行
public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Car porsche911 = new Car(1100000L, "Porsche");
Class clazz = porsche911.getClass();
Method buyMethod = clazz.getMethod("buy", long.class);
buyMethod.invoke(porsche911,10000L);
}
打印结果:
购买失败
通过上面使用Java反射来查看修改成员变量或者是调用成员方法,我们会发现要调用某个类的某个方法,或者是查看某个类的成员变量,需要拐弯抹角地执行,远不如直接new出来一个对象,然后通过.
调用其成员方法或者是查看其成员变量这么直接,看似很多此一举。
那什么时候用反射呢?
JAVA的动态代理就是基于反射来做的,而Spring这样的框架也都是大量使用了反射,比如Spring中的AOP。这些内容都比较庞大,三言两语也说不清楚,后期我会单独写出来。
简单总结一下反射的作用:
反射引入了运行时自省的能力,赋予了 Java 语言令人意外的活力,通过运行时操作元数据或对象,Java 可以灵活地操作运行时才能确定的信息。
前面讲了反射的一些基本概念以及基本用法,下面将以动态代理为例来介绍反射在这之中发挥的作用。
在设计模式之中有一个代理模式,何为代理模式?
代理可以看作是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成。就像我们去租房一样,往往都不是直接与房东进行联系,直接找房东租房,而是联系中介。和房东 1 对 1的联系,我们只能租这个房东的房子,和中介联系,我们可以租各式各样的房子,中介这个代理能为我们增加很多选择,但是我们也要花费相应的代价,现实中就是得多花钱。我们在代码中使用代理,增加了开销,但是好处就是我们可以不用修改原代码就可以在原代码的基础上增添非常多的功能。
如下,将演示一个简单的动态代理。
定义一个接口 Hello
package proxy;
public interface Hello {
void sayHello();
void sayHelloToTeacher();
}
编写 Hello 接口的实现类 HelloImpl
package proxy;
/**
* @author <a href="zhaoyingling12@163.com">Zhao Simon</a>
* @program: java-learning
* @description: Hello 接口的实现类
* @since
*/
public class HelloImpl implements Hello {
@Override
public void sayHello() {
System.out.println("Hello!");
}
@Override
public void sayHelloToTeacher() {
System.out.println("Hello Teacher!");
}
}
按照正常的逻辑,接下来我们就可以开始调用了
public class Main {
public static void main(String[] args) {
Hello hello = new HelloImpl();
hello.sayHello();
}
}
输出如下,符合预期。
Hello!
现在我们想给 HelloImpl 的 sayHello 方法增添一些功能,比如在打印 Hello 之前,输出一下当前时间。按照正常的逻辑,我们只需要修改一下 HelloImpl 中的 sayHello 方法。比如,我们可以修改为如下的形式。
@Override
public void sayHello() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
System.out.println("Invoking sayHello: " + format.format(Calendar.getInstance().getTime()));
System.out.println("Hello!");
}
再次进行调用得到如下输出,满足我们的要求。
Invoking sayHello: 2021-04-11-23-53-55
Hello!
这么做也是一种方法,但是如果我有一个要求,不能对 HelloImpl 这个类进行任何修改,毕竟上面那种方法需要对原代码进行修改,对于业务具有侵入性,如果我们每增加一个功能,就要对原代码进行一次修改,那么我们的业务代码将会非常混乱,那这时我们该如何来实现这个功能呢?这就可以使用动态代理了,其底层就是反射。
定义一个 Handler,在该 Handler 中指明我们需要对 HelloImpl 增强哪些功能。
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Calendar;
/**
* @author <a href="zhaoyingling12@163.com">Zhao Simon</a>
* @program: java-learning
* @description:
* @since
*/
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
System.out.println("Invoking sayHello: " + format.format(Calendar.getInstance().getTime()));
Object result = method.invoke(target, args);
return result;
}
}
然后进行调用
package proxy;
import java.lang.reflect.Proxy;
/**
* @author <a href="zhaoyingling12@163.com">Zhao Simon</a>
* @program: java-learning
* @description: 动态代理
* @since
*/
public class MyDynamicProxy {
public static void main(String[] args) {
Hello hello = new HelloImpl();
// 构造代码实例
MyInvocationHandler handler = new MyInvocationHandler(hello);
Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
// 调用代理方法
proxyHello.sayHello();
}
}
输出如下,符合我们的需求,没有对原代码进行修改,使用代理来增加我们想要的功能。
Invoking sayHello: 2021-04-11-23-59
Hello!
同样的,如果我们要在 sayHelloToTeacher 方法执行前,打印下当前时间,同样可以这么修改。
再定义一个 Handler
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Calendar;
/**
* @author <a href="zhaoyingling12@163.com">Zhao Simon</a>
* @program: java-learning
* @description:
* @since
*/
public class MyInvocationHandler2 implements InvocationHandler {
private Object target;
public MyInvocationHandler2(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
System.out.println("Invoking sayHelloToTeacher: " + format.format(Calendar.getInstance().getTime()));
Object result = method.invoke(target, args);
return result;
}
}
还是按照上述方法中的步骤,进行调用。
package proxy;
import java.lang.reflect.Proxy;
/**
* @author <a href="zhaoyingling12@163.com">Zhao Simon</a>
* @program: java-learning
* @description: 动态代理
* @since
*/
public class MyDynamicProxy {
public static void main(String[] args) {
Hello hello = new HelloImpl();
MyInvocationHandler2 handler2 = new MyInvocationHandler2(hello);
Hello proxyHelloToTeacher = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler2);
proxyHelloToTeacher.sayHelloToTeacher();
}
}
输出如下
Invoking sayHelloToTeacher: 2021-04-12-22-23
Hello Teacher!
上面的 JDK Proxy 例子,非常简单地实现了动态代理的构建和代理操作。首先,实现对应的 InvocationHandler;然后,以接口 Hello 为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑。我们实例化的是 Proxy 对象,而不是真正的被调用类型。这种方法有一定的局限性,因为它是以接口为中心的,相当于添加了一种对于被调用者没有实现相关的接口,我们就无法使用这种方式。我们实际调用的是 HelloImpl 类中的方法,但是我们却必须得以 Hello 这个接口为中心。
如果被调用者没有实现接口,而我们还是希望利用动态代理机制,那么就不能使用 Java 自带的 Proxy,可以考虑其他方式。比如说Spring AOP ,Spring AOP 说到底也就是动态代理,它支持两种模式的动态代理,JDK Proxy 或者 cglib,如果我们选择 cglib 方式,就不需要依赖于接口了。
Spring AOP 相关的实践,可以看一下我的这篇文章
因篇幅问题不能全部显示,请点此查看更多更全内容