设计模式 -- 代理模式

305 阅读7分钟

1. 代理模式使用场景

生活中的房产中介、婚介、经纪人、快递、无侵入式日志监听等都是代理模式的实际体现。**代理模式的定义也非常简单,是指为其他对象提供一种代理,以控制这个对象的访问。代理对象在客户端和目标对象之前起到中介的作用,代理模式属于结构型设计模式。使用代理模式有两个目的:一是保护目标对象,二是增强目标对象

代理模式又被分为两种,一是静态代理,二是动态代理。


2. 静态代理


静态代理就是代理类在编译期间就创建好了,是手动创建的类。即代理类和目标对象的关系在程序运行前就已经存在了。它适合用于目标对象中要代理的方法比较少且确定的情况,如果目标对象中有方法很多就会很难受,要在代理类中写一堆的代理方法

2.1 静态代理场景演绎

下面来模拟一个场景,就是房主提供房源,房产中介帮助销售,成功销售后并收取 2% 的利润

首先需要一个房主接口类 HomeOwnerInterface

/**
 * 房主接口,拥有一套房,交给房产中介打算转手卖掉
 *
 * 具体的房主对象和房产中介对象都要实现这个接口
 */
public interface HomeOwnerInterface {

    /**
     * prise - 房主期望的卖掉房子的价格
     */
    public void sailHome(double prise);

}


现在张三找到房产中介,想卖掉自己的房子,因此需要创建张三房主类 ZhangSanHomeOwner

/**
 * 房主张三
 */
public class ZhangSanHomeOwner implements HomeOwnerInterface {

    @Override
    public void sailHome(double prise) {
        System.out.println("房主卖掉房子获得" + prise + "元");
    }
}


我们还需要创建房产中介对象 StaticHomeAgent 来帮助房主卖房子

/**
 * 房产中介,帮助房主卖房,并收取2%的利润,作为房主的代理对象,也要实现房主接口
 */
public class StaticHomeAgent implements HomeOwnerInterface{

    private final HomeOwnerInterface homeOwnerInterface;


    public StaticHomeAgent(HomeOwnerInterface homeOwnerInterface) {
        this.homeOwnerInterface = homeOwnerInterface;
    }

    @Override
    public void sailHome(double prise){
        homeOwnerInterface.sailHome(prise * (1 - 0.02));
        System.out.println("房产中介收取" + prise * 0.02 + "的利润");
    }
}


最后顾客找房产中介买房,房产中介以房主期望的价格卖掉房子,并收取 2% 的提成

/**
 * 主函数,顾客找房产中介买房,房产中介以房主期望的价格卖掉房子,并收取 2% 的提成
 */
public class StaticMain {
    public static void main(String[] args) {

        ZhangSanHomeOwner owner = new ZhangSanHomeOwner();

        StaticHomeAgent homeAgent = new StaticHomeAgent(owner);

        homeAgent.sailHome(10000);

    }
}


最后看一下控制台输出的结果
image.png

2.2 静态代理总结

静态代理的代理类是程序编译前就已经编写好的,若是目标对象,也就是这里的房主对象有很多的方法需要代理,那代理类实现和房主实现同样的接口,需要对每个接口分别进行处理,十分的麻烦,无法进行统一的处理




3. 动态代理


代理类是在程序运行时创建的代理方式称为动态代理,相比于静态代理而言,动态代理的优势是可以对代理类中的方法进行统一的处理,而不用修改每个代理类中的方法。动态代理常用的又有两种:一是 jdk 动态代理,**二是 cglib 动态代理,**两种方式各有好处


3.1 jdk 动态代理


3.1.1 jdk 动态代理代码实现

在 jdk 动态代理中主要涉及两个类,java.lang.reflect.InvocationHandlerjava.lang.reflect.Proxy,需要创建一个代理类来实现InvocationHandler 接口,这个接口中只有一个方法 invoke,我们在对目标对象中所有的方法调用都会变成是调用invoke 方法。这样我们就可以在 invoke 方法中添加一些特定的逻辑进行处理

现在我们对上面静态代理的案例进行改造,实现动态代理卖房子。房主类不需要变,只需要新建一个动态代理类

/**
 * 房产中介,帮助房主卖房,并收取2%的利润,作为房主的代理对象
 */
public class DynamicHomeAgent{

    private static final List<HomeOwnerInterface> homeOwners = new ArrayList<>();

    static {
        homeOwners.add(new ZhangSanHomeOwner());
    }

    public HomeOwnerInterface getHomeOwnerProxy(){
        HomeOwnerInterface homeOwner = homeOwners.remove(0);

        return (HomeOwnerInterface) Proxy.newProxyInstance(
                homeOwner.getClass().getClassLoader(),
                homeOwner.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("房产中介收取" + (double) args[0] * 0.02 + "元的利润");

                        args[0] = (double)args[0] * (1 - 0.02);

                        return method.invoke(homeOwner,args);
                    }
                });
    }

}


最后通过主函数进行测试

/**
 * 主函数,模拟房主提供房源,顾客找房产中介买房
 */
public class DynamicMain {
    public static void main(String[] args) {

        HomeOwnerInterface homeOwnerProxy = new DynamicHomeAgent().getHomeOwnerProxy();

        homeOwnerProxy.sailHome(10000);

    }
}


最后控制台输出结果
image.png


3.1.2 jdk 动态代理核心 API


jdk 动态代理的用法已经写在代码里了,咱对这里的用法和核心 api 做一个回顾

jdk 的动态代理,要求被代理的对象所属类必须实现一个以上的接口,代理对象的创建使用 Proxy.newProxyInstance 方法,该方法中有三个参数:

  • ClassLoader loader :被代理的对象所属类的类加载器
  • Class<?>[] interfaces :被代理的对象所属类实现的接口
  • InvocationHandler h :代理的具体代码实现


在这三个参数中,前面两个都容易理解,最后一个 InvocationHandler 是一个接口,它的核心方法 invoke 中也有三个参数,一一来看:

  • Object proxy :代理对象的引用(代理后的)
  • Method method :代理对象执行的方法
  • Object[] args :代理对象执行方法的参数列表


具体的代理逻辑就在 InvocationHandler 的 invoke 方法中编写。





3.2 cglib 动态代理


使用 jdk 动态代理需要我们对目标对象进行接口的抽取,而 Cglib 不用,它可以直接使用字节码增强的技术,同样实现动态代理。

3.2.1 导包

使用 cglib 动态代理之前需要先导 cglib 的依赖

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

使用 Cglib 时有几个小小的前提:被代理的类不能是 final 的( Cglib 动态代理会创建子类,final 类型的 Class 无法继承),被代理的类必须有默认的 / 无参构造方法(底层反射创建对象时拿不到构造方法参数)。


3.2.2 创建代理类


然后只需要新建一个基于 cglib 的代理类即可

/**
 * cglib 动态代理,房产中介代理类
 */
public class CglibHomeAgent {

    private static final List<HomeOwnerInterface> homeOwners = new ArrayList<>();


    static {
        homeOwners.add(new ZhangSanHomeOwner());
    }


    public HomeOwnerInterface getProxy(){
        HomeOwnerInterface homeOwner = homeOwners.remove(0);
        return (HomeOwnerInterface) Enhancer.create(homeOwner.getClass(), new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

                System.out.println("房产中介收取" + (double) args[0] * 0.02 + "元的利润");

                args[0] = (double)args[0] * (1 - 0.02);

                return method.invoke(homeOwner,args);

            }
        });
    }

}

3.2.3 使用主函数测试

/**
 * 主函数,模拟房主提供房源,顾客找房产中介买房
 */
public class DynamicMain {
    public static void main(String[] args) {
        HomeOwnerInterface homeOwnerProxy = new DynamicHomeAgent().getHomeOwnerProxy();

        homeOwnerProxy.sailHome(10000);

    }
}


查看控制台,得到一样的结果
![image.png](https://cdn.nlark.com/yuque/0/2021/png/2649107/1615258896492-32f87ea1-a0d5-4fb0-aa0e-94f165992423.png#align=left&display=inline&height=85&margin=%5Bobject%20Object%5D&name=image.png&originHeight=85&originWidth=387&size=8147&status=done&style=none&width=387)


3.2.4 Cglib动态代理的核心 API

同 jdk 动态代理相似,Cglib 动态代理的内容相对较少,它只需要传入两个东西:

  • Class type :被代理的对象所属类的类型
  • Callback callback :增强的代码实现


由于一般情况下我们都是对类中的方法增强,所以在传入 Callback 时通常选择这个接口的子接口 MethodInterceptor (所以也就有了上面代码中 new 的 MethodInterceptor 的匿名内部类)。
MethodInterceptorintercept 方法中参数列表与 InvocationHandlerinvoke 方法类似,唯独多了一个 MethodProxy ,它是对参数列表中的 Method 又做了一层封装,利用它可以直接执行被代理对象的方法,就像这样:

// 执行代理对象的方法
method.invoke(proxy, args);
// 执行原始对象(被代理对象)的方法
methodProxy.invokeSuper(proxy, args);

3.3 cglib 和 jdk 动态代理对比

  1. jdk 动态代理生成的代理类实现了被代理对象的接口,cglib 代理生成的代理类继承了被代理对象
  2. jdk 动态代理和 cglib 动态代理都在运行期生成字节码,jdk 动态代理直接写 Class 字节码,cglib 代理使用 ASM 框架写 Class 字节码,cglib 代理实现更复杂,生成代理类的效率比 jdk 代理低
  3. jdk 动态代理调用代理方法是通过反射机制调用的,cglib 代理是通过 fastclass 机制直接调用方法的,cglib 代理的执行效率更高





4. 静态代理和动态代理本质区别


1、静态代理只能通过手动完成代理操作,如果被代理类增加了新的方法,代理类需要同步增加,违背开闭原则
2、动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则(即被代理类增加了新的方法,代理类会自动生成)



5. 代理模式的优缺点


代理模式具有以下优点:

1、代理模式能将代理对象与真实被调用目标对象分离,可以起到保护目标对象的作用
2、在一定层度降低了系统的耦合性,扩展性好
3、可以增强目标对象的功能

代理模式也有缺点:

1、在客户端和目标对象总增加代理对象,会导致请求处理速度变慢
2、增加了系统的复杂度