设计模式-代理模式
什么是代理模式
在软件开发中,也有一种设计模式可以提供与代购网站类似的功能。由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称之为“代理”的第三者来实现间接访问,该方案对应的设计模式被称为代理模式。
代理模式是一种应用很广泛的结构型设计模式,而且变化形式非常多,常见的代理形式包括远程代理、保护代理、虚拟代理、缓冲代理、智能引用代理等,后面将学习这些不同的代理形式。
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
代理模式是一种对象结构型模式。在代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到中介的作用,它去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。
为什么要用代理模式
- 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
- 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
角色
- Subject(抽象主题角色): 它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
- Proxy(代理主题角色): 它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
- RealSubject(真实主题角色): 它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。
优缺点
优点
- 职责清晰。
- 高扩展性。
- 智能化。
缺点
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
适用场景
- 远程代理。
- 虚拟代理。
- Copy-on-Write 代理。
- 保护(Protect or Access)代理。
- Cache代理。
- 防火墙(Firewall)代理。
- 同步化(Synchronization)代理。
- 智能引用(Smart Reference)代理。
注意事项
- 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
- 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
静态代理
其实代理的一般模式就是静态代理的实现模式:首先创建一个接口(JDK代理都是面向接口的),然后创建具体实现类来实现这个接口,在创建一个代理类同样实现这个接口,不同之处在于,具体实现类的方法中需要将接口中定义的方法的业务逻辑功能实现,而代理类中的方法只要调用具体类中的对应方法即可,这样我们在需要使用接口中的某个方法的功能时直接调用代理类的方法即可,将具体的实现类隐藏在底层。
实现
公用接口
代理类和真实类的公用接口
public interface Subject {
void request();
}
真实类的请求
实现公有的接口
public class RealSubject implements Subject{
@Override
public void request() {
System.out.println("真实请求");
}
}
创建代理类
代理请求,引入了真实类对象,对方法进行了增强。
public class Proxy implements Subject{
//真实请求接口
private Subject realSubject;
@Override
public void request() {
if (realSubject == null) {
realSubject = new RealSubject();
}
System.out.println("前置增强");
//转发真实请求
realSubject.request();
System.out.println("后置增强");
}
}
测试
public class ProxyDemo {
public static void main(String[] args){
Proxy proxy = new Proxy();
proxy.request();
}
}
静态代理总结
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
动态代理
在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK再运行时为我们动态的来创建。
编写动态处理器
动态代理需要实现InvocationHandler接口
/**
* 动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。
* 该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类
*/
public class DynamicProxyHandler implements InvocationHandler {
// 目标对象
private Object targetObject;
/**
* 绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。
* @param targetObject
* @return
*/
public Object newProxyInstance(Object targetObject){
this.targetObject=targetObject;
//该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
//第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
//第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口
//第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法
//根据传入的目标返回一个代理对象
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(),this);
}
/**
* 关联的这个实现类的方法被调用时将被执行
* InvocationHandler接口的方法
* @param proxy 代理对象
* @param method 原对象被调用的方法
* @param args 表示方法的参数*
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强");
//调用目标方法
Object result = method.invoke(targetObject, args);
System.out.println("后置增强");
return result;
}
}
测试
public class DynamicProxyDemo {
public static void main(String[] args){
DynamicProxyHandler dynamicProxyHandler=new DynamicProxyHandler();
UserManager userManager=(UserManager)dynamicProxyHandler.newProxyInstance(new UserManagerImpl());
UserManager userManager=new UserManagerImpl();
userManager.addUser("1111", "张三");
}
}
动态代理总结
虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫Proxy。Java的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在Java中本质上就行不通。有很多条理由,人们可以否定对 class代理的必要性,但是同样有一些理由,相信支持class动态代理会更美好。接口和类的划分,本就不是很明显,只是到了Java中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。但是,不完美并不等于不伟大,伟大是一种本质,Java动态代理就是佐例。
CGLIB动态代理
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
创建CGLIB代理类
public class CglibProxy implements MethodInterceptor {
// 目标对象
private Object targetObject;
public Object getInstance(final Object target) {
this.target = target;
//CGLIB enhancer增强类对象
Enhancer enhancer = new Enhancer();
//设置代理类
enhancer.setSuperclass(this.targetObject.getClass());
//定义代理逻辑对象为当前对象 ,对象要继承MethodInterceptor。并实现逻辑方法intercept
enhancer.setCallback(this);
return enhancer.create();
}
/**
* 代理逻辑方法
* @param proxy 代理对象
* @param method 方法
* @param args 方法参数
* @param methodProxy 方法代理
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("前置增强");
//真的是代理对象,不像JDk代理,还是真实对象
Object result = methodProxy.invokeSuper(proxy, args);
System.out.println("后置增强");
return result;
}
}
这里用了CGLIB的加强者Enhancer,通过设置超类的方法setSuperclass,然后通过setCallback方法设置哪个类为它的代理类。其中,参数为this就意味着是当前对象,那就要求用this这个对象实现接口Methodlnterceptor的方法intercept,然后返回代理对象。那么此时当前类的intercept方法就是其代理逻辑方法,其参数内容见代码注解,我们在反射真实对象方法前后进行了打印,CGLIB是通过如下代码完成的。
Object result=methodProxy.invokeSuper(proxy,args);
测试
public class CglibProxyDemo {
public static void main(String arg[]) {
CglibProxy cglib = new CglibProxy();
Dog dog = (Dog) cglib.getInstance(Dog.class);
//调用代理对象的isAnimal()方法
dog.isAnimal();
}
}
CGLIB代理总结
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
两者对比
使用对比
- JDK动态代理是面向接口的。
- CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败)。
性能对比
传言的 cglib 比 jdk动态代理高出 10 倍
环境
JDK 1.8
win10 i7
结果
我们经过测试并不是这样,或者说以前是这样,大家可以测试下,我把结果贴出来
JDKProxy创建耗时:1
JDKProxy调用耗时:2
CjLibProxy创建耗时:110
CjLibProxy调用耗时:21
创建耗时JDK比CGLIB快了将近100倍
调用也比CGLIB快了将近十倍
所以使用代理的时候最好使用面向接口的JDK的动态代理。