代理模式

【 故事背景 】

小明想买法国香水送女朋友,但国内没有货源售卖
小明托在法国的小红帮忙购买,并给5%辛苦费
小红答应并开启代购模式,转到了很多手续费

故事中,[ 小明是一个客户 ],他让小红帮忙购买香水,[ 小红就成了代理对象 ],而[ 香水提供商是一个真实的对象 ],可以售卖香水,小明通过代理商小红,购买到法国的香水,这就是一个代购的例子。
在这里插入图片描述
这就是最典型的代理模式,代购从供应商购买货物后返回给调用者。


代理可以分为静态代理动态代理两大类:

【 静态代理 】

  • 优点:代码结构简单,较容易实现
  • 缺点:无法适配所有代理场景,如果有新的需求,需要修改代理类,[ 不符合软件工程的开闭原则 ]

【 动态代理 】

  • 优点:能够动态适配特定的代理场景,扩展性较好,[ 符合软件工程的开闭原则 ]
  • 缺点:动态代理需要利用到反射机制和动态生成字节码,导致其性能会比静态代理稍差一些,相比优点可忽略不计

本文将通过以下几点,尽可能让你理解Java代理中所有重要的知识点:

  1. 学习代理模式(实现故事的代码,解释代理模式的类结构特点)
  2. 比较静态代理和动态代理二者的异同
  3. Java中常见的两种动态代理的实现(JDK Proxy 和 Cglib)
  4. 动态代理的应用(Spring AOP)

【 代理模式 】

(1)我们定义好一个[ 售卖香水 ]的接口,定义好售卖香水的方法并传入香水的价格。

在这里插入图片描述
(2)定义香奈儿(Chanel)香水提供商,实现接口
在这里插入图片描述
(3)定义 [小红] 代理类,她需要代购去售卖香奈儿香水,所以他是香奈儿香水提供商的代理对象,同样实现接口,并在内部保存对目标对象(香奈儿提供商)的引用,控制其他对象对目标对象的访问。
在这里插入图片描述
(4)小明是一个需求者,他需要去购买香水,只能通过小红去购买,所以他去找小红购买 [1999.99] 香水
在这里插入图片描述

我们来看看上面4个类似组成的类图关系结构,可以发现 [小红][香奈儿提供商]都实现了 [售卖香水] 这一接口,而小红内部增加了对提供商的引用, 用于调用提供商的售卖香水功能。
在这里插入图片描述

实现代理模式,需要走以下几个步骤:

  • [定义真实对象和代理对象的公共接口](售卖香水接口)
  • [代理对象内部保存对真实目标对象的引用](小红引用提供商)
  • 访问者仅能通过代理对象访问真实目标对象,[不可直接访问目标对象](小明只能通过小红去购买香水,不能直接到香奈儿提供商购买)

代理模式很容易产生错误思维的一个地方:代理对象并不是真正提供服务的一个对象,他只是替访问者访问目标对象的一个[中间人],真正提供服务的还是目标对象,而代理对象的作用就是在目标对象提供服务之前和之后能够执行额外的逻辑。
从故事来说,小红并不是真正卖香水的,卖香水的还是香奈儿提供商,而小红只不过是在让香奈儿卖香水之前和之后执行了一些自己额外加上去的操作。

讲完这个代理模式的代码实现,我们来系统的学习它究竟是如何定义的,以及实现它需要注意什么规范。

代理模式的定义:[给目标对象提供一个代理对象,代理对象包含该目标对象,并控制对该目标对象的访问]

代理模式的目的:

  • 通过代理对象的隔离,可以在对目标对象访问前后[增加额外的业务逻辑,实现功能增强]
  • 通过代理对象访问目标对象,可以[防止系统大量地对目标对象进行不正确访问],出现不可预测的后果

静态代理和动态代理

代理为什么要分静态的和动态的?

他们的相同点:

  • 都能够实现代理模式
  • 无论是静态代理还是动态代理,代理对象和目标对象都需要实现一个公共接口

不同之处,动态代理在静态代理的基础上做了改进,极大地提高了程序的可维护性可扩展性。我先列出他们两的不同之处,再详细解释为何静态代理不具备这两个特性:

  • 动态代理产生代理对象的时机是[运行时动态生成],他没有java源文件,[直接生成字节码文件实例化代理对象];而静态代理的代理对象,在[程序编译]时已经写好java文件了,直接new一个代理对象即可。
  • 动态代理比静态代理更加稳健,对程序的可维护性和可扩展性更加友好。

目前来看,代理对象小红已经能够代理购买香水了,如果小红的另一个朋友小何想购买[法国红酒],想让小红做代理。

但问题是:在程序中,小红只能代理购买香水,[如果要代理购买红酒],要怎么做呢?

  • 创建售卖红酒的接口
  • 售卖红酒提供商和代理对象小红都需要实现该接口
  • 小何访问小红,小红卖给他红酒

在这里插入图片描述
我们来探讨一下,面对这种新增的场景,上面的这种实现方法有没有什么缺陷呢?

我们不得不提的是软件工程中的[开闭原则]

开闭原则:在编写程序的过程中,软件的所有对象应该是对扩展是开放的,而对修改是关闭的

静态代理违反了开闭原则,原因是:面对新的需求时,需要修改代理类,增加实现新的接口和方法,导致代理类越来越庞大,变得难以维护。

[所以,为了提高类的可扩展性和可维护性,满足开闭原则,Java提供了动态代理机制]


常见的动态代理实现

我们来明确一点,[动态代理解决的问题是面对新的需求时,不需要修改代理对象的代码,只需要新增接口和真实对象,在客户端调用即可完成新的代理。]

这样做的目的:满足软件工程的开闭原则,提高类的可扩展性和可维护性。

JDK Proxy

JDK Proxy 是 JDK 提供的一个动态代理机制,它涉及到两个核心类,分别是ProxyInvocationHandler,我们先来了解如何使用它们

以小红代理卖香水的故事为例,香奈儿香水提供商依旧是真实对象,实现了SellPerfume接口,重点是[小红代理],这里的代理对象不再是一个人,而是一个[代理工厂],里面会有许多的代理对象。
在这里插入图片描述

小明来到代理工厂,需要购买香奈儿香水,那么工厂就会[就会找一个可以实际的代理对象(动态实例化)]分配给小明,让该代理对象完成小明的需求。[该代理工厂含有无穷无尽的代理对象可以分配,且每个对象可以代理的事情可以根据程序的变化而动态变化,无需修改代理工厂。]

如果有天小明需要找一个可以[代购红酒]的代理对象,该代理工厂依旧可以满足它的需求,无论日后需要什么代理,都可以满足,我们来学习如何使用它

我们看一下动态代理的UML类图长什么样子
在这里插入图片描述
可以看到和静态代理区别不大,唯一的变动就是代理对象,[由代理工厂生产]

这句话的意思是:[代理对象是在程序运行过程中, 由代理工厂动态生成,代理对象本身不存在Java源文件]

那么,我们的关注点有2个:

  • 如何实现一个代理工厂
  • 如何通过代理工厂动态生成代理对象

首先,代理工厂需要实现InvocationHandler接口并实现其invoke()方法。
在这里插入图片描述
invoke() 方法有3个参数:

  • Object proxy:代理对象
  • Method method:真正执行的方法
  • Object[] args:调用第二个参数method是传入的参数列表值

invoke() 方法是一个代理方法,也就是说最后客户端请求代理时,执行的就是该方法。代理工厂类到这里为止已经结束了。接下来看第二点:[如何通过代理工厂动态生成代理对象]

生成代理对象需要用到Proxy类,它可以帮助我们生成任意一个代理对象,里面提供一个静态方法newProxyInstance
在这里插入图片描述
实例化代理对象时,需要传入3个参数:

  • ClassLoader loader:加载动态代理类的类加载器
  • Class<?> interfaces:代理类实现的接口,可以传入多个接口
  • InvocationHandler h:指定代理类的[调用处理程序],即调用接口中的方法时,会找到该代理工厂h,执行invoke()方法

我们在客户端请求代理时,就需要用到上面这个方法。

public class XiaoMing {
    public static void main(String[] args) {
        ChanelFactory chanelFactory = new ChanelFactory();
        SellProxyFactory sellProxyFactory = new SellProxyFactory(chanelFactory);
        SellPerfume sellPerfume = (SellPerfume) Proxy.newProxyInstance(chanelFactory.getClass().getClassLoader(),
                chanelFactory.getClass().getInterfaces(),
                sellProxyFactory);
        sellPerfume.sellPerfume(1999.99);
    }
}

执行结果和静态代理的结果相同,但二者的思想是不一样的,一个是静态,一个是动态

注意看下图,相比静态代理的前置增强和后置增强,少了小红二字,实际上代理工厂分配的代理对象是随机的,不会针对某一个具体的代理对象,所以每次生成的代理对象都不一样,也就不确定是不是小红了,但是能够唯一确定的是,[这个代理对象能和小红一样帮小明买到香水]

在这里插入图片描述
按照之前的故事线发展,[小明又想买法国红酒],所以去找代理工厂,让它再分配一个人帮小明买红酒。

我们需要实现两个类:红酒提供商类 和售卖红酒接口

在这里插入图片描述
然后我们的小明在请求代理工厂时,就可以[实例化一个售卖红酒的代理]

public class XiaoMing {
    public static void main(String[] args) {
        // 实例化一个红酒销售商
        RedWineFactory redWineFactory = new RedWineFactory();
        // 实例化代理工厂,传入红酒销售商引用控制对其的访问
        SellProxyFactory sellProxyFactory = new SellProxyFactory(redWineFactory);
        // 实例化代理对象,该对象可以代理售卖红酒
        SellWine sellWineProxy = (SellWine) Proxy.newProxyInstance(redWineFactory.getClass().getClassLoader(),
                redWineFactory.getClass().getInterfaces(),
                sellProxyFactory);
        // 代理售卖红酒
        sellWineProxy.sellWine(1999.99);
    }
}

查看执行结果,发现能够代理售卖红酒了,但是我们[没有修改代理工厂]
在这里插入图片描述
回顾新增红酒代理功能时,需要2个步骤:

  • 创建新的红酒提供商SellWineFactory和售卖红酒接口SellWine
  • 在客户端实例化一个代理对象,然后向该代理对象购买红酒

再回想[开闭原则:面向扩展开放,面向修改关闭]。动态代理正是满足了这一重要原则,在面对功能需要扩展时,只需要关注扩展的部分,不需要修改系统中原有的代码。

总结一下JDK的动态代理

(1)JDK动态代理的使用方法

  1. 代理工厂需要实现invocationHandler接口,调用代理方法时会转向执行invoke()方法
  2. 生成代理对象需要使用Proxy对象中的newProxyInstance()方法,返回对象可强转成传入的其中一个接口,然后调用接口方法即可实现代理

(2)JDK动态代理的特点

  • 目标对象强制需要实现一个接口,否则无法使用JDK动态代理

【扩展内容】


Proxy.newProxyInstance()是生成动态代理对象的关键,来看下它的内部逻辑

private static final Class<?>[] constructorParams ={ InvocationHandler.class };
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {
    // 获取代理类的 Class 对象
    Class<?> cl = getProxyClass0(loader, intfs);
    // 获取代理对象的显示构造器,参数类型是 InvocationHandler
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    // 反射,通过构造器实例化动态代理对象
    return cons.newInstance(new Object[]{h});
}

我们看到第6行获取了一个动态代理对象,那么是如何生成的呢?
在这里插入图片描述
发现里面用到一个缓存ProxyClassCache,从结构来看类似于map结构,根据类加载器loader和真实对象实现的接口interfaces查看是否有对应的Class对象,我们接着往下看get()方法
在这里插入图片描述
在get()方法中,如果没有从缓存中获取到Class对象,则需要利用[subKeyFactory]去实例化一个动态代理对象,而在[Proxy]类中包含一个[ProxyClassFactory]内部类,由它来创建一个动态代理类,所以我们接着去看 ProxyClassFactory中的 apply()方法。
在这里插入图片描述

apply() 方法中注意有两个非常重要的方法

  • ProxyGenerator.generateProxyClass():他是生成字节码文件的方法,它返回了一个字节数组,字节码文件本质就是一个字节数组,所以proxyClassFile数组就是一个字节码文件
  • defineClass0:生成字节码文件的Class对象,它是一个native本地方法,调用操作系统底层的方法创建类对象

proxyName是代理对象的名字,我们可以看到它利用了proxyClassNamePrefix + 计数器拼接成一个新的名字。所以在DEBUG时,停留在代理对象变量上,你会发现变量名是$Proxy()

在这里插入图片描述

CGLIB

CGLIB(Code generation Library) 不是JDK自带的动态代理,他需要导入第三方依赖,它是一个字节码生成类库,能够在运行时动态生成代理类对Java类和java接口扩展。

CGLIB不仅能够为Java接口做代理,而且能够为普通的Java类做代理,而JDK Proxy 只能为实现了接口的Java类做代理,所以CGLIB为Java的代理做了很好的扩展。如果需要代理的类没有实现接口,可以选择cglib作为实现代理的工具

一句话概括:[CGLIB可以代理没有实现接口的Java类]

我们以[小明找代理工厂买香水]为例

(1)导入依赖
在这里插入图片描述

还有另外一个CGLIB包,二者的区别是带有-nodep的依赖内部已经包括了ASM字节码框架的相关代码,无需额外依赖ASM

(2)CGLIB代理中有两个核心的类:MethodInterceptor接口和Enhancer类,前者是实现一个代理工厂的根接口,后者是创建动态代理对象的类,结构图如下:
在这里插入图片描述
首先我们来定义代理工厂SellProxyFactory
在这里插入图片描述

intercept() 方法涉及到4个参数:

  • Object o:被代理对象
  • Method method:被拦截的方法
  • Object[] objects:被拦截方法的所有入参值
  • MethodProxy methodProxy:方法代理,用于调用原始的方法

对于methodProxy参数调用的方法,在其内部有两种选择:invoke()invokeSuper(),二者的区别不在本文展开说明

getStance()方法中,利用Enhancer类实例化代理对象(可以看作是小红)返回给调用者小明,即可完成代理操作
80在这里插入图片描述
我们关注点依旧放在可扩展性和可维护性上,Cglib依旧符合开闭原则

总结一下 CGLIB 动态代理:

(1)CGLIB的使用方法:

  • 代理工厂需要实现 MethodInterceptor 接口,并重写方法,内部关联真实对象,控制第三者对真实对象的访问;代理工厂内部暴露getInstance(Object realObject)方法,用于从代理工厂中获取一个代理对象实例
  • Enhancer类用于从代理工厂中实例化一个代理对象,给调用者提供代理服务。
JDK Proxy 和 CGLIB的对比

相似之处:

JDK ProxyCGLIB
代理工厂实现接口InvocationHandlerMethodInterceptor
构造代理对象给Client服务ProxyEnhancer

二者都是用到了两个核心的类,它们也有不同:

  • 最明显的不同:CGLIB可以代理大部分类,而 JDK Proxy仅能够代理实现了接口的类
  • CGLIB采用动态创建被代理类的子类实现方法拦截,子类内部重写被拦截的方法,所以CGLIB不能代理被final关键字修饰的类和方法

动态代理的精髓在于程序在运行时动态生成代理类对象,拦截调用方法,在调用方法前后扩展额外功能而生成动态代理对象的原理就是反射机制

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值