一文搞懂设计模式—代理模式

2024年 2月 26日 139.5k 0

代理模式(Proxy Pattern)是一种结构型设计模式,也叫做委托模式,它允许你提供一个间接访问对象的方式。

用一句话描述代理模式就是:为其他对象提供一种代理以控制对这个对象的访问

使用场景

  • 远程代理(Remote Proxy):用于在不同地址空间中代表对象,使得客户端可以访问远程的对象。
  • 虚拟代理(Virtual Proxy):用于按需创建昂贵对象的代表,延迟对象的实例化,提高系统性能。
  • 保护代理(Protection Proxy):用于控制对真实对象的访问权限,在访问真实对象之前进行安全检查。
  • 智能引用(Smart Reference):用于在访问对象时执行额外的操作,如引用计数、懒加载等。
  • 日志记录(Logging Proxy):用于记录方法调用的日志信息,方便调试和监控系统运行状态。
  • 权限控制(Access Control Proxy):用于控制用户对对象的访问权限,限制某些用户的操作。
  • 延迟加载(Lazy Loading Proxy):用于延迟加载对象的数据,直到真正需要使用时才进行加载。

代理模式在Java中的Spring框架和Dubbo框架中都有广泛的应用:

  • Spring框架中的AOP(面向切面编程):Spring使用代理模式实现AOP功能,允许开发者定义切面(Aspect),并通过代理机制将切面织入到目标对象的方法调用中,实现横切关注点的管理,如日志记录、事务管理等。
  • Dubbo框架中的远程服务代理:Dubbo是一种高性能的分布式服务框架,其中的服务消费者与服务提供者之间的通信通过代理模式来实现。Dubbo会根据配置信息动态生成接口的代理实现类,在远程调用时通过代理对象进行通信,隐藏了远程调用的复杂性,使得调用方可以像调用本地方法一样调用远程服务。

通过代理模式,可以实现对对象的访问控制、附加功能增强、性能优化等目的,提高系统的灵活性、可维护性和可扩展性。

具体实现

代理模式涉及以下几个角色:

  • 抽象主题(Subject):是一个接口或抽象类,定义了真实主题和代理对象共同实现的方法,客户端通过抽象主题访问真实主题。
  • 真实主题(Real Subject):是真正执行业务逻辑的对象,实现了抽象主题定义的方法,是代理模式中被代理的对象。
  • 代理(Proxy):持有对真实主题的引用,可以控制对真实主题的访问,在其自身的方法中可以调用真实主题的方法,同时也可以在调用前后执行一些附加操作。

实现代理模式步骤如下:

首先定义一个接口:

public interface Subject {
    void request();
}

然后实现真实主题类:

public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("Real Subject handles the request.");
    }
}

接着创建代理类:

public class Proxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        preRequest();
        realSubject.request();
        postRequest();
    }
    //前置处理
    private void preRequest() {
        System.out.println("Proxy performs pre-request actions.");
    }
    //后置处理
    private void postRequest() {
        System.out.println("Proxy performs post-request actions.");
    }
}

客户端调用:

public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.request();
    }

输出:

Proxy performs pre-request actions.
Real Subject handles the request.
Proxy performs post-request actions.

Tips:一个代理类,可以代理多个真实角色,并且真实角色之间允许有耦合关系。

普通代理 & 强制代理

在代理模式中,可以区分普通代理和强制代理:

  • 普通代理(Normal Proxy):由代理类控制对真实主题的访问,客户端直接与代理类交互,代理类负责将请求转发给真实主题,调用者只知代理而不用知道真实的角色是谁,屏蔽了真实角色的变更对高层模块的影响。
  • 强制代理(Force Proxy):“强制”必须通过真实角色查找到代理角色,否则不能访问。并且只有通过真实角色指定的代理类才可以访问,也就是说由真实角色管理代理角色。强制代理不需要产生一个代理出来,代理的管理由真实角色自己完成。

上面提供的代码例子就是普通代理,下面用代码演示下强制代理:

// 抽象主题接口
public interface Subject {
    /**
     * 待具体实现的方法
     */
    void request();

    /**
     * 获取每个具体实现对应的代理对象实例
     * @return 返回对应的代理对象
     */
    Subject getProxy();
}


// 强制代理对象
public class ForceProxy implements Subject {

    private Subject subject;

    public ForceProxy(Subject subject) {
        this.subject = subject;
    }

    /**
     * 待具体实现的方法
     */
    @Override
    public void request() {
        preRequest();
        subject.request();
        postRequest();
    }

    /**
     * @return 返回对应的代理对象就是自己
     */
    @Override
    public Subject getProxy() {
        return this;
    }

    private void postRequest() {
        System.out.println("访问真实主题以后的后续处理");
    }

    private void preRequest() {
        System.out.println("访问真实主题之前的预处理");
    }
}


// 具体的实现对象
public class RealSubject implements Subject {

    /**
     * 该具体实现对象的代理对象
     */
    private Subject proxy;

    @Override
    public Subject getProxy() {
        proxy = new ForceProxy(this);
        return proxy;
    }

    /**
     * 待具体实现的方法
     */
    @Override
    public void request() {
        if (isProxy()) {
            System.out.println("访问真实主题方法");
        } else {
            System.out.println("请使用指定的代理访问");
        }
    }

    private boolean isProxy() {
        return proxy != null;
    }
}

客户端调用:

public static void main(String[] args) {
        Subject subject = new RealSubject();
        subject.request();
    }
    Output:
    请使用指定的代理访问
      
      
      
    public static void main(String[] args) {
        Subject subject = new RealSubject();
        Subject proxy = new ForceProxy(subject);
        proxy.request();
    }
    Output:
    访问真实主题之前的预处理
    请使用指定的代理访问
    访问真实主题以后的后续处理
      
      
      
  public static void main(String[] args) {
        Subject subject = new RealSubject();
        Subject proxy = subject.getProxy();
        proxy.request();
    }
    Output:
    访问真实主题之前的预处理
    访问真实主题方法
    访问真实主题以后的后续处理

通过代码可以观察到,强制代理模式下,不允许通过真实角色来直接访问,只有通过真实角色来获取代理对象,才能访问。

高层模块只需调用getProxy就可以访问真实角色的所有方法,它根本就不需要产生一个代理出来,代理的管理已经由真实角色自己完成。

动态代理

前面讲的普通代理和强制代理都属于静态代理,也就是说自己写代理类的方式就是静态代理。

静态代理有一个缺点就是要在实现阶段就要指定代理类以及被代理者,很不灵活。

而动态代理是一种在运行时动态生成代理类的机制,可以在不预先知道接口的情况下动态创建接口的实现类,允许在运行阶段才指定代理哪一个对象,比如Spring AOP就是非常经典的动态代理的应用

下面是两个动态代理常用的实现方式:

  • JDK 动态代理 :基于 Java 反射机制,在运行时动态创建代理类和对象。JDK 动态代理要求被代理的类实现一个或多个接口,通过 java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 接口来实现代理对象的生成和方法调用。
  • CGLIB 动态代理:不要求被代理的类实现接口,通过继承被代理类来生成代理对象。CGLIB 使用字节码生成库ASM来动态生成代理类,因此性能略高于 JDK 动态代理。

JDK动态代理

JDK实现动态代理的核心机制就是java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

JDK动态代理的动态代理类需要去实现JDK自带的java.lang.reflect.InvocationHandler接口,该接口中的invoke()方法能够让动态代理类实例在运行时调用被代理类需要对外实现的所有接口中的方法,也就是完成对真实主题类方法的调用。

具体实现步骤如下:

  • 创建一个接口Subject表示被代理的对象需要实现的方法。
  • 创建一个真实主题类RealSubject,实现Subject接口,定义真正的业务逻辑。
  • 创建一个实现InvocationHandler接口的代理处理器类DynamicProxyHandler,在invoke方法中执行额外的操作,并调用真实主题的方法。
  • 在主程序中使用Proxy.newProxyInstance()方法动态生成代理对象,并调用代理对象的方法。
  • 下面是动态代理的示例代码,一起来感受一下:

    // 1. 创建接口
    public interface Subject {
        void request();
    }
    
    // 2. 创建真实主题类
    public class RealSubject implements Subject {
        @Override
        public void request() {
            System.out.println("RealSubject handles the request.");
        }
    }
    
    // 3. 创建代理处理器类
    public class DynamicProxyHandler implements InvocationHandler {
        private Object target;
    
        DynamicProxyHandler(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 执行额外操作
            System.out.println("Before requesting...");
            
            // 调用真实主题对象的方法
            Object result = method.invoke(target, args);
            
            // 执行额外操作
            System.out.println("After requesting...");
            
            return result;
        }
    }
    
    public class DynamicProxyExample {
    
        public static void main(String[] args) {
            // 创建真实主题对象
            Subject realSubject = new RealSubject();
    
            // 创建代理处理器对象
            InvocationHandler handler = new DynamicProxyHandler(realSubject);
    
            // 创建动态代理对象
            Subject proxy = (Subject) Proxy.newProxyInstance(
                    realSubject.getClass().getClassLoader(),
                    realSubject.getClass().getInterfaces(),
                    handler);
    
            // 调用代理对象的方法
            proxy.request();
        }
    }

    这段代码演示了使用 JDK 动态代理实现动态代理的过程:

  • 首先,创建了一个真实主题对象 realSubject,表示被代理的真实对象。
  • 接着,创建了一个代理处理器对象 handler,类型为 InvocationHandler,并将真实主题对象传入代理处理器中,用于处理代理对象的方法调用。
  • 然后,通过 Proxy.newProxyInstance() 方法创建了一个动态代理对象 proxy,该方法接受三个参数:
    • 类加载器:使用真实主题对象的类加载器。
    • 接口数组:指定代理对象需要实现的接口,这里使用真实主题对象的接口数组。
    • 处理器:指定代理对象的调用处理器,即前面创建的代理处理器对象 handler。
  • 最后,通过代理对象 proxy 调用 request() 方法,实际上会委托给代理处理器 handler 的 invoke() 方法来处理方法调用,进而调用真实主题对象的对应方法。
  • 这段代码通过 JDK 动态代理机制实现了代理对象的动态创建和方法调用处理,实现了对真实主题对象的间接访问,并在调用真实主题对象方法前后进行了额外的处理。

    其动态调用过程如图所示:

    cglib动态代理

    JDK的动态代理机制只能代理实现了接口的类,否则不能实现JDK的动态代理,具有一定的局限性。

    CGLIB(Code Generation Library)是一个功能强大的字节码生成库,可以用来在运行时扩展Java类和实现动态代理。

    相对于JDK动态代理基于接口的代理,cglib动态代理基于子类的代理,可以代理那些没有接口的类,通俗说cglib可以在运行时动态生成字节码。

    cglib的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,因为采用的是继承,所以不能对final修饰符的类进行代理。

    下面是一个使用cglib实现动态代理的示例代码,包括实现步骤:

  • 创建一个真实主题类RealSubject,无需实现任何接口。
  • 创建一个实现MethodInterceptor接口的代理处理器类DynamicProxyHandler,在intercept方法中执行额外的操作,并调用真实主题的方法。
  • 在主程序中使用Enhancer类创建代理对象,并设置代理处理器。
  • 使用 cglib 需要添加对应的依赖:

    
    
        cglib
        cglib
        3.3.0
    
    // 1. 创建真实主题类
    public class RealSubject {
        public void request() {
            System.out.println("RealSubject handles the request.");
        }
    }
    
    // 2. 创建代理处理器类
    public class DynamicProxyHandler implements MethodInterceptor {
    
        /**
         * 通过Enhancer 创建代理对象
         */
        private Enhancer enhancer = new Enhancer();
    
        /**
         * 通过class对象获取代理对象
         * @param clazz class对象
         * @return 代理对象
         */
        public Object getProxy(Class clazz) {
            // 设置需要代理的类
            enhancer.setSuperclass(clazz);
            // 设置enhancer的回调
            enhancer.setCallback(this);
            return enhancer.create();
        }
      
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            // 执行额外操作
            System.out.println("Before requesting...");
    
            // 调用真实主题对象的方法
            Object result = proxy.invokeSuper(obj, args);
    
            // 执行额外操作
            System.out.println("After requesting...");
    
            return result;
        }
    }
    
    public class CglibProxyExample {
        public static void main(String[] args) {
            DynamicProxyHandler proxy = new DynamicProxyHandler();
            RealSubject realSubject = (RealSubject) proxy.getProxy(RealSubject.class);
    
            // 调用代理对象的方法
            realSubject.request();
        }
    }

    输出:

    Before requesting...
    RealSubject handles the request.
    After requesting...

    cglib动态代理相比于JDK动态代理的优缺点如下:

    优点:

    • 可以代理没有实现接口的类。
    • 性能更高,因为直接操作字节码,无需反射。

    缺点:

    • 生成的代理类会继承被代理类,可能会影响某些设计。
    • 无法代理static方法,因为cglib是基于继承来生成代理类的,而静态方法是属于类而非对象的
    • 对于final方法,cglib无法覆盖,仍然会调用父类方法。

    总结

    代理模式是一种常用的设计模式,在软件开发中有着广泛的应用。通过引入代理对象,可以实现对真实对象的访问控制、附加功能增强、性能优化等目的。

    优点

    • 可以控制对真实对象的访问,在不改变原始类代码的情况下扩展其行为。
    • 代理模式能将客户端与目标对象分离,在一定程序上降低了系统的耦合度.

    缺点

    • 增加了系统复杂性,引入了多余的代理类,因此有些类型的代理模式可能会造成请求的处理速度变慢。
    • 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

    总的来说,代理模式通过引入代理对象,实现了对真实对象的间接访问和控制,为系统的设计提供了一种简洁而有效的解决方案。在日常的软件开发中,合理地运用代理模式可以为系统带来更好的结构和性能表现。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论