反射
什么是反射?
理解class对象
考虑我们声明一个类时,实际上我们所做的只是在硬盘上新建了一个.class文件。当我们new一个该类的对象时,JVM会将我们的.class文件加载到内存中,这个过程是加载。而在将.class读入内存时,JVM自动创建一个class对象,用于表示这个类,一个类只有一个class对象。加载完成后,还需要经过链接(到静态区域分配存储空间等),最后才是执行类的初始化函数。而我们重点考虑加载过程。
Class对象可以说就是这个类的一个抽象,其标识了这个类的所有信息。JVM必须根据Class对象才能正确生成一个类的实例。
更加深究的话,JVM在将.class文件加载进入内存时,会调用类加载器ClassLoader的defineClass方法来构造这个Class对象,因此Class对象不能显式声明。
继续深入的话我们可以研究ClassLoader。其功能就是用于加载Class,而我们意识到,实际场景中我们不一定只是用.class文件,还有可能是.jar包,甚至网络传输的字节流。我们将这所有的可能承载类信息的数据统一看作字节码,它可能具有不同的格式。而ClassLoader的工作很简单,就是相当于解码这些字节码并构建相应的Class对象,同时作为Class对象的容器。因此,不同类型的字节码数据通常对应不同的ClassLoader,甚至可以定制ClassLoader来实现数据加密。
延迟加载
延迟加载,就是一般意义上的懒加载,指JVM在运行时按需加载类对象,即不会一次性加载所有Class对象,而是在遇到没见过的Class名称时调用ClassLoader加载对象并储存。甚至,当你访问一个类的静态字段时,其实例字段并不会加载,直到你真正实例化其对象。
反射
反射是一种Java语言的机制,其指的是Java类的各个成分都会被映射为一个个的Java对象,这也就是上面我们提到的将承载类信息的数据加载成为Class对象的理论核心。同时,Java还将类的字段、方法、构造器都进行了进一步的抽象,即Field、Method和Constructor三个类。而利用反射机制,才是我们程序员学习其机制的根本目的。
利用反射,我们能够根据类名创建实例,即实现动态加载。这是有赖于Java的懒加载特性的。比如我们在某个业务中,可能运用ObjectA来面向移动端,而运用ObjectB来面向桌面端,我们如果要在一份代码中实现业务目标,需要对场景做判断,然后再按需实例化对象。但是实际场景中有一种场景非常独特——框架,作为框架设计者,你不知道用户传入的类的名称是什么,你所做的就是按照其传入的参数来实例化对应的类。利用反射机制,可以通过用户传入的参数来获取对应Class对象并进行实例化,而不必进行复杂的条件判断。
同时,通过反射获取Method类对象,你还可以通过传入的参数来动态调用类的方法(Method.invoke)并获得返回值;通过反射来获取Field类对象,从而得到类所有字段的名称;通过反射来获取Constructor对象从而创建一个相应类的实例。
利用反射我们能够做到什么?
反射是框架的基础
正如上文所提到的,反射机制为框架技术提供了极大的便利。用户只需要按一定规范传入自己编写的类的Class对象,框架系统就能够根据这些对象进行分析和构建,从而实现其业务功能。Java的反射机制是非常强大的,其能够让编程人员灵活地对所有加载到JVM中的类继续检测,极大地提高了在Runtime的灵活性。
反射API
假设我们有一个类Student:
class Student{
private Integer age;
public String name;
/**
*无参构造方法
*/
Student(){
}
/**
*有参构造方法
*/
Student(String name, int age){
this.name = name;
this.age = age;
}
/**
*私有构造方法
*/
private Student(String name, int age){
this.name = name;
this.age = age - 10;
}
public getAge(){
return this.age - 10;
}
public setAge(int age){
this.age = age;
}
}
然后我们在假设我们不知道自己使用的是这个Student类的情况下,编写一个检测其信息的框架程序:
Class Frame{
public static void detectClass(String classFullPathName){
try{
Class who=Class.forName(classFullPathName);//根据传入的全路径名称找到目标类
Constructor[]cons=who.getConstructors();//获取所有的公共构造方法
Constructor[]allCons=who.getDeclaredConstructors();//获取所有的构造方法,无论限定符
Constructor nonParaCon=who.getConstructor(null);//获取公有无参构造
Constructor paraCon=who.getConstructor(String.class,int.class);//根据传入的参数(注意是Class对象)获取对应的公有有参构造方法
Object someone=paraCon.newInstance(new Object[]{"Qiume",18});//以Object[]的方式传入参数,来调用有参构造方法,从而生成该类实例
Field f=who.getField("age");
Method getMethod=who.getMethod("get"+f.getName().substring(0,1).toUpperCase+f.getName().substring(1));
System.out.printLn(f.toString()+":"+getMethod.invoke(someone,new Object[]{}));
//利用字段名称获取对应get函数,然后通过Method.invoke指定调用someone实例的getAge方法并获得返回值
}catch(Exception e){
e.printTracktrace();
}
}
}
动态代理
动态代理是Java中利用反射机制实现的一种重要方法,这篇文章讲的尤为详细:java动态代理实现和原理详细分析
代理模式
设计模式中的代理模式,指设置代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。
这模式的好处在于,委托类的代码可以在不修改的情况下,通过代理类实现业务逻辑的修改,这支撑了AOP——面向切面编程的思想,使得我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,通过代理来切入一些其他操作。
代理的不同方式
静态代理
静态代理指在程序运行前我们就指定好了代理类和委托类的具体类型,并完成了实现。这是无需依托反射机制的,在此按下不表。
动态代理
动态代理中,代理类并没有在运行前定义好,而是在运行过程中动态生成的,这是我们需要重点学习的。通过动态代理,我们能够很好地对代理类的行为进行统一的处理,比如统一在一个被代理的方法前切入特定的行为,静态代理下我们需要找到所有调用这个方法的地方并添加行为,而动态代理允许我们进行统一的管理。
在Java中若要实现动态代理,首先需要实现一个继承自InvocationHandler的自己的类MyInvocationHandler,其需要接受一个委托类的实例对象进行初始化。该类需要实现invoke
方法,该方法接受一个代理类实例proxy、被代理的方法method、以及其他参数。每当调用被代理的方法时,InvocationHandler.invoke会调用method.invoke来通过反射机制调用目标方法,而同时你可以在InvocationHandler.invoke中调用method.invoke前后切入其他的行为。
在Java中,通过一个委托类Target、一个表明切入行为的MyClassHandler对象,我们能够得到一个代理类对象:
InvocationHandler handlerInstance = new MyInvocaionHandler<Target>(targetInstance);
Target proxy = (Target)Proxy.newProxyInstance(Target.class.getClassLoader, new Class<?>[]{Person.class}, handlerInstance);
当调用这个proxy对象的method时,其按MyInvocationHandler.invoke定义的切入行为调用targetInstanvce的对应方法。
注意逻辑的层次:
- InvocationHandler是以委托类Target对象为参数初始化的InvocationHandler对象。
- proxy对象是以InvocationHandler类对象为参数初始化的委托类对象(或者严格意义是和委托类实现同一接口)。
- proxy对象所属的类是以委托类Target为参数生成的,其接口形式与委托类Target完全一致。
- proxy对象持有一个handlerInstance对象,handlerInstance对象又持有一个targetInstance对象。调用proxy的method就会触发handlerInstance的invoke,传入的参数就targetInstance里的同名method。