反射与代理机制是Java中比较高级的一种特性,它完全是站在Java虚拟机的角度去看待各种类的运行,特别是在Java EE中运用广泛。在学习之前,首先思考几个问题:
- 给定一个类的名字(字符串形式),怎么创建该类的对象?
- 什么是反射机制?
- Java静态代理和动态代理的异同有哪些?
- Java静态代理和动态代理的优缺点是?
接下来此篇博文来一一探索并解决以上问题,借此学习反射与代理机制~
一. 反射机制(reflection)
当Java程序在运行过程中需要识别每一个对象的类型,这种识别方法有两种:
- RTTI(Run-Time Type Identification)
- Java反射机制
1.RTTI(Run-Time Type Identification)
首先举个例子来了解第一种方法RTTI,例如有一个类Shape,然后它有3个子类Circle、Square、Triangle,都重新实现了方法draw()
,类图如下:
Shape[] orchestra = {
new Circle(),
new Square(),
new Triangle()
};
观察以上代码,现在创建一个数组,声明时是父类的类型,但是存储的数据是具体实现的子类。
若需要将元素从数组中取出来时,Java虚拟机会自动将元素类型转换为Shape,这就是RTTI最基本的使用形式。因为在Java当中所有的类型转换都是在运行时进行操作,并且经过正确性检查的。
大部分代码尽可能少地了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(如这个例子中的Shape)。这样的代码形式容易理解且便于维护。
2. Java反射机制
重点还是回到Java程序在运行过程中需要识别每一个对象的类型进行识别所使用的方法,第一种已经初步了解了,接下来着重了解第二种方法——Java反射机制。
(1)定义
Java反射机制是在运行状态中,能够知道任何一个类的所有方法和属性;可以调用任何一个对象的方法或属性,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
(2)类Class
为了易于读者理解,在介绍类Class之前,先介绍一个众所熟知的基础而又特殊的类Object,Java中常说万物皆对象,而这个Object相当于万物之父,可以表示任意的对象,也是所有对象的父类。来了解它的基础方法:
方法 | 含义 ____ |
---|---|
hashCode() | 获取任意一个Object对象的哈希值 |
equals() | 用于比较两个对象是否为同一个对象 |
clone() | 克隆一个对象 |
toString() | 将一个对象转换为字符串 |
notify() | 通知 |
wait() | 等待 |
在熟悉了Object类后,就要引出Java中另一个基础而又特殊的类——Class。在每装载一个新类的时候,Java虚拟机就会在Java堆中创建一个Class的实例,这个实例就代表这个Class类型,通过实例获取类型信息。该类中一些常用的方法如下:
方法 | 含义 ______________________ |
---|---|
Method[] getMethods() | (Method是java中的一个类)该方法就是返回Class这个实例类中所有的方法,以数组形式返回 |
Field[] getFields() | (Field也是java中的一个类,表示类中的成员变量)该方法就是返回Class这个实例类中所有的成员变量,也就是域,以数组形式返回 |
Constructor[] | 该方法就是获得Class这个实例类中所有的构造方法 |
了解完Class类常用的三个方法后,我们可以意识到实际上此类就是帮组获得某一个类的所有方法、域、构造方法,获取后就可以调用使用类实例。
(3)用Class类创建实例
//创建Class类的一个对象,返回一个类的引用
Class class = Class.forName("Airplane");//返回一个类型
//通过类的引用创建实例
class.newInstance
需要注意的是Class类这个类型的对象表达的是一个类的引用。这个forName()
是Class类的一个静态方法,这样class对象代表的是Airplane这个类型,得到这个类型引用了,接下来需要创建此类型,还是Class类的静态方法 —— newInstance()
,此方法返回值实际上是一个实例,即之前指定好的Airplane,创建实例过程中一般是调用Airplane类的默认构造方法。
所以,通过以上两行代码就可以利用一个类名字符串去创建对应的类实例,来查看一个完整的例子:
class Airplane{
public String toString(){
return "in airplane";
}
publc class CreateInstance{
public static void main(String[] args) throws Exception{
//创建Class类的一个对象,描述了类Airplane
Class class = Class.forName("Airplane");
System.out.println(class);
//创建实例的另外一个方法
Object object = class.newInstance;
System.out.println(object.toString());
}
}
}
输出:
class Airplane
in airplane
我们知道传统创建对象是通过new来操作,即Airplane air = new Airplane()
,但是现在你知道了可以利用一个类的名字去创建这个类的实例,这样更加灵活。
(4)Method类的invoke
上例已获得类实例,那么如何调用该类的方法呢?
Java反射机制中已提供了这种途径:有一个类Method中的invoke
方法,该方法对带有指定参数的指定对象,调用Method对象中的表示的方法,即可获取目标对象的方法。看下例:
public class ClassA{
public void add(Integer param1, Integer param2){
System.out.println(param1.intValue() + param2.intValue())
}
public void StringAdd(String abc){
System.out.println("out" + abc);
}
public static void main(String[] args){
try{
Method mth = ClassA.class.getMethod("add", new Class[]{Integer.class, Integer.class});
mth.invoke(ClassA.class.newInstance(), new Integer(1), new Interger(2));
Method mth1 = ClassA.class.getMethod("StringAdd", new Class[]{Integer.class, Integer.class});
mth1.invoke(ClassA.class.newInstance(), "--test");
}catch(Exception e){}
}
}
输出:
3
out--test
可以看出ClassA这个类中包含两个基本方法add
、StringAdd
,重点是try catch块中的代码,又接触了Class类的一个新方法getMethod
,为了可以调用ClassA中的add
方法,则获取ClassA类的Method对象,其中传入的参数是方法名和方法参数。接着调用Method对象的invoke方法,第一个参数是方法归属类的实例对象,用法已经熟悉了,然后是调用add
方法需要传入的参数。这样两行代码完成了对指定对象方法的调用,下一个方法的使用如法炮制,不再赘述。
所以通过Method类可以表示任意一个类中的方法,再调用Method类的invoke方法可以调用指定对象的方法。
二. Java静态代理
首先来了解“代理”的含义,在Java中有些类或者对象不愿直接被访问、控制,它们通过中间的一个中介渠道来进行访问、控制。举个生活中的例子,比如打官司过程律师就是代理,租房子时中介就是代理。在Java中任意对象都可以有代理,只是取决于什么时候使用,这就引出代理模式。
1. 代理模式介绍
- 在某些情况下,一个客户不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。例如权限原因导致客户端对象不能直接访问目标对象,此时可以通过中介来架起两者的桥梁。
- 代理模式的作用是为其它对象提供一种代理去控制或访问目标对象.
2. 代理模式角色及使用实例
查看上图,左上角是客户端Client,它想要调用目标对象的requrast
方法,左下角ProxySubject是一个代理对象,与右下角RealSubject真是对象拥有同样的访问接口,但是它还要做其它一些事情。以上我们清楚认识到了代理模式涉及到的角色:
- 抽象角色:声明真实对象和代理对象的共同接口。即这个真实对象和代理角色都能够提供哪些方法。
- 代理角色:代理对象角色内部包含对真实对象的引用,从而操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能够替代真实对象。同时,代理对象可以在执行真实对象操作时,附加其它的操作,相当于对真实对象进行封装,即在调用真实对象的方法时,可以在调用之前做一些预处理操作,在调用之后做一些后处理,诸如之类。
- 真实角色:代理角色所代表的真实对象,使我们最终要引用的对象。
//真实对象和代理对象的共同接口
abstact class Subject{
public abstact void request();
}
//真实角色
class RealSubject extends Subject{
public void requrst(){
System.out.println("From Real Subject");
}
}
//代理角色
class ProxySubject extends Subject{
//代理角色对象内部含有对真实对象的引用
private RealSubject realSubject;
@Override
public void requeat(){
//在真实角色操作之前的预处理操作
preRequest();
if(null == realSubject){
realSubject = new RealSubject();
}
//真实角色完成的操作
realSubject.request();
//在真实角色操作之后的后处理
postRequest();
}
private void preRequest(){
System.out.println("pre Request");
}
private void postRequest(){
System.out.println("post Request");
}
}
//客户端
public class Client{
public static void main(String[] args){
Subject subject = new ProxySubject();
subject.request();
}
}
以上代码重点放在代理类和其操作,首先抽象角色Subject声明好代理角色和真实角色共有的方法,然后代理角色ProxySubject 和真实角色RealSubject 分别具体实现。最后,我们在main
方法中创建了一个代理对象,通过代理角色这个中介间接调用了真实角色的操作(因为代理角色ProxySubject 内部持有真实角色RealSubject 的引用),并且代理角色中还可以做一些预处理和后处理操作。以上就是Java静态代理的一个基本例子。
3. Java静态代理的优缺点
- 优点:简单明确,业务类只需要关注业务逻辑本身,保证了业务类的重用性,这是代理的共有优点。
- 缺点:代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。不仅如此,后期增加抽象角色中的方法,会导致所以代理类和真实类都要实现此方法,增加了代码维护的复杂度。
三. Java动态代理
Java动态代理顾名思义是相较于上一节的静态代理,必定改进了静态代理存在的一些缺点。首先来了解涉及到的类:
1. 类Proxy
它是Java动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。来查看主要方法:
方法 | 含义 ____ |
---|---|
static InvocationHandler getInvocationHandler(Object proxy) | 用于获取指定代理对象所关联的调用处理器 |
static Class getProxyClass(ClassLoader loader, Class[] interfaces) | 用于获取关联于指定类装载器和一组接口的动态代理类的类对象 |
static boolean isProxyClass(Class cl) | 用于判断指定类对象是否是一个动态代理类 |
static Object newProxyInstance(ClassLoader loader, class[] interfaces, InvocationHandler h) | 用于指定类装载器、一组接口及调用处理器生成动态代理类实例 |
2. 类InvocationHandler 及实例
这是调用处理器接口,它自定义了一个invoke
方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
Object invoke(Object proxy, Method method, Object[] args)
该方法负责集中处理动态代理类上的所有方法调用,第一个参数是代理类实例,第二个参数是委托类的方法对象,第三个参数是调用方法集合。调用处理器根据这三个参数进行预处理或分派到委托类实例上执行。
invoke这个方法很强大,它可以调用任意一个代理对象的方法,传入参数,然后执行该方法。来看一个例子:
//抽象角色
interface Subject{
public void request()
}
//真实角色
class RealSubject implements Subject{
public void requrst(){
System.out.println("From Real Subject");
}
}
//代理角色,必须实现InvocationHandler
class DynamicSubject implements Subject{
private Object sub;
public DynamicSubject(){}
public DynamicSubject(Object obj){
sub = obj;
}
public Object invoke(Object proxy, Method method, Object[] args)throws Throwable{
System.out.println("before calling " + method);
method.invoke(sub, args);
System.out.println("after calling " + method);
return null;
}
}
//客户端
public class Client{
public static void main(String[] args){
//指定被代理类
RealSubject rs = new RealSubject();
InvocationHandler ds = new InvocationHandler(rs);
//一次性生成代理
Class cls = rs.getClass();
Subject subject = (Subject)Proxy.newProxyInstance(cls.getClassLoader(), cla.getInterfaces(),ds);
subject.request();
}
}
首先来看代理类DynamicSubject ,它需要实现的接口是InvocationHandler,在invoke
方法中操作真实类RealSubject。在静态方法中,调用真实类的重点是持有真实类的引用,在动态中查看invoke
方法的参数,它还持有真实类具体方法Method的引用。同样的,在调用真实类的具体方法前后可以进行一些预处理和后处理操作。
接着查看main
方法中时如何使用代理类:
- (1). 首先实例化一个真实类RealSubject 的对象,它将会被用来指定代理类;
- (2). 接着实例化一个代理类DynamicSubject ,同时将真实类对象传给代理类的构造方法,表示这个代理类代理的是哪一个真实类,最后赋值给调用的处理器InvocationHandler ,因为代理类实现了此接口,这也是一个“向上转型”;
- (3). 获取这个真实类实例化对象的类型Class;
- (4). 获取Subject实例化对象,但是此对象是通过Proxy类的
newProxyInstance
来产生的,三个参数分别是真实类对象的类装载器、真实类对象的接口,调用处理器。 - (5). 最后调用抽象角色Subject的指定接口方法。这样后续会调用到代理类的invoke方法,而invoke方法里间接调用了真实类的指定方法,最后完成这代理模式的真实类调用。
3. 动态代理的特点
包:如果所代理的接口都是public的,那么将被定义在顶层包(即包路径为空), 如果所代理的接口中有非public 的接口,那么它将被定义在该接口所在包,这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题二无法被成功定义并访问。
类修饰符:该代理类具有final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表Proxy类第N此生成的动态代理类,值得注意的是:并不是每次调用 Proxy 类的静态方法创建动态代理类都会使得N 值增加,原因是如果对同一组接口(包括接口排列顺序相同)试图重复创建动态代理类,它会返回先前已经创建好的代理类的类对象,而不是重复创建一个新的代理类。这样可以节省不必要的重复代码,提高了代理类的创建效率。
类继承关系:该类的继承关系如图:
4. 动态代理的优缺点
优点:动态代理与静态代理想比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量较多的时候也可以进行灵活处理,而不需要像静态代理那样对每一个接口中的方法进行中转。(上面的例子接口中只有一个方法,如果有2个及以上的方法,静态代理和动态代理的差别会很明显的显示出来)
缺点: Proxy类已经设计得非常优美,但是它始终无法摆脱仅支持 interface代理的限制。
了解完以上知识点后,相信对Java的反射和代理机制有了一定的理解,但是深入的话还需要通过写代码的方式多实践。如有错误,请指教~
希望对你们有帮助:)