定义
代理模式的核心定义为:为其他对象提供一种代理以控制这个对象的访问。当一个对象由于各种原因,比如对象创建开销巨大、访问权限限制或者需要在访问前后添加额外逻辑等,不适合或不能直接被引用时,代理对象便挺身而出,在客户端和目标对象之间承担起中介的角色。例如,在网络编程中,当客户端需要访问远程服务器上的资源时,由于网络延迟、安全性等因素,直接访问可能不太现实,这时就可以通过代理服务器来进行间接访问。代理服务器作为代理对象,接收客户端的请求,然后将请求转发给远程服务器,并将服务器的响应返回给客户端,有效地控制了客户端对远程资源的访问。
在面对复杂对象需要多份副本存在的情况时,代理模式还能与享元模式相结合,从而减少存储器用量。典型做法是创建一个复杂对象以及多个代理对象,每个代理对象都引用到原本的复杂对象。对代理对象的运算操作会传递到原本的复杂对象上执行。当所有代理对象都不再被使用时,与之关联的复杂对象也会被移除,以此达到节省内存的目的。
作用
保护目标对象:代理模式可以对目标对象的访问进行限制和控制。例如,在一个系统中,某些敏感数据的访问权限只授予特定的用户或模块。通过代理对象,可以在客户端请求访问目标对象时,对客户端的身份、权限等进行验证。只有当客户端满足特定的权限条件时,代理对象才会将请求转发给目标对象,从而保护了目标对象不被非法访问。
增强目标对象:代理对象不仅可以控制对目标对象的访问,还可以在目标对象的方法调用前后添加额外的逻辑,从而增强目标对象的功能。比如,在日志记录场景中,代理对象可以在调用目标对象的方法之前记录方法调用的相关信息,如调用时间、参数等;在方法调用之后记录方法的返回值和执行时间。这样,通过代理对象的增强,无需修改目标对象的代码,就为其添加了日志记录功能。又比如在缓存场景中,代理对象可以先检查缓存中是否存在目标对象的计算结果,如果存在则直接返回缓存中的数据,避免了重复计算,提高了系统的性能。
类图
角色
抽象角色(Subject):通过接口或抽象类声明真实角色实现的业务方法。它定义了代理对象和真实对象共同的接口,使得客户端可以统一地使用代理对象和真实对象,而无需关心它们具体的实现类。例如,在一个图形绘制系统中,可能定义一个抽象的Shape接口,其中声明了draw方法。无论是真实的图形对象(如Circle、Rectangle)还是代理图形对象,都需要实现这个Shape接口。
代理角色(Proxy):实现抽象角色,是真实角色的代理。它内部持有对真实角色的引用,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。在客户端调用代理对象的方法时,代理对象会先执行自己的附加操作(如权限验证、日志记录等),然后再调用真实角色的相应方法,并在方法调用后可能执行一些后续处理。例如,在一个文件访问系统中,代理对象可以在访问真实文件对象之前,检查当前用户是否具有访问该文件的权限;在访问之后记录文件访问的日志信息。
真实角色(Real Subject):实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。它是实际执行核心业务功能的对象。比如在上述图形绘制系统中,Circle类和Rectangle类就是真实角色,它们分别实现了Shape接口中的draw方法,完成具体的图形绘制逻辑。
优缺点
优点
职责清晰:真实角色专注于实现实际的业务逻辑,无需关心与业务无关的其他事务,如访问控制、日志记录等。这些额外的功能由代理角色来承担,通过后期的代理完成整个事务,使得代码结构更加简洁清晰,各个角色的职责明确。例如,在一个电商系统中,商品管理模块的真实角色负责处理商品的添加、修改、删除等核心业务逻辑,而代理角色可以负责对用户的操作权限进行验证,确保只有管理员用户能够进行敏感操作,两者职责分离,互不干扰。
高拓展性:当需要对系统的功能进行扩展时,只需要修改代理类即可,而无需修改真实角色的代码。这符合软件设计中的开闭原则,即对扩展开放,对修改关闭。例如,如果要为一个系统添加缓存功能,只需要在代理类中添加缓存相关的逻辑,而真实角色的业务逻辑代码保持不变。这样,即使真实角色的代码已经非常复杂,或者在多个地方被引用,也不会因为添加新功能而受到影响,大大提高了系统的可扩展性。
智能化:代理对象可以在客户端和目标对象之间起到中介作用,根据不同的情况进行智能处理。例如,在网络访问中,代理对象可以根据网络状况、服务器负载等因素,选择最优的服务器进行请求转发,或者对请求进行缓存、压缩等处理,提高系统的性能和响应速度。同时,代理对象还可以对客户端的请求进行预处理和过滤,确保只有合法、有效的请求才能到达目标对象,增强了系统的安全性和稳定性。
降低耦合度:代理模式将客户端与目标对象分离,客户端只与代理对象交互,而不需要直接依赖目标对象。这样在一定程度上降低了系统的耦合度,使得系统的各个部分可以相对独立地进行开发、测试和维护。当目标对象的实现发生变化时,只需要保证代理对象的接口不变,客户端的代码就无需修改,提高了系统的灵活性和可维护性。
缺点
请求处理速度变慢:由于在客户端和真实主题之间增加了代理对象,请求需要经过代理对象的转发和处理,这可能会导致请求的处理速度变慢。尤其是在一些对性能要求极高的场景下,代理对象的额外处理开销可能会对系统的整体性能产生较大影响。例如,在一个实时性要求很高的游戏服务器中,如果使用代理模式进行数据转发和处理,可能会因为代理对象的处理延迟而导致游戏画面卡顿,影响玩家的游戏体验。
实现复杂:实现代理模式需要额外的工作,尤其是在一些复杂的代理场景下,代理类的实现可能会非常复杂。例如,在动态代理中,需要使用反射机制来生成代理对象,并且要处理好代理对象与真实对象之间的方法调用关系,这对于开发者来说需要具备较高的技术水平和编程经验。此外,代理模式还可能涉及到多个类之间的协作和交互,增加了代码的复杂度和调试难度。
使用案例
引用计数指针对象:在一些资源管理系统中,使用引用计数指针对象来管理资源的生命周期。当一个对象被多个地方引用时,通过引用计数来记录对象被引用的次数。代理对象可以控制对实际资源对象的访问,当引用计数为 0 时,代理对象可以释放实际资源对象,从而实现资源的有效管理。例如,在一个图像编辑软件中,图像数据可能是一个占用大量内存的资源。通过引用计数指针对象,当多个图像编辑工具同时引用同一图像数据时,只有当所有引用都被释放,即引用计数为 0 时,才真正释放图像数据所占用的内存。
Windows 里面的快捷方式:Windows 系统中的快捷方式就是代理模式的一个典型应用。快捷方式作为代理对象,指向实际的文件或应用程序(真实对象)。用户通过双击快捷方式来启动对应的程序或打开文件,而无需直接找到实际文件所在的位置。快捷方式可以方便地创建、删除和移动,并且可以在不改变实际文件位置和内容的情况下,为用户提供一种便捷的访问方式。同时,快捷方式还可以在启动程序或打开文件之前,执行一些额外的操作,如检查文件的权限、更新软件版本等。
Spring AOP:Spring 框架中的面向切面编程(AOP)是动态代理的一个重要应用。AOP 通过代理机制,在不修改目标对象代码的情况下,为目标对象添加横切关注点,如日志记录、事务管理、权限验证等。在 Spring AOP 中,通过配置或注解的方式定义切面(Aspect),切面中包含了在目标对象方法调用前后需要执行的增强逻辑。Spring 会在运行时动态生成代理对象,将切面的增强逻辑织入到目标对象的方法调用中。例如,在一个企业级应用中,通过 Spring AOP 可以很方便地为所有的业务方法添加事务管理功能,确保数据的一致性和完整性,而无需在每个业务方法中手动编写事务代码。
动态代理实现方式
基于 JDK 的动态代理
首先创建代理类构造器,它是InvocationHandler接口的实现类。在这个实现类中,需要重写接口中的invoke方法。在invoke方法中,通过method.invoke(被代理的对象,参数)来调用需要执行的方法。同时,我们可以在此方法中对原方法的功能进行增强。例如,在方法调用前输出日志信息,在方法调用后进行性能统计等。
可以将代理类创建方法Proxy.newProxyInstance写在代理类构造器中,封装为getProxy方法,该方法返回Object类型的代理对象。在使用时,需要将返回的对象强转为具体的接口类型。Proxy.newProxyInstance方法的参数包括:被代理对象的类加载器(target.getClass().getClassLoader())、接口(可以直接写接口.class,或者通过实现类.class.getInterfaces () 获取)以及代理类构造器(this)。以下是一个简单的示例代码:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyInvocationHandler implements InvocationHandler {
/**
* 被代理接口
*/
private Object target;
public ProxyInvocationHandler(Object target) {
this.target = target;
}
public Object getProxy() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// todo 这部分可以处理代理对象的逻辑
System.out.println("代理对象调用之前");
Object res = method.invoke(target, args);
System.out.println("代理对象调用之后");
return res;
}
}
在main方法中使用动态代理:
public static void main(String[] args) {
// 被代理对象
MapReal mapReal = new MapReal();
// 代理对象
ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler(mapReal);
// 创建代理对象
MapInterface mapproxy = (MapInterface) proxyInvocationHandler.getProxy();
// 调用代理对象的方法
mapproxy.display();
}
基于 CGLIB 实现的动态代理
CGLIB 是一个强大的高性能的代码生成库,它可以在运行时动态生成一个类的子类,从而实现动态代理。在基于 CGLIB 的动态代理中,需要创建一个实现了MethodInterceptor接口的类。在这个类中,重写intercept方法。在intercept方法中,可以在调用目标对象的方法前后添加自定义的逻辑。例如:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibTest implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib代理类 调用前");
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("cglib代理类 调用后");
return result;
}
public Object getProxy(Class cla) {
return net.sf.cglib.proxy.Enhancer.create(cla, this);
}
}
在main方法中使用 CGLIB 动态代理:
public static void main(String[] args) {
CglibTest cglibTest = new CglibTest();
MapReal mapReal = (MapReal) cglibTest.getProxy(MapReal.class);
mapReal.display();
}
与 JDK 动态代理不同的是,CGLIB 动态代理不需要目标对象实现接口,它通过生成目标对象的子类来实现代理功能。这使得 CGLIB 在一些无法使用接口的场景下(如对一个没有实现接口的类进行代理)具有很大的优势。但是,由于 CGLIB 采用了继承机制,所以不能对被final修饰的类进行代理。同时,CGLIB 的动态代理性能通常比 JDK 动态代理要高一些,因为它采用了 FastClass 机制,直接调用目标对象的方法,而不是通过反射机制。