在java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?答案是肯定的。
这种动态获取类的信息以及动态调用对象的方法的功能来自于java 语言的反射(reflection)机制。
java 反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法
reflection 是java被视为动态(或准动态)语言的一个关键性质。这个机制允许程序在运行时透过reflection
api取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static
等等)、superclass(例如object)、实现之interfaces(例如serializable),也包括fields和methods
的所有信息,并可于运行时改变fields内容或调用methods
在jdk中,主要由以下类来实现java反射机制,这些类都位于java.lang.reflect包中
class类:代表一个类。
field 类:代表类的成员变量(成员变量也称为类的属性)。
method类:代表类的方法。
constructor 类:代表类的构造方法。
array类:提供了动态创建数组,以及访问数组的元素的静态方法
proxy类以及invocationhandler接口:提供了动态生成代理类以及实例的方法
其中,class类是reflection api 中的核心类,它有以下方法:
getname():获得类的完整名字
getfields():获得类的public类型的属性
getdeclaredfields():获得类的所有属性
getmethods():获得类的public类型的方法
getdeclaredmethods():获得类的所有方法
getmethod(string name, class[] parametertypes):获得类的特定方法,name参数指定方法的名字,parametertypes 参数指定方法的参数类型
getconstructors():获得类的public类型的构造方法
getconstructor(class[] parametertypes):获得类的特定构造方法,parametertypes 参数指定构造方法的参数类型
newinstance():通过类的不带参数的构造方法创建这个类的一个对象
每当一個类被载入时,jvm就自动为其生成一个class对象,通过操作class对象,我们可以得到该对象的所有成员并操作它们,举个例子:
运行结果如下:
getname: javatest.student
getfields: [ljava.lang.reflect.field;@4e25154f
getdeclaredfields: [ljava.lang.reflect.field;@70dea4e
getmethods: [ljava.lang.reflect.method;@5c647e05
isinterface: false
isprimitive: false
isarray: false
superclass: java.lang.object
通过反射得到类对象:
获取方式
说明
示例
object.getclass()
每个对象都有此方法
获取指定实例对象的class
list list = new arraylist();
class listclass = list.getclass();
class. getsuperclass()
获取当前class的继承类class
class superclass = listclass. getsuperclass();
object.class
.class直接获取
class listclass = arraylist.class;
class.forname(类名)
用class的静态方法,传入类的全称即可
try {
class c = class.forname("java.util.arraylist");
} catch (classnotfoundexception e) {
e.printstacktrace();
}
primitive.type
基本数据类型的封装类获取class的方式
class longclass = long.type;
class integerclass = integer.type;
class voidclass = void.type;
平常情况我们通过new object来生成一个类的实例,但有时候我们没法直接new,只能通过反射动态生成。
通过反射实例化对象:
实例化无参构造函数的对象,两种方式:
① class. newinstance();
② class. getconstructor (new class[]{}).newinstance(new object[]{})
实例化带参构造函数的对象:
class.getconstructor(class<?>... parametertypes) . newinstance(object... initargs)
接下来举个例子实战一下:
1.反射实例化(无参): javatest.user@7852e922
2.反射实例化(无参): javatest.user@4e25154f
反射实例化(带参): javatest.user@70dea4e 属性name的值: test
正常实例化: javatest.user@5c647e05
通过反射调用method(方法):
获得当前类以及超类的public method:
method[] arrmethods = classtype.getmethods();
获得当前类申明的所有method:
method[] arrmethods = classtype.getdeclaredmethods();
获得当前类以及超类指定的public method:
method method = classtype.getmethod(string name, class<?>... parametertypes);
获得当前类申明的指定的method:
method method = classtype.getdeclaredmethod(string name, class<?>... parametertypes)
通过反射动态运行指定method:
object obj = method.invoke(object obj, object... args)
例:动态操纵method,改变一下上面的例子的主函数:
通过反射调用field(变量):
获得当前类以及超类的public field:
field[] arrfields = classtype.getfields();
获得当前类申明的所有field:
field[] arrfields = classtype.getdeclaredfields();
获得当前类以及超类指定的public field:
field field = classtype.getfield(string name);
获得当前类申明的指定的field:
field field = classtype.getdeclaredfield(string name);
通过反射动态设定field的值:
fieldtype.set(object obj, object value);
通过反射动态获取field的值:
object obj = fieldtype.get(object obj) ;
例:动态操纵field,改变一下上面的例子的主函数:
java反射总结:
1、只要用到反射,先获得class object
2、没有方法能获得当前类的超类的private方法和属性,你必须通过getsuperclass()找到超类以后再去尝试获得
3、通常情况即使是当前类,private属性或方法也是不能访问的,你需要设置压制权限setaccessible(true)来取得private的访问权。但说实话,这已经破坏了面向对象的规则,所以除非万不得已,请尽量少用。
代理:设计模式
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
图 1. 代理模式
为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。
相关的类和接口:
要了解 java 动态代理的机制,首先需要了解以下相关的类或接口:
java.lang.reflect.proxy:这是 java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。
清单 1. proxy 的静态方法
java.lang.reflect.invocationhandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
清单 2. invocationhandler 的核心方法
每次生成动态代理类对象时都需要指定一个实现了该接口的调用处理器对象(参见 proxy 静态方法 4 的第三个参数)。
java.lang.classloader:这是类装载器类,负责将类的字节码装载到 java
虚拟机(jvm)中并为其定义类对象,然后该类才能被使用。proxy
静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 jvm
在运行时动态生成的而非预存在于任何一个 .class 文件中。
每次生成动态代理类对象时都需要指定一个类装载器对象(参见 proxy 静态方法 4 的第一个参数)
代理机制及其特点:
首先让我们来了解一下如何使用 java 动态代理。具体有如下四步骤:
1、通过实现 invocationhandler 接口创建自己的调用处理器;
2、通过为 proxy 类指定 classloader 对象和一组 interface 来创建动态代理类;
3、通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
4、通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
清单 3. 动态代理对象创建过程
实际使用过程更加简单,因为 proxy 的静态方法 newproxyinstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下
清单 4. 简化的动态代理对象创建过程
接下来让我们来了解一下 java 动态代理机制的一些特点。
首先是动态生成的代理类本身的一些特点。
1)包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.cnblogs.wu 包中的某非 public 接口 a,那么新生成的代理类所在的包就是 com.cnblogs.wu),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
2)类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
3)类名:格式是“$proxyn”,其中 n 是一个逐一递增的阿拉伯数字,代表 proxy 类第 n 次生成的动态代理类,值得注意的一点是,并不是每次调用 proxy 的静态方法创建动态代理类都会使得 n 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。
4)类继承关系:该类的继承关系如图:
图 2. 动态代理类的继承图
由图可见,proxy 类是它的父类,这个规则适用于所有由 proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。
接下来让我们了解一下代理类实例的一些特点。每个实例都会关联一个调用处理器对象,可以通过 proxy 提供的静态方法 getinvocationhandler 去获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行,此外,值得注意的是,代理类的根类 java.lang.object 中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashcode,equals 和 tostring,可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。
接着来了解一下被代理的一组接口有哪些特点。首先,要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误。其次,这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。再次,需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。最后,接口的数目不能超过 65535,这是 jvm 设定的限制。
最后再来了解一下异常处理方面的特点。从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于 throwable 接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛 throwable 异常。那么如果在 invoke 方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,java 动态代理类已经为我们设计好了解决方法:它将会抛出 undeclaredthrowableexception 异常。这个异常是一个 runtimeexception 类型,所以不会引起编译错误。通过该异常的 getcause 方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。
机制和特点都介绍过了,接下来让我们通过源代码来了解一下 proxy 到底是如何实现的。
首先记住 proxy 的几个重要的静态变量:
清单 5. proxy 的重要静态变量
然后,来看一下 proxy 的构造方法:
清单 6. proxy 构造方法
接着,可以快速浏览一下 newproxyinstance 方法,因为其相当简单:
清单 7. proxy 静态方法 newproxyinstance
由此可见,动态代理真正的关键是在 getproxyclass 方法,该方法负责为一组接口动态地生成代理类类型对象。在该方法内部,您将能看到 proxy 内的各路英雄(静态变量)悉数登场。该方法总共可以分为四个步骤:
1、对这组接口进行一定程度的安全检查,包括检查接口类对象是否对类装载器可见并且与类装载器所能识别的接口类对象是完全相同的,还会检查确保是 interface 类型而不是 class 类型。这个步骤通过一个循环来完成,检查通过后将会得到一个包含所有接口名称的字符串数组,记为 <code>string[] interfacenames</code>。总体上这部分实现比较直观,所以略去大部分代码,仅保留留如何判断某类或接口是否对特定类装载器可见的相关代码。
清单 8. 通过 class.forname 方法判接口的可见性
2、从 loadertocache 映射表中获取以类装载器对象为关键字所对应的缓存表,如果不存在就创建一个新的缓存表并更新到 loadertocache。缓存表是一个 hashmap 实例,正常情况下它将存放键值对(接口名字列表,动态生成的代理类的类对象引用)。当代理类正在被创建时它会临时保存(接口名字列表,pendinggenerationmarker)。标记 pendinggenerationmarke 的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)代理类正在被创建,请保持等待直至创建完成。
清单 9. 缓存表的使用
3、动态创建代理类的类对象。首先是确定代理类所在的包,其原则如前所述,如果都为 public 接口,则包名为空字符串表示顶层包;如果所有非 public 接口都在同一个包,则包名与这些接口的包名相同;如果有多个非 public 接口且不同包,则抛异常终止代理类的生成。确定了包后,就开始生成代理类的类名,同样如前所述按格式“$proxyn”生成。类名也确定了,接下来就是见证奇迹的发生 —— 动态生成代理类:
清单 10. 动态生成代理类
由此可见,所有的代码生成的工作都由神秘的 proxygenerator 所完成了,当你尝试去探索这个类时,你所能获得的信息仅仅是它位于并未公开的 sun.misc 包,有若干常量、变量和方法以完成这个神奇的代码生成的过程,但是 sun 并没有提供源代码以供研读。至于动态类的定义,则由 proxy 的 native 静态方法 defineclass0 执行。
代码生成过程进入结尾部分,根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。
走完了以上四个步骤后,至此,所有的代理类生成细节都已介绍完毕,剩下的静态方法如 getinvocationhandler 和 isproxyclass 就显得如此的直观,只需通过查询相关变量就可以完成,所以对其的代码分析就省略了。
代理类实现推演:
分析了 proxy 类的源代码,相信对 java 动态代理机制形成一个更加清晰的理解,整理一下思绪,一起来完成一次完整的推演过程吧。
清单 11. 代理类中方法调用的分派转发推演实现
模拟推演为了突出通用逻辑所以更多地关注正常流程,而淡化了错误处理,但在实际中错误处理同样非常重要。从以上的推演中我们可以得出一个非常通用的结构化流程:第一步从代理接口获取被调用的方法对象,第二步分派方法到调用处理器执行,第三步返回结果。在这之中,所有的信息都是可以已知的,比如接口名、方法名、参数类型、返回类型以及所需的装箱和拆箱操作。
接下来让我们把注意力重新回到先前被淡化的错误处理上来。在异常处理 1 处,由于我们有理由确保所有的信息如接口名、方法名和参数类型都准确无误,所以这部分异常发生的概率基本为零,所以基本可以忽略。而异常处理 2 处,我们需要思考得更多一些。回想一下,接口方法可能声明支持一个异常列表,而调用处理器 invoke 方法又可能抛出与接口方法不支持的异常,再回想一下先前提及的 java 动态代理的关于异常处理的特点,对于不支持的异常,必须抛 undeclaredthrowableexception 运行时异常。所以通过再次推演,我们可以得出一个更加清晰的异常处理 2 的情况:
清单 12. 细化的异常处理 2