开发者社区 > 博文 > 设计模式之代理模式:武器附魔之道
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

设计模式之代理模式:武器附魔之道

  • jd****
  • 2024-12-25
  • IP归属:北京
  • 2浏览

       大家好,今天我们聊聊设计模式中的代理模式。作为一种经典设计模式,它的应用极为广泛。不论你是刚刚入门,还是已经熟悉设计模式,相信这篇文章都会让你有所收获。

    一、引子:叫个代驾

       让我们从一个引子开始:司机和代驾。「私家车司机」和「代驾」是什么关系?很简单,「私家车司机」是客户,「代驾」负责提供服务,帮他们开车。

       不同点:「私家车司机」有自己的车,他们可能自己开车,也可能找代驾开车;

                   「代驾」没有自己的车,但他们会接到代驾订单,从而开「私家车司机」的车。

       相同点:他们都会开车,有驾照。换句话说,他们都属于「司机」。

       说到这里,我想他们的关系就很清楚了:「司机」是「私家车司机」和「代驾」的父类。虽然都会开车,但他们对“开车”这个行为有不同的实现。

       让我们把这三个类的关系表示出来。首先定义一个抽象类「司机」,就叫Driver好了:

    @Data
    public abstract class Driver {
    
        String name;
    
        abstract void driveCar();
    }

       我们要求每个司机都有一个名字,且都必须会开车。

       接下来看看「私家车司机」,CommonDriver类:

    @Data
    public class CommonDriver extends Driver {
    
        CommonDriver(String name) {
            this.name = name;
        }
    
        @Override
        public void driveCar() {
            System.out.println(this.getName() + "的汽车正在行驶...");
        }
    }

       也很简单,每次开车的时候打印一行日志即可。最后看看「代驾」,就叫ProxyDriver吧:

    @Data
    public class ProxyDriver extends Driver {
    
        private Driver realDriver;
    
        ProxyDriver(String name, Driver realDriver) {
            this.name = name;
            this.realDriver = realDriver;
        }
    
        @Override
        void driveCar() {
            System.out.printf("代驾「%s」正在为%s服务...\n", this.getName(), this.getRealDriver().getName());
            this.realDriver.driveCar();
        }
    }

       我们要求每个代驾都要有一个服务的客户,也就是被代理的司机。我们将这位被代理的司机——realDriver作为了代驾类的私有变量存起来。当代驾在开车时,他实际上开的是客户的车。因此,他直接去调用realDriver的开车方法即可。

       三个类定义好了,让我们先创建一个「私家车司机」——小张,让小张自己开车;再帮他叫一个「代驾」——就叫他小代吧,让小代帮他开车:

    public class Main {
        public static void main(String[] args) {
            CommonDriver zhang = new CommonDriver("小张");
            zhang.driveCar();
            ProxyDriver proxyDriver = new ProxyDriver("小代", zhang);
            proxyDriver.driveCar();
        }
    }

       运行一下:

    小张的汽车正在行驶...
    代驾小代正在为小张服务...
    小张的汽车正在行驶...

       结果符合预期:不管是谁在开车,结果都是一样的,开的都是小张的汽车。


    二、代理模式:武器附魔之道

    代理模式的定义

       以上例子展示了一个代理模式的基本实现。代理模式(Proxy Pattern)的定义是:使用代理以代替对真实对象的访问。它属于一种结构型设计模式。

       例子中的「司机」、「私家车司机」和「代驾」三个角色,分别对应了代理模式中的三个基本元素:

       「私家车司机」——真实主题:被代理的角色,是业务逻辑的具体执行者。

       「代驾」——代理主题:负责代理真实主题,所有对其业务方法的调用,都会被委托给其真实主题实现。

       「司机」——抽象主题:可以是接口,也可以是抽象类。代理主题和真实主题都会去实现/继承同一个抽象主题。

       下面是代理模式的类图:

    优点及应用

       为真实的对象设置一个代理,可以带来什么好处?在哪些应用场景下,我们需要用到代理模式?

       要回答这个问题,我们不妨想想代理的特点:间接访问。没错,代理模式的优点就在于通过代理间接访问真实对象。通过间接访问,我们就可以让代理做许多中间操作,通过这些中间操作,我们就可以在不修改真实对象的前提下,实现功能增强。

       我的理解:如果把真实主题比做一把「宝剑」,专门用来处理核心逻辑,那么就可以将它的代理比作「附魔」,用来给真实主题提供一些强化功能,附魔的种类就很多啦:火焰🔥、冰霜❄️、毒素🧪、雷电⚡️等等...能造成各种属性伤害,它们的共同点都是增强这把宝剑,但不会侵入式地去修改宝剑本身。

       那么一般可以选择哪些“附魔”呢?一些常见的应用场景:

       (1)「日志代理」:让代理帮忙记录方法出入参、调用记录等等日志;

       (2)「保护代理」:让代理帮忙做权限控制,拦截异常访问,保护真实对象;

       (3)「缓存代理」:让代理帮忙缓存真实对象的调用结果,从而减少对真实对象的调用量;

       (4)「虚拟代理」:延迟真实对象的初始化直到真正需要时,从而提高应用的启动速度和运行效率,多用于创建一个对象需要消耗大量资源时,也被称作“懒加载”。


       说到这里,我想大家就可以感到代理模式的强大了,代理模式的优点:

       (1)职责清晰:前面提到,作为真实主题的一种“增强”,代理与真实主题的职责划分十分清晰,这有利于维持真实主题的简洁,让真实主题专注于处理核心逻辑。

       (2)高扩展性:正因为职责清晰,代理与真实主题是松耦合的,对任何一方的修改都不会对另一方造成影响,适合业务逻辑需要经常扩展的场景。


    三、强制代理

       让我们回到代驾的例子,话题又回到小张身上:这天小张喝醉了。“道路千万条,安全第一条”,看来今天这车必须要让代驾开了。如何在方法中限制一下,规定只允许代驾开车呢?这就需要用到强制代理(Forced Proxy)。

       强制代理的定义是:对真实对象的访问,必须通过特定的代理对象进行。这句话包含了两层含义:1. 不允许对真实对象的直接访问。 2.必须通过特定的代理访问真实对象。

    《设计模式之禅》勘误

       如何实现强制代理?简单来说,在真实对象方法的调用中,增加对“是否使用了代理访问”的判断即可。在设计模式经典著作《设计模式之禅》中,作者给出了一种实现方式(但我认为存在一些问题),让我们先看看用他的方式如何实现吧~

       只需要对真实主题进行修改。在CommonDriver类,我们存储一个proxyDriver变量——用于记录这个司机叫的代驾,就如同代驾记录他代理的司机一样。接下来写一个 “叫代驾” 的方法——callProxy,用来给司机生成一个代驾对象。最后在实际开车的逻辑中,判断自己是否存在代驾对象。如果不存在,则认为司机没有叫代驾。具体实现如下:

    @Data
    public class CommonDriver extends Driver {
    
        private ProxyDriver proxyDriver = null;
    
        CommonDriver(String name) {
            this.name = name;
        }
    
        public ProxyDriver callProxy(String proxyName) {
            System.out.printf("%s叫了个代驾:%s\n", this.getName(), proxyName);
            this.proxyDriver = new ProxyDriver(proxyName, this);
            return this.proxyDriver;
        }
    
        @Override
        void driveCar() {
            if (this.isProxy()) {
                System.out.println(this.getName() + "的汽车正在行驶...");
            } else {
                System.out.println("酒后不开车,请叫代驾!");
            }
        }
    
        /**
         * 校验是否是代理访问
         */
        private boolean isProxy() {
            return this.proxyDriver != null;
        }
    }

       让我们试一试:先让小张开车,再给他叫一个代驾小代,让小代帮他开车:

    public class Main {
        public static void main(String[] args) {
            CommonDriver zhang = new CommonDriver("小张");
            zhang.driveCar();
            ProxyDriver proxy = zhang.callProxy("小代");
            proxy.driveCar();
        }
    }

       运行一下,符合预期,即满足了“不允许对真实对象的直接访问”:

    酒后不开车,请叫代驾!
    小张叫了个代驾:小代
    代驾小代正在为小张服务...
    小张的汽车正在行驶...

       强制代理的第二个要求:必须通过特定的代理访问真实对象。这次我们自己new一个假冒的代驾,让他去给小张做代驾:

    public class Main {
        public static void main(String[] args) {
            CommonDriver zhang = new CommonDriver("小张");
            ProxyDriver proxy = new ProxyDriver("假冒的代驾", zhang);
            proxy.driveCar();
        }
    }

       运行结果也符合预期:

    代驾「假冒的代驾」正在为小张服务...
    酒后不开车,请叫代驾!

       上述实现,看似符合强制代理的要求,但真的如此吗?眼尖的读者应该已经发现不对劲了。略微修改上面的用例,就可以证明这种实现存在缺陷:

    public class Main {
        public static void main(String[] args) {
            CommonDriver zhang = new CommonDriver("小张");
            ProxyDriver proxy1 = zhang.callProxy("小代");
            ProxyDriver proxy2 = new ProxyDriver("假冒的代驾", zhang);
            proxy2.driveCar();
        }
    }

       运行如下:

    小张叫了个代驾:小代
    代驾假冒的代驾正在为小张服务...
    小张的汽车正在行驶...    

       如上,给小张先叫一个代驾小代,但我们又自己new了一个假冒的代驾,让他去给小张开车。结果车真的被假代驾开走了!这说明该实现并没有满足“必须通过特定的代理访问真实对象”的要求。

       接下来,我们再试着让小张叫一个代驾,结果小张等不及代驾来,自己先上车了:

    public class Main {
        public static void main(String[] args) {
            CommonDriver zhang = new CommonDriver("小张");
            ProxyDriver proxy = zhang.callProxy("小代");
            zhang.driveCar();
        }
    }

       运行如下:

    小张叫了个代驾:小代
    小张的汽车正在行驶...

       可见,这种实现也无法满足“不允许对真实对象的直接访问”的要求。因此,这种强制代理的实现是有严重缺陷的。究其根本原因,就在于在上述实现中,仅通过该真实对象是否拥有代理对象来判断是否可以访问,而并未检查实际调用者的身份。因此只要事先通过真实对象创建了代理对象,以后就可以任意调用了。

    改进后的实现

       明确了问题所在,修改起来就容易了:只需在真实对象执行前,判断调用者是否是指定的调用者即可。同时,我们需要在调用方法时传入调用者。实现如下:

       Driver:

    @Data
    public abstract class Driver {
    
        String name;
        
        abstract void driveCar(Driver driver);
    }

       CommonDriver:

    @Data
    public class CommonDriver extends Driver {
    
        private ProxyDriver proxyDriver = null;
    
        CommonDriver(String name) {
            this.name = name;
        }
    
        public ProxyDriver callProxy(String proxyName) {
            System.out.printf("%s叫了个代驾:%s\n", this.getName(), proxyName);
            this.proxyDriver = new ProxyDriver(proxyName, this);
            return this.proxyDriver;
        }
    
        @Override
        void driveCar(Driver driver) {
            if (this.isProxy(driver)) {
                System.out.println(this.getName() + "的汽车正在行驶...");
            } else {
                System.out.println("酒后不开车,请叫代驾!");
            }
        }
    
        /**
         * 校验是否是代理访问
         */
        private boolean isProxy(Driver driver) {
            return this.proxyDriver == driver;
        }
    }

       ProxyDriver:

    @Data
    public class ProxyDriver extends Driver {
    
        private Driver realDriver;
    
        ProxyDriver(String name, Driver realDriver) {
            this.name = name;
            this.realDriver = realDriver;
        }
    
        @Override
        void driveCar(Driver driver) {
            System.out.printf("代驾「%s」正在为%s服务...\n", this.getName(), this.getRealDriver().getName());
            this.realDriver.driveCar(this);
        }
    }

           运行刚才的用例,这次不论是假冒的代驾还是小张自己,都无法正常访问了:

    public class Main {
        public static void main(String[] args) {
            CommonDriver zhang = new CommonDriver("小张");
            ProxyDriver proxy1 = zhang.callProxy("小代");
            ProxyDriver proxy2 = new ProxyDriver("假冒的代驾", zhang);
            zhang.driveCar(zhang);
            proxy2.driveCar(proxy1);
            proxy1.driveCar(proxy1);
        }
    }
    小张叫了个代驾:小代
    酒后不开车,请叫代驾!
    代驾「假冒的代驾」正在为小张服务...
    酒后不开车,请叫代驾!
    代驾「小代」正在为小张服务...
    小张的汽车正在行驶...  


    四、动态代理

    静态代理和动态代理

       上面介绍的代理模式,全部属于“静态代理”。与之相对的还有“动态代理”。相比于静态代理,动态代理有更广泛的应用。二者区别如下:

    静态代理:代理类和被代理类的关系在编译时就已经固定。
    其逻辑简单直观,易于理解和实现,但缺乏灵活性,适合简单的代理逻辑场景。
    动态代理:代理对象在程序运行时被动态生成,通常依赖反射机制实现。
    其灵活性高、可扩展性强,虽然相比静态代理增加些微性能开销,但完全可以接受。

    JDK动态代理

       让我们看看如何使用JDK方式实现动态代理。举一个最简单的例子:有一个HelloWorld接口,其实现类HelloWorldImpl实现了其helloWorld()方法,并返回“Hello world!”。

    public interface HelloWorld {
        String helloWorld();
    }
    public class HelloWorldImpl implements HelloWorld {
        @Override
        public String helloWorld() {
            return "Hello world!";
        }
    }

       如果是静态代理,我们会再定义一个代理类(也许会叫ProxyHelloWorld),再让它去实现HelloWorld,对吧?而在动态代理中,我们需要创建一个通用的动态代理类,该类需要实现java.lang.reflect.InvocationHandler接口,并重写invoke()方法——该方法负责处理所有通过代理对真实主题的访问。同时,类似于静态代理,我们会在这个动态代理类中保存一个私有变量target,记录被代理的真实主题。代码如下:

    public class DynamicProxy implements InvocationHandler {
    
        private Object target;
    
        public DynamicProxy(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.printf("进入动态代理,调用方法:%s开始,入参:%s\n", method.getName(), JSON.toJSONString(args));
            Object result = method.invoke(target, args);
            System.out.printf("进入动态代理,调用方法:%s结束,出参:%s\n", method.getName(), JSON.toJSONString(result));
            return result;
        }
    }

       如上所示,我们同时在invoke方法中记录方法的出入参,这样就实现了一个日志代理的功能。

       写好了动态代理类,我们就可以这个类就可以动态代理任何接口。让我们代理一下HelloWorld接口吧~实现如下:

    public class Main {
        public static void main(String[] args) {
            HelloWorld helloWorld = new HelloWorldImpl();
            System.out.println(helloWorld.helloWorld());
    
            HelloWorld helloWorldProxy = (HelloWorld) Proxy.newProxyInstance(HelloWorldImpl.class.getClassLoader(),
                    new Class[]{HelloWorld.class}, new DynamicProxy(helloWorld));
    
            System.out.println(helloWorldProxy.helloWorld());
        }
    }

       我们先创建了一个真实主题helloworld,并直接访问了其方法;接下来为其使用的方法是java.lang.reflect.Proxy#newProxyInstance(),它可以生成一个动态代理对象,其中三个参数分别为:

       (1)被代理的真实主题的类加载器;

       (2)需要代理的接口类列表;

       (3)动态代理类,即我们刚才定义的,实现了java.lang.reflect.InvocationHandler接口的类。

       该方法默认返回Object类型的对象,我们将其显式转换为HelloWorld类型,就可以访问其方法啦。运行一下:

    Hello world!
    进入动态代理,调用方法:helloWorld开始,入参:null
    进入动态代理,调用方法:helloWorld结束,出参:"Hello world!"
    Hello world!

    CGlib动态代理

       CGlib(Code Generation Library)是一个基于字节码生成的 Java 库。相比于使用反射的JDK动态代理,CGlib通过操作字节码实现了更为高效的动态代理。类似于JDK动态代理需要创建一个实现了InvocationHandler的代理类,CGlib动态代理同样需要创建一个实现了net.sf.cglib.proxy.MethodInterceptor接口的代理类,并重写intercept()方法以控制所有通过代理对真实对象的访问,同时记录被代理的真实主题。代码如下:

    public class DynamicProxyCglib implements MethodInterceptor {
    
        private Object target;
        
        public DynamicProxyCglib(Object target) {
            this.target = target;
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.printf("进入CGlib动态代理,调用方法:%s开始,入参:%s\n", method.getName(), JSON.toJSONString(args));
            Object result = methodProxy.invoke(target, args);
            System.out.printf("进入CGlib动态代理,调用方法:%s结束,出参:%s\n", method.getName(), JSON.toJSONString(result));
            return result;
        }
    }

       别忘了引入CGlib依赖:

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>

       CGlib的动态代理不是基于接口的,而是基于类的,这意味着我们可以跳过接口直接代理实现类,甚至是没有实现接口的类。这是因为CGlib的动态代理是基于类的继承实现的——其代理类是被代理类的子类,它会覆盖被代理类的所有非final方法,并插入拦截和回调机制。举个例子,让我们直接创建一个不实现任何接口的类Hello:

    public class Hello {
        public String hello(String name) {
            return "Hello, " + name;
        }
    }

       用CGlib对Hello类进行代理:

    public static void main(String[] args) {
        Hello hello = new Hello();
        System.out.println(hello.hello("world"));
    
        Enhancer helloEnhancer = new Enhancer();
        helloEnhancer.setSuperclass(Hello.class);
        helloEnhancer.setCallback(new DynamicProxyCglib(hello));
    
        Hello helloProxy = (Hello) helloEnhancer.create();
        System.out.println(helloProxy.hello("world"));
    }

       在以上代码中,我们使用了net.sf.cglib.proxy.Enhancer类,这是CGlib用于生成动态代理类的核心工具类。我们创建了一个Enhancer实例,并使用setSuperclass()方法指定了被代理的类(即代理类的父类)、使用setCallback()方法指定拦截回调对象(即实现了MethodInterceptor的实例)。

    CGlib同样支持接口维度的动态代理,当代理接口时,使用Enhancer.setInterfaces()方法,传入一个或多个接口的类对象数组,以指定被代理接口。

       这样,当我们调用其create()方法时,就可以创建一个代理对象,将其转换为被代理的类型,就可以访问其方法啦。以上代码的运行结果:

    Hello, world
    进入CGlib动态代理,调用方法:hello开始,入参:["world"]
    进入CGlib动态代理,调用方法:hello结束,出参:"Hello, world"
    Hello, world


    动态代理与AOP

    面向切面编程
       面向切面编程(Aspect-Oriented Programming,AOP),是一种基于「横切关注点」的编程范式。作为传统的面向对象编程(OOP)的一种补充,其注重于解决在OOP过程中出现的跨越多个类或对象的「横切关注点」问题。
       「横切关注点」是指多个模块或类中都存在的、但是又不属于任何一个单独模块或类的特定功能或行为,如日志记录、权限控制等非功能性需求。这些功能往往跨越多个模块和类,很容易导致代码重复,难以维护。在传统的OOP中,横切关注点往往不易模块化。而在AOP中,横切关注点被单独抽离出来,并封装为切面(Aspect),将需要执行切面的地方称为切入点(Pointcut),并通过织入(Weaving)将切面逻辑和目标代码结合起来。这种对横切关注点的模块化有利于减少代码重复,提升可读性、可复用性和可扩展性。常见的AOP框架有Spring AOP、AspectJ等。

       简要介绍完了AOP,不难发现:AOP与动态代理似乎非常接近。实际上,动态代理和AOP确实有着紧密的联系:

       (1)动态代理和AOP要解决的问题相似:

           在AOP中力求解决的「横切关注点」问题实际上也是代理模式,尤其是动态代理能够解决的问题——都是通过将重复的非功能性逻辑进行抽离封装以实现软件架构的优化

       (2)动态代理是AOP的一种实现技术:

           在Spring AOP框架中,通过动态代理模式实现切面的织入。

           当切入点所在的对象(简称目标对象)实现了一个/多个接口时,Spring AOP将使用JDK动态代理,为其创建一个实现了相同接口的类作为代理类;

           当目标对象没有实现接口时,Spring AOP将使用CGlib动态代理,为其创建一个重写其方法的子类作为代理类。

       需要明确的是:虽然二者联系紧密,看似十分接近,但不能简单地认为AOP是升级版的动态代理,二者并非等价的概念:

       (1)动态代理本质是一种设计模式,方便我们通过代理对象实现拦截调用,其核心功能是代理,同时适合通过代理解决横切关注点问题;

       (2)AOP本质是一种编程范式,核心在分离和模块化横切关注点。动态代理可以实现AOP,但AOP并不局限于用动态代理实现,如AspectJ框架也使用了静态织入(编译期)方式实现AOP。


           以上就是关于代理模式的全部介绍啦,作为一种非常流行且强大的设计模式,结合实际场景活学活用,相信它一定可以在您的开发之路上有用武之地。希望这篇文章可以对大家有所帮助,也欢迎大家交流、指正!