第二章 代理模式
一、简介
你可能会对这个设计模式感到陌生,但是你对 Spring 一定不陌生,而 Spring AOP 就是代理模式的一种实现,因此它的重要性不言而喻
简而言之,代理模式是设置一个中间代理来控制访问被代理对象,以达到增强源被代理对象的功能和简化访问方式
代理模式 UML 类图
代理模式分为静态代理和动态代理,其中动态代理又分为 JDK 代理和 CGB 代理
一、静态代理
静态的代理在使用时,需要定义一个接口,然后让被代理对象和代理对象一起实现这个接口
下面举个例子,设计一个 Tank 类作为代理类,再设计两个代理类,以达到可以在不修改被代理类的情况下为其添加日志和计时功能
第一步,定义一个接口
interface Movable{
void move();
}
第二步,定义一个被代理类
class Tank implements Movable{
@Override
public void move(){
System.out.println("Moving......");
}
try{
Thread.sleep(new Random().nextInt(10000));
}catch(InterruptedException e){
e.printStackTrace();
}
}
第三步,定义一个代理类
class TankTimeProxy() implements Movable{
Movable m;
public TankTimeProxy(Movable m){
this.m = m;
}
@Override
public void move(){
long start = System.currentTimeMillis();
super.move();
long end = System.currentTimeMillis();
System.out.println("运行了 " + end - start + " ms");
}
}
第四步,再定义一个代理类
class TankLogProxy{
Movable m;
public TankLogProxy{
this.m = m;
}
@Override
public void move(){
System.out.println("start...");
m.move();
System.out.println("stopped!");
}
第五步,写 main 方法
public static void main(String args[]){
new TankLogProxy(
new TankTimeProxy(
new Tank;
)
).move();
}
对于代理类来讲,它们都实现了 Movable 接口并且聚合了一个 Movable 对象,它们能代理的类型也实现了 Movable 接口,这是它们能够互相嵌套的前提
在 main 方法里,我们先 new 一个被代理类,然后把这个被代理类作为参数 new 一个 代理类,然后再把这个被代理类作为参数 new 另一个代理类,最后再调用 move() 方法,这样就实现了在不修改被代理类的情况下为其添加日志和计时功能
现在来思考一个问题,如果我想让 LogProxy 可以重用,不只是用来代理 Tank,而是让它可以代理其他任何类型
但是静态代理只能代理继承了 Movable 接口的被代理类,那样的话就只能继承 Object 类,但是那样就很不方便
这个问题从本质上来讲,是想实现代理行为和代理对象分离。但是我们之前把代理对象写死了,只能代理实现 Movable 接口的类
现在我们希望能代理各种各样的类,如果仍然使用静态代理的话就很麻烦,因为无从得知即将代理的是什么类,也无从得知类的方法有哪些。之前的 Tank 类,我们知道它实现了 Movalbe 接口,所以我们知道它有一个 move 方法,所以我们能通过重写 move 方法然后嵌套,这样就可以为 move 方法扩展新的功能
现在的问题就是我们不知道被代理类实现了什么接口,万一被代理没有实现接口该怎么办
这个时候我们就需要动态代理
在介绍动态代理之前我们现总结一下静态代理的问题:
- 代码冗余,由于代理类要实现与被代理对象一致的接口,所以会产生过多的代理类
- 不易维护,一旦接口增加方法,被代理类和代理类都要进行修改
二、动态代理
动态代理是什么意思呢?就是说代理类的代码不是我们自己写的,而是在运行时后台动态生成的
动态代理有很多种实现方式,我们先从 JDK 代理入手
1.JDK 代理
我们仍然举 Tank 类的例子
前两不是定义接口和定义 Tank 类,和静态代理一致,这里就不重复写了
第三步,定义一个调用时处理器,里面写需要扩展的功能
class LogHandler implements InvocationHandler{
Tank tank;
public LogHandler(Tank tank){
this.tank = tank;
}
@Override
public Object invoke(Object proxy,Method method,Object[] args) throw Throwable{
System.out.println("Method" + method.getName() + "start...");
Object o = method.invoke(tank,args);
System.out.println("method" + method.getName() + "end!");
return o;
}
}
第四步,写 main 方法
public static void main(String argps[]){
Tank tank = new Tank();
//创建代理实例
//静态代理是明确生成了一个 TankLogProxy 对象
//这里的 newProxyInstance 相当于是直接在运行时生成了一个 TankLogProxy,连代理类都不用我们写,只需要写扩展功能
//newProxyInstance() 有三个参数
//1、ClassLoader loader 你要使用哪一个 ClassLoader,使用把你 new 出来的代理对象加载到内存时得 ClassLoader 即可
//2、Class<?> [] interfaces 你希望代理类实现哪些接口
//3、InvocationHandler h 调用时处理器
Movable m = (Movable)Proxy.mewProxyInstance(Tank.class.getClassLoader(),
new Class[]{Movable.class},
new LogHandler(tank));
m.move();
运行结果:
method move start…
Moving…
method move end!
这里有一个奇怪的现象,我们认真地看一下 main 方法,我们会发现,没有任何语句调用了 InvocationHandler 的 invoke 方法,我们只调用了 move 方法,但是 invoke() 里写的日志功能也被打印出来了
因此我们可以合理地推测,是我们在调用 move() 的过程中,在一个我们看不见的地方隐式地调用了 invoke 方法
我一步一步地解释一下整个过程
在执行 newProxyInstance()方法时,会生成一个 $Proxy() 类的.class 文件,这个类就是根据我们传入的参数按照一定规则生成的代理类
这个代理类继承了一个父类叫 Proxy,这个父类里有一个聚合对象叫 InvocationHandler h,跟我们的第三个参数是同一类型的
这个父类的构造函数是有参构造,传入的参数为 InvovationHandler h,并且令 this.h = h,也就是用这个参数给聚合对象 h 赋值
代理类作为它的子类也有一个有参构造,参数为 InvocationHandler var1,只是它不像它的父类有一个 InvocationHandler h
它的构造函数里只有一句 super(var1),即让 var1 作为参数调用父类构造
这样,父类的聚合对象 h 就被指定为我们传入的第三个参数,也就是我们自己定义的调用时处理器
我们再来仔细分析一下我们调用的 move() 是哪个类的方法?是被代理类的吗?
显然不是,我们创建的 m 对象实际上是 $Proxy0 的实例,因此我们调用的是 $Proxy0 的方法
这样就破案了,原来是 $Proxy0 做的手脚,它在自己的 move 方法里调用了 InvcationHandler 的 invoke 方法
因此,我们的扩展功能得以实现
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。动态代理除了通过 JDK 反射来实现之外还有其他各种各样的方式
比如 Instrument,它是利用一个钩子函数,在 class 加载到内存之前将其拦截,并且对其进行一些定制,使其具有扩展功能
但是这种方式用的太少了,因为太过复杂,你必须理解 class 文件的二进制码中的每一个 0 和 1 的实际含义
在介绍 cglib 之前,我们总结一下 JDK代理和静态代理的区别:
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的 class 文件
- 动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时动态生成类字节码,并加载到 JVM 中。而且,动态代理类不需要实现接口,但是要求被代理类要实现接口
还有一种常用且简单的方式,cglib(Code Generation Library ),也叫子类代理
2.CGLib 代理
cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口
它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 dynaop,为他们提供方法的 interception(拦截)
cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类
不鼓励直接使用 ASM,因为它需要你对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉
第一步,定义一个被代理类,不实现任何接口
class Tank{
public void move(){
System.out.println("Moving......");
try{
Thread.sleep(new Random.nextInt(10000));
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
第二步,定义一个拦截器,扩展功能
class TimeMethodInterceptor implements MethodInterceptor{
@Override
public Object intercept(Object o,Method method,Object[] objects,MethodProxy methodProxy){
System.out.println("before");
Object result = null;
result = methodProxy.invokeSuper(o,objects);
System.out.println("after");
return result;
}
}
第三步,写 main 方法
public static void main(String args[]){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Tank.class); //把 enhancer 的父类设置为 Tank
//设定回调函数,TimeMEthodInterceptor 是一个拦截器,相当于 InvocationHandler
enhancer.setCallback(new TimeMethodInterceptor());
Tank tank = (Tank)enhancer.create(); //生成动态代理
tank.move(); //调用 move(),同时会调用到 intercept() 方法,执行我们的操作
我们来思考一下,使用 cglib 确实简单,但是它是怎么实现的呢
enhancer.setSuperclass(Tank.class);
我们关注一下这条语句,我们可以知道生成的动态代理类是被代理类的一个子类
cglib 代理就是在内存中构建一个子类对象,并对子类进行增强,从而实现对被代理类功能的扩展
具体一点,就是生成了一个被代理类的子类,那么就可以重写被代理类的方法,然后就能实现对被代理类功能的扩展
我们总结一下 cglib 代理和 JDK 代理的区别:
- JDK 代理的类必须实现接口
- cglib 代理的类无需实现接口,但是不能是 final 类,否则无法生成子类
三、总结
- 静态代理实现简单,只要代理类对被代理类进行包装即可。静态代理在编译时生成 class 字节码文件,可以直接使用,效率高。但是静态代理只能为一个被代理类服务,如果被代理类过多,就会产生很多代理类,从而导致代码冗余。其次不易维护,如果接口改变,被代理类和代理类都需要修改
- JDK 代理,只有被代理类需要实现接口,而代理类只需要实现 InvocationHandler 接口。但是 JDK 代理需要使用反射,比较消耗系统性能。
- cglib 代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题。但是 cglib 需要继承被代理类重写方法,因此被代理类不能被 final 修饰
我对代理模式的理解
代理模式跟我们生活中的中介公司很像,在没有中介的时候,我们租房子都是直接联系房东,房东要亲自带你看房,谈好租金后直接跟房东签合同。这样的话,每次有人要看房,房东就要亲自带他去看房,很麻烦,也很浪费时间和精力。对于求租者来说,找房子也很困难,要四处收集租房信息,浪费时间和精力。有了中介之后,就方便多了,房东跟中介签一份合同,把房子交给中介,中介代理房东出租。当有人要租房的时候,中介带着求租者去看房就好了,而且中介代理的肯定不只一个房东,这样中介就可以一个人带着求租者看好几套房,求租者也不用四处收集信息,有什么需求直接跟中介提就好了。有了中介之后,房东和求租者都省时省力
我们开发中的代理模式是通过给被代理类创建一个代理类,并由代理类控制对被代理类的引用,来达到增强源被代理对象的功能和简化访问方式的目的。代理类就相当于中介,被代理类就是房东,而想要使用被代理的方法就是求租者。生活中的中介不只是代理房东带求租者看房和签合同这么简单,中介还提供了增强服务,比如一次性可以带求助者看很多套房,而且如果求租者提出需求,中介可以为求租者筛选合适的房子。我们代理模式也是一样,代理类不只是代理的作用,还有扩展被代理类功能的作用,比如扩展日志功能和计时功能等等
代理模式通常分为静态代理和动态代理,动态代理又分为很多种,其中最常用的是 JDK 代理和 cglib 代理。静态代理之所以叫静态代理,是因为静态代理是在程序运行前就为被代理类提供代理对象,而动态代理是在运行时才为被代理类提供代理对象
要实现静态代理,我们要先定义一个接口,然后被代理类和代理类都要实现这个接口并重写方法,并且在代理类中聚合一个被代理类对象。这样框架就搭建好了,然后实现细节。细节就在代理类的重写方法里实现,我在这个方法里扩展我们自己的功能,再调用被代理类对象的重写方法,这样我们的扩展功能和被代理类的功能就在同一方法里了。最后在 main 方法里创建被代理类对象并且调用它的重写方法就可以了
但是静态代理存在一些问题,首先它要求被代理类和代理类都要实现相同接口,那要是被代理类没有接口就不行了。第二,静态代理只能为一个被代理类代理,这样当有多个被代理类的时候就要写多个代理类,会造成代码冗余。为了解决两个问题,我们可以使用动态代理。之前提到动态代理分为 JDK 代理和 cglib 代理,我们先来说一说 JDK 代理。JDK 代理之所叫 JDK 代理,是因为它使用了 JDK 里的一个叫 Reflect 的 API。它的实现原理比较复杂而且比较绕,因为它是在运行时创建代理对象的,并且我们不设置让它保存创建的代理对象它也是不会让我们看见的。它的实现过程也是先定义一个接口,然后让被代理类实现这个接口,第三步就不一样了,第三步是创建一个叫调用时处理器的类并且在类里聚合一个被代理对象,这个类需要继承一个调用时处理器的接口,重写这个接口的一个叫 invoke 的方法,我们在这个方法里写我们需要扩展的功能并调用被代理对象的原始功能,这样扩展功能和原始功能就在同一个方法里了,我们只需要调用就好了。第四步是写 main 方法,在 main 方法里使用一个叫 newProxyInstance 的方法,这个方法就是用于在运行时创建代理对象的。这个方法需要传入三个参数,第一个参数是类加载器,使用和被代理类一样的类加载器就可以了,第二个参数是一个接口数组,存放被代理对象实现的接口,第三个参数就是我们第三步创建的调用时处理器。然后调用这个代理对象的方法。我们不是给 newProxyInstance 传入了一个接口数组嘛,这个运行时创建的代理对象就是通过这个数组实现了和被代理对象一样的接口,并且重写了接口方法,因此这个方法和被代理对象的方法是同名的,然后就是在这个同名方法里调用的我们在第三步的 invoke() 方法
JDK 代理已经能满足我们的开发需求了,虽然说它需要使用反射机制,比较消耗系统性能,但还是可用的。只是他有一个和静态代理一样的局限,就是它的被代理类必须实现接口,要是被代理类没有实现接口的话就需要使用 cglib 代理了。cglib 是一个第三方包,cglib 代理无需实现接口,通过生成类字节码实现代理,比反射稍快。它首先要定义一个被代理类,不用实现任何接口。第二步定义一个拦截器,实现 MethodInterceptor 接口,并重写接口方法 intercept,把扩展功能写在这个方法里。第三步写 main 方法,在 main 方法里创建一个 enhancer 实例,并设置被代理类为它的父类,然后为 enhancer 设置回调函数,这个函数需要传入一个对象,也就是我们第二步定义的拦截器的一个实例。最后创建被代理类的子类,这个子类和 JDK 代理一样,是在运行时创建的,然后调用这个子类的方法就好了。cglib 代理的实现比较简单,原理也比较简单,前面说了那么多,其实简而言之就是创建了被代理对象的子类,然后子类重写父类的方法,在这个重写的方法写我们需要的功能,然后调用这个方法就好了,为的就是不修改父类的方法。所以 cglib 的局限很明显,被代理类不能被 final 修饰。因此在程序开发过程中,我们常常同时使用 JDK 代理和 cglib 代理,实现互补