18.古今成大事者,必以多选替身为第一要义——代理模式详解

本文介绍了代理模式的核心思想,探讨了其在编程中的静态代理和动态代理(包括JDK代理和Cglib代理)的应用,以及在解决实际问题中的优劣分析。作者还通过电视剧《三叉戟》中的预审场景进行了生动说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

“杏市而外,尚有何人可以分统?亦须早早提拔。办大事者以多多选替手为第一义,满意之选不可得,姑节取其次,以待徐徐教育可也。 ——曾国藩·同治元年四月十二日”

在这里插入图片描述


一言

代理模式核心思想是为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象。


概述

古今成大事者,必以多选替身为第一要义。“风云人物”们最容易犯的错误是恋权恋栈。在他们自己看来,总是以为“非我不可”,别人都不如我。曾国藩则恰恰相反,他在血腥镇压了太平天国运动之后,主动推进湘军的裁撤,交权、交人,功成身退。
用他自己的话来说就是“办大事者以多多选替手为第一义”。编程如做人,设计模式是一门哲学。代理模式的思想恰恰就是这包含大智慧的“找替身”。
在这里插入图片描述

我们在实际的开发过程中,常常遇到开销极大的对象、需要安全控制的对象或者一些远程对象,在面临以上场景的时候总会有一种尾大不掉的感觉。在这种时候如果能对代理模式稍加应用真的就会有种“柳暗花明”的感觉。
在这里插入图片描述
我习惯将代理模式分为两种:静态代理、动态代理,当然,很多资料会把动态代理细分为JDK代理和Cglib代理,这次我也会逐一进行拆解。


三叉戟登场

电视剧《三叉戟》真的是我的珍藏剧目了,尤其是预审嫌疑人的桥段,三叉戟分工明确,有的搜集证据,有的旁敲侧击,有的单刀直入…我们不妨以这个场景为例来拆解下代理模式。
在这里插入图片描述


静态代理

设计

在这里插入图片描述

代码实现

审讯能力

public interface InterrogationDao {
    void interrogation();
}

预审民警

public class Police implements InterrogationDao{
    @Override
    public void interrogation() {
        System.out.println("预审警察正在提审嫌疑人");
    }
}

预审文员

public class PoliceProxy implements InterrogationDao{
    private Police police;
    public PoliceProxy(Police police) {
        this.police = police;
    }
    @Override
    public void interrogation() {
        System.out.println("开始代理...预审文员协助梳理卷宗");
        police.interrogation();
        System.out.println("代理结束...预审文员协助归纳供词");
    }
}

测试

public class Client {
    public static void main(String[] args) {
        PoliceProxy policeProxy = new PoliceProxy(new Police());
        policeProxy.interrogation();
    }
}

在这里插入图片描述

优劣分析

在上例中,文员与预审民警都具有审问能力,而文员更多的是充当一个代理人的角色,预审过程中一些前置和后置的操作都由他来代理完成。预审警察则承担起主要的业务职责。
在不修改目标对象功能的前提下,能够通过代理对象对目标功能进行扩展。但是由于代理对象需要与目标对象实现一样的接口,导致代理类的数量会逐渐膨胀而且一旦接口增加方法,目标对象与代理对象势必要同时进行维护,这其实是很致命的缺陷。


动态代理

相信很多同学看到这里已经想起了一些常用的东西,没错,就是SpringAOP。SpringAOP是代理模式最普遍的应用之一,它正是通过所谓的切面编程实现了极低耦合下的业务嵌入开发。但我们仔细想想,如果是按照上文所描述的静态代理模式,整体架构的耦合度怎么可能低呢?
事实上,SpringAOP的实现并非基于静态代理,而是基于动态代理实现的。


JDK代理

java生态中,泛化关系的实现要么通过类继承、要么通过接口实现。JDK代理在处理动态代理不可避免的泛化问题时,采用了接口实现的这个路线。
在JDK代理模式中,代理对象不需要实现接口,但目标对象需要实现接口,否则就不能实现动态代理。所以说JDK代理也被称为接口代理。
JDK代理基于JDK API,动态的在内存中构建代理对象。

代码实现

审讯能力

public interface InterrogationDao {
    void interrogation();
    void sayHello();
}

预审民警

public class Police implements InterrogationDao {
    @Override
    public void interrogation() {
        System.out.println("预审警察正在提审嫌疑人");
    }
    @Override
    public void sayHello() {
        System.out.println("你这样做,对得起党和人民的信任吗?!");
    }
}

预审文员筹备处

public class PoliceProxyOffice {
    private Object target;
    public PoliceProxyOffice(Object target) {
        this.target = target;
    }
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),(proxy,method,args)->{
            System.out.println("JDK代理开始...预审文员办公室正在梳理卷宗");
            Object returnVal = method.invoke(target, args);
            System.out.println("JDK代理提交...预审文员办公室完成供词归纳");
            return returnVal;
        });

    }
}

测试

public class Client {
    public static void main(String[] args) {
        InterrogationDao target = new Police();
        InterrogationDao proxyInstance = (InterrogationDao) new PoliceProxyOffice(target).getProxyInstance();
        System.out.println("proxyInstance"+proxyInstance);
        System.out.println("proxyInstanceClass"+proxyInstance.getClass());
        proxyInstance.sayHello();
        proxyInstance.interrogation();
    }
}

在这里插入图片描述

分析

我们可以清晰的看到,通过JDK动态代理,我们直接切断了预审民警和代理类的直接耦合关系,整个过程也更加清爽和优雅。随着业务的扩展,即便业务类中的实现细节快速变化或增长,动态代理的模式下并不需要过度关心代理类的实现。依托于反射机制,无论业务类如何扩充,代理类都会巧妙的自动扩充。


Cglib代理

刚刚有说过,java生态下泛化关系的实现要么通过类继承、要么通过接口实现。
静态代理和JDK代理都要求目标对象去实现接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候可以使用目标对象子类来实现代理,这就是Cglib代理的底层原理。
在内存中构建一个子类对象从而实现对目标对象功能的扩展,通过类继承的方式实现代理,这也是为什么Cglib代理又被称为子类代理的原因。
Cglib是一个强大的高性能的代码生成包,它可以在运行期间扩展java类。正是因为这一特性,它才得以被广大的AOP框架应用,这其中就包括大名鼎鼎的SpringAOP。

代码实现

首先要注意,在没有其它依赖的情况下要引入cglib的核心包
在这里插入图片描述

预审警察

public class IterrogationDao {
    public void interrogation(){
        System.out.println("预审警察正在提审嫌疑人");
    }
    public String sayHello(){
        String msg = "你这样做,对得起党和人民的信任吗?!";
        System.out.println(msg);
        return msg;
    }
}

预审文员筹备处

public class PoliceProxyOffice implements MethodInterceptor {
    private Object target;
    public PoliceProxyOffice(Object target) {
        this.target = target;
    }
    public Object getProxyInstance(){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib代理开始...预审文员办公室正在梳理卷宗");
        Object returnVal = method.invoke(target, args);
        System.out.println("Cglib代理提交...预审文员办公室完成供词归纳");
        return returnVal;
    }
}

测试

public class Client {
    public static void main(String[] args) {
        IterrogationDao target = new IterrogationDao();
        IterrogationDao proxyInstance = (IterrogationDao) new PoliceProxyOffice(target).getProxyInstance();
        proxyInstance.interrogation();
        String msg = proxyInstance.sayHello();
        System.out.println("msg:"+msg);
    }
}

在这里插入图片描述

分析

可以很清晰的看到,在基于cglib实现动态代理的过程中,并没有任何接口的实现。这也是JDK代理与Cglib代理的最大区别。Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类。

需要注意的是,cglib既然是基于类继承的方式实现了动态代理,那么目标对象的目标方法就必须能够被继承,这也就要求目标方法必然是不可以用final修饰的。同理对于static修饰的方法,动态代理也自然会跳过。如果违反上述规定就会报错:java.lang.IllegalArgumentException

在不使用AOP框架的前提下,如何选择两种动态代理的实现方式也许是我们最关注的问题,通常情况下:

  • 如果目标对象需要实现接口,用JDK代理
  • 如果目标对象不需要实现接口,用Cglib代理

几种变体

在这里插入图片描述

前面我们花了大量的篇幅来讲静态代理和动态代理,相信大家对于这两种编程中最常见的代理模式都有了自己的理解。
事实上代理思想并不仅仅拘泥于上述的几种场景。
下面几种大家耳熟能详的场景都是代理模式的应用,比如说:

  • 内网通过代理穿透防火墙实现公网访问的防火墙代理;
  • 为了应对各种击穿,大家经常用缓存处理高频数据读取的缓存代理;
  • 多线程编程中的多线程同步用到的同步代理等等;

编程是一门艺术,它使得每个人对于不同的场景都有不同的解读方式,尤其是针对设计模式而言,它不仅能让我们学会一种解决方案,更能让我们从中领悟人生的哲理。
愿我们都能不忘初心,保持热爱,奔赴山海。


关注我,共同进步,我们的世界中万物皆代码,又不只有代码。希望我的文字能澎湃你的思绪。——Wayne

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值