代理?哦,就是那个赚差价中间商啊
作者:程序员cxuan
原文链接:
说在前面:今天我们来聊一聊Java中的代理 , 先来聊聊故事背景:
小明想购买法国某个牌子的香水送给女朋友 , 但是在国内没有货源售卖 , 亲自去法国又大费周章了 , 而小红现在正在法国玩耍 , 她和小明是好朋友 , 可以帮小明买到这个牌子的香水 , 于是小明就找到小红 , 答应给她多加5%的辛苦费 , 小红答应了 , 小明成功在中国买到了法国的香水 。 之后小红开启了疯狂的代购模式 , 赚到了很多手续费 。
在故事中 , 小明是一个客户 , 他让小红帮忙购买香水 , 小红就成了一个代理对象 , 而香水提供商是一个真实的对象 , 可以售卖香水 , 小明通过代理商小红 , 购买到法国的香水 , 这就是一个代购的例子 。 我画了一幅图帮助理解这个故事的整个结构 。
静态代理
优点:代码结构简单 , 较容易实现缺点:无法适配所有代理场景 , 如果有新的需求 , 需要修改代理类 , 不符合软件工程的开闭原则小红现在只是代理香水 , 如果小明需要找小红买法国红酒 , 那小红就需要代理法国红酒了 , 但是静态代理去扩展代理功能必须修改小红内部的逻辑 , 这会让小红内部代码越来越臃肿 , 后面会详细分析 。
动态代理
优点:能够动态适配特定的代理场景 , 扩展性较好 , 符合软件工程的开闭原则缺点:动态代理需要利用到反射机制和动态生成字节码 , 导致其性能会比静态代理稍差一些 , 但是相比于优点 , 这些劣势几乎可以忽略不计如果小明需要找小红代理红酒 , 我们无需修改代理类小红的内部逻辑 , 只需要关注扩展的功能点:代理红酒 , 实例化新的类 , 通过一些转换即可让小红既能够代理香水也能够代理红酒了 。
本文将会通过以下几点 , 尽可能让你理解Java代理中所有重要的知识点:
学习代理模式(实现故事的代码 , 解释代理模式的类结构特点)比较静态代理与动态代理二者的异同Java中常见的两种动态代理实现(JDKProxy和Cglib)动态代理的应用(SpringAOP)代理模式(1)我们定义好一个售卖香水的接口 , 定义好售卖香水的方法并传入该香水的价格 。
publicinterfaceSellPerfume{voidsellPerfume(doubleprice);}(2)定义香奈儿(Chanel)香水提供商 , 实现接口 。
publicclassChanelFactoryimplementsSellPerfume{@OverridepublicvoidsellPerfume(doubleprice){System.out.println("成功购买香奈儿品牌的香水 , 价格是:"+price+"元");}}(3)定义小红代理类 , 她需要代购去售卖香奈儿香水 , 所以她是香奈儿香水提供商的代理对象 , 同样实现接口 , 并在内部保存对目标对象(香奈儿提供商)的引用 , 控制其它对象对目标对象的访问 。
publicclassXiaoHongSellProxyimplementsSellPerfume{privateSellPerfumesellPerfumeFactory;publicXiaoHongSellProxy(SellPerfumesellPerfumeFactory){this.sellPerfumeFactory=sellPerfumeFactory;}@OverridepublicvoidsellPerfume(doubleprice){doSomethingBeforeSell();//前置增强sellPerfumeFactory.sellPerfume(price);doSomethingAfterSell();//后置增强}privatevoiddoSomethingBeforeSell(){System.out.println("小红代理购买香水前的额外操作...");}privatevoiddoSomethingAfterSell(){System.out.println("小红代理购买香水后的额外操作...");}}(4)小明是一个需求者 , 他需要去购买香水 , 只能通过小红去购买 , 所以他去找小红购买1999.99的香水 。
publicclassXiaoMing{publicstaticvoidmain(String[]args){ChanelFactoryfactory=newChanelFactory();XiaoHongSellProxyproxy=newXiaoHongSellProxy(factory);proxy.sellPerfume(1999.99);}}我们来看看运行结果 , 小红在向小明售卖香水前可以执行额外的其它操作 , 如果良心点的代购就会打折、包邮··· , 如果黑心点的代购就会加手续费、售出不退还··· , 是不是很刺激 。
从故事来说 , 小红并不是真正卖香水的 , 卖香水的还是香奈儿提供商 , 而小红只不过是在让香奈儿卖香水之前和之后执行了一些自己额外加上去的操作 。
讲完这个代理模式的代码实现 , 我们来系统地学习它究竟是如何定义的 , 以及实现它需要注意什么规范 。
代理模式的定义:给目标对象提供一个代理对象 , 代理对象包含该目标对象 , 并控制对该目标对象的访问 。
代理模式的目的:
通过代理对象的隔离 , 可以在对目标对象访问前后增加额外的业务逻辑 , 实现功能增强 。 通过代理对象访问目标对象 , 可以防止系统大量地直接对目标对象进行不正确地访问 , 出现不可预测的后果静态代理与动态代理你是否会有我一样的疑惑:代理为什么还要分静态和动态的?它们两个有啥不同吗?
很明显 , 所有人都会有这样的疑惑 , 我们先来看看它们的相同点:
都能够实现代理模式(这不废话吗...)无论是静态代理还是动态代理 , 代理对象和目标对象都需要实现一个公共接口重点当然是它们的不同之处 , 动态代理在静态代理的基础上做了改进 , 极大地提高了程序的可维护性和可扩展性 。 我先列出它们俩的不同之处 , 再详细解释为何静态代理不具备这两个特性:
动态代理产生代理对象的时机是运行时动态生成 , 它没有Java源文件 , 直接生成字节码文件实例化代理对象;而静态代理的代理对象 , 在程序编译时已经写好Java文件了 , 直接new一个代理对象即可 。 动态代理比静态代理更加稳健 , 对程序的可维护性和可扩展性更加友好目前来看 , 代理对象小红已经能够代理购买香水了 , 但有一天 , 小红的另外一个朋友小何来了 , 他想购买最纯正的法国红酒 , 国内没有这样的购买渠道 , 小红刚巧也在法国 , 于是小何就想找小红帮他买红酒啦 , 这和小明找小红是一个道理的 , 都是想让小红做代理 。
但问题是:在程序中 , 小红只能代理购买香水 , 如果要代理购买红酒 , 要怎么做呢?
创建售卖红酒的接口售卖红酒提供商和代理对象小红都需要实现该接口小何访问小红 , 让小红卖给他红酒
开闭原则:在编写程序的过程中 , 软件的所有对象应该是对扩展是开放的 , 而对修改是关闭的
静态代理违反了开闭原则 , 原因是:面对新的需求时 , 需要修改代理类 , 增加实现新的接口和方法 , 导致代理类越来越庞大 , 变得难以维护 。
虽然说目前代理类只是实现了2个接口 , 如果日后小红不只是代理售卖红酒 , 还需要代理售卖电影票、代购日本寿司······实现的接口会变得越来越多 , 内部的结构变得越来越复杂 , 整个类显得愈发臃肿 , 变得不可维护 , 之后的扩展也会成问题 , 只要任意一个接口有改动 , 就会牵扯到这个代理类 , 维护的代价很高 。
所以 , 为了提高类的可扩展性和可维护性 , 满足开闭原则 , Java提供了动态代理机制 。
常见的动态代理实现动态代理最重要的当然是动态两个字 , 学习动态代理的过程 , 最重要的就是理解何为动态 , 话不多说 , 马上开整 。
我们来明确一点:动态代理解决的问题是面对新的需求时 , 不需要修改代理对象的代码 , 只需要新增接口和真实对象 , 在客户端调用即可完成新的代理 。
这样做的目的:满足软件工程的开闭原则 , 提高类的可维护性和可扩展性 。
JDKProxyJDKProxy是JDK提供的一个动态代理机制 , 它涉及到两个核心类 , 分别是Proxy和InvocationHandler , 我们先来了解如何使用它们 。
以小红代理卖香水的故事为例 , 香奈儿香水提供商依旧是真实对象 , 实现了SellPerfume接口 , 这里不再重新写了 , 重点是小红代理 , 这里的代理对象不再是小红一个人 , 而是一个代理工厂 , 里面会有许多的代理对象 。 我画了一幅图 , 你看了之后会很好理解:
我们看一下动态代理的UML类图结构长什么样子 。
那么 , 我们的关注点有2个:
如何实现一个代理工厂如何通过代理工厂动态生成代理对象首先 , 代理工厂需要实现InvocationHanlder接口并实现其invoke()方法 。
publicclassSellProxyFactoryimplementsInvocationHandler{/**代理的真实对象*/privateObjectrealObject;publicSellProxyFactory(ObjectrealObject){this.realObject=realObject;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{doSomethingBefore();Objectobj=method.invoke(realObject,args);doSomethingAfter();returnobj;}privatevoiddoSomethingAfter(){System.out.println("执行代理后的额外操作...");}privatevoiddoSomethingBefore(){System.out.println("执行代理前的额外操作...");}}invoke()方法有3个参数:
Objectproxy:代理对象Methodmethod:真正执行的方法Object[]agrs:调用第二个参数method时传入的参数列表值invoke()方法是一个代理方法 , 也就是说最后客户端请求代理时 , 执行的就是该方法 。 代理工厂类到这里为止已经结束了 , 我们接下来看第二点:如何通过代理工厂动态生成代理对象 。
生成代理对象需要用到Proxy类 , 它可以帮助我们生成任意一个代理对象 , 里面提供一个静态方法newProxyInstance 。
Proxy.newProxyInstance(ClassLoaderloader,Class>[]interfaces,InvocationHandlerh);实例化代理对象时 , 需要传入3个参数:
ClassLoaderloader:加载动态代理类的类加载器Class>[]interfaces:代理类实现的接口 , 可以传入多个接口InvocationHandlerh:指定代理类的调用处理程序 , 即调用接口中的方法时 , 会找到该代理工厂h , 执行invoke()方法我们在客户端请求代理时 , 就需要用到上面这个方法 。
publicclassXiaoMing{publicstaticvoidmain(String[]args){ChanelFactorychanelFactory=newChanelFactory();SellProxyFactorysellProxyFactory=newSellProxyFactory(chanelFactory);SellPerfumesellPerfume=(SellPerfume)Proxy.newProxyInstance(chanelFactory.getClass().getClassLoader(),chanelFactory.getClass().getInterfaces(),sellProxyFactory);sellPerfume.sellPerfume(1999.99);}}执行结果和静态代理的结果相同 , 但二者的思想是不一样的 , 一个是静态 , 一个是动态 。 那又如何体现出动态代理的优势呢?别急 , 往下看就知道了 。
注意看下图 , 相比静态代理的前置增强和后置增强 , 少了小红二字 , 实际上代理工厂分配的代理对象是随机的 , 不会针对某一个具体的代理对象 , 所以每次生成的代理对象都不一样 , 也就不确定是不是小红了 , 但是能够唯一确定的是 , 这个代理对象能和小红一样帮小明买到香水!
/**售卖红酒接口*/publicinterfaceSellWine{voidsellWine(doubleprice);}/**红酒供应商*/publicclassRedWineFactoryimplementsSellWine{@OverridepublicvoidsellWine(doubleprice){System.out.println("成功售卖一瓶红酒 , 价格:"+price+"元");}}然后我们的小明在请求代理工厂时 , 就可以实例化一个可以售卖红酒的代理了 。
publicclassXiaoMing{publicstaticvoidmain(String[]args){//实例化一个红酒销售商RedWineFactoryredWineFactory=newRedWineFactory();//实例化代理工厂 , 传入红酒销售商引用控制对其的访问SellProxyFactorysellProxyFactory=newSellProxyFactory(redWineFactory);//实例化代理对象 , 该对象可以代理售卖红酒SellWinesellWineProxy=(SellWine)Proxy.newProxyInstance(redWineFactory.getClass().getClassLoader(),redWineFactory.getClass().getInterfaces(),sellProxyFactory);//代理售卖红酒sellWineProxy.sellWine(1999.99);}}期待一下执行结果 , 你会很惊喜地发现 , 居然也能够代理售卖红酒了 , 但是我们没有修改代理工厂 。
如果感兴趣想深究的朋友 , 把注意力放在Proxy.newProxyInstance()这个方法上 , 这是整个JDK动态代理起飞的一个方法 。
讲到这里 , JDK提供的动态代理已经到尾声了 , 我们来总结一下JDK的动态代理:
(1)JDK动态代理的使用方法
代理工厂需要实现InvocationHandler接口 , 调用代理方法时会转向执行invoke()方法生成代理对象需要使用Proxy对象中的newProxyInstance()方法 , 返回对象可强转成传入的其中一个接口 , 然后调用接口方法即可实现代理(2)JDK动态代理的特点
目标对象强制需要实现一个接口 , 否则无法使用JDK动态代理(以下为扩展内容 , 如果不想看可跳过)
Proxy.newProxyInstance()是生成动态代理对象的关键 , 我们可来看看它里面到底干了些什么 , 我把重要的代码提取出来 , 一些对分析无用的代码就省略掉了 。
privatestaticfinalClass>[]constructorParams={InvocationHandler.class};publicstaticObjectnewProxyInstance(ClassLoaderloader,Class>[]interfaces,InvocationHandlerh){//获取代理类的Class对象Class>cl=getProxyClass0(loader,intfs);//获取代理对象的显示构造器 , 参数类型是InvocationHandlerfinalConstructor>cons=cl.getConstructor(constructorParams);//反射 , 通过构造器实例化动态代理对象returncons.newInstance(newObject[]{h});}我们看到第6行获取了一个动态代理对象 , 那么是如何生成的呢?接着往下看 。
privatestaticClass>getProxyClass0(ClassLoaderloader,Class>...interfaces){//去代理类对象缓存中获取代理类的Class对象returnproxyClassCache.get(loader,interfaces);}发现里面用到一个缓存proxyClassCache , 从结构来看类似于是一个map结构 , 根据类加载器loader和真实对象实现的接口interfaces查找是否有对应的Class对象 , 我们接着往下看get()方法 。
publicVget(Kkey,Pparameter){//先从缓存中查询是否能根据key和parameter查询到Class对象//...//生成一个代理类ObjectsubKey=Objects.requireNonNull(subKeyFactory.apply(key,parameter));}在get()方法中 , 如果没有从缓存中获取到Class对象 , 则需要利用subKeyFactory去实例化一个动态代理对象 , 而在Proxy类中包含一个ProxyClassFactory内部类 , 由它来创建一个动态代理类 , 所以我们接着去看ProxyClassFactory中的apply()方法 。
privatestaticfinalclassProxyClassFactoryimplementsBiFunction[],Class>>{//非常重要 , 这就是我们看到的动态代理的对象名前缀!privatestaticfinalStringproxyClassNamePrefix="$Proxy";@OverridepublicClass>apply(ClassLoaderloader,Class>[]interfaces){Map,Boolean>interfaceSet=newIdentityHashMap<>(interfaces.length);//一些状态校验//计数器 , 该计数器记录了当前已经实例化多少个代理对象longnum=nextUniqueNumber.getAndIncrement();//动态代理对象名拼接!包名+"$Proxy"+数字StringproxyName=proxyPkg+proxyClassNamePrefix+num;//生成字节码文件 , 返回一个字节数组byte[]proxyClassFile=ProxyGenerator.generateProxyClass(proxyName,interfaces,accessFlags);try{//利用字节码文件创建该字节码的Class类对象returndefineClass0(loader,proxyName,proxyClassFile,0,proxyClassFile.length);}catch(ClassFormatErrore){thrownewIllegalArgumentException(e.toString());}}}apply()方法中注意有两个非常重要的方法:
ProxyGenerator.generateProxyClass():它是生成字节码文件的方法 , 它返回了一个字节数组 , 字节码文件本质上就是一个字节数组 , 所以proxyClassFile数组就是一个字节码文件defineClass0():生成字节码文件的Class对象 , 它是一个native本地方法 , 调用操作系统底层的方法创建类对象而proxyName是代理对象的名字 , 我们可以看到它利用了proxyClassNamePrefix+计数器拼接成一个新的名字 。 所以在DEBUG时 , 停留在代理对象变量上 , 你会发现变量名是$Proxy0 。
CGLIB不仅能够为Java接口做代理 , 而且能够为普通的Java类做代理 , 而JDKProxy只能为实现了接口的Java类做代理 , 所以CGLIB为Java的代理做了很好的扩展 。 如果需要代理的类没有实现接口 , 可以选择Cglib作为实现动态代理的工具 。
废话太多 , 一句话概括:CGLIB可以代理没有实现接口的Java类
下面我们来学习它的使用方法 , 以小明找代理工厂买法国香水这个故事背景为例子 。
(1)导入依赖
cglibcglib-nodep3.3.0test还有另外一个CGLIB包 , 二者的区别是带有-nodep的依赖内部已经包括了ASM字节码框架的相关代码 , 无需额外依赖ASM
(2)CGLIB代理中有两个核心的类:MethodInterceptor接口和Enhancer类 , 前者是实现一个代理工厂的根接口 , 后者是创建动态代理对象的类 , 在这里我再贴一次故事的结构图 , 帮助你们理解 。
Objecto:被代理对象Methodmethod:被拦截的方法Object[]objects:被拦截方法的所有入参值MethodProxymethodProxy:方法代理 , 用于调用原始的方法对于methodProxy参数调用的方法 , 在其内部有两种选择:invoke()和invokeSuper() , 二者的区别不在本文展开说明
在getInstance()方法中 , 利用Enhancer类实例化代理对象(可以看作是小红)返回给调用者小明 , 即可完成代理操作 。
publicclassXiaoMing{publicstaticvoidmain(String[]args){SellProxyFactorysellProxyFactory=newSellProxyFactory();//获取一个代理实例SellPerfumeFactoryproxyInstance=(SellPerfumeFactory)sellProxyFactory.getProxyInstance(newSellPerfumeFactory());//创建代理类proxyInstance.sellPerfume(1999.99);}}我们关注点依旧放在可扩展性和可维护性上 , Cglib依旧符合开闭原则 , 如果小明需要小红代理购买红酒 , 该如何做呢?这里碍于篇幅原因 , 我不再将完整的代码贴出来了 , 可以自己试着手动实现一下 , 或者在心里有一个大概的实现思路即可 。
我们来总结一下CGLIB动态代理:
(1)CGLIB的使用方法:
代理工厂需要实现MethodInterceptor接口 , 并重写方法 , 内部关联真实对象 , 控制第三者对真实对象的访问;代理工厂内部暴露getInstance(ObjectrealObject)方法 , 用于从代理工厂中获取一个代理对象实例 。 Enhancer类用于从代理工厂中实例化一个代理对象 , 给调用者提供代理服务 。 JDKProxy和CGLIB的对比(2)仔细对比一下 , JDKProxy和CGLIB具有相似之处:
JDKProxyCGLIB代理工厂实现接口InvocationHandlerMethodInterceptor构造代理对象给Client服务ProxyEnhancer
二者都是用到了两个核心的类 , 它们也有不同:
最明显的不同:CGLIB可以代理大部分类(第二点说到);而JDKProxy仅能够代理实现了接口的类CGLIB采用动态创建被代理类的子类实现方法拦截 , 子类内部重写被拦截的方法 , 所以CGLIB不能代理被final关键字修饰的类和方法细心的读者会发现 , 讲的东西都是浅尝辄止(你都没有给我讲源码 , 水文实锤) , 动态代理的精髓在于程序在运行时动态生成代理类对象 , 拦截调用方法 , 在调用方法前后扩展额外的功能 , 而生成动态代理对象的原理就是反射机制
动态代理的实际应用传统的OOP编程符合从上往下的编码关系 , 却不符合从左往右的编码关系 , 如果你看不懂 , 可以参考下面的动图 , OOP满足我们一个方法一个方法从上往下地执行 , 但是却不能从左往右嵌入代码 , 而AOP的出现很好地弥补了这一点 , 它允许我们将重复的代码逻辑抽取出来形成一个单独的覆盖层 , 在执行代码时可以将该覆盖层毫无知觉的嵌入到原代码逻辑里面去 。
SpringAOP如下图所示 , method1和method2都需要在方法执行前后记录日志 , 实际上会有更多的方法需要记录日志 , 传统的OOP只能够让我们在每个方法前后手动记录日志 , 大量的Log.info存在于方法内部 , 导致代码阅读性下降 , 方法内部无法专注于自己的逻辑 。
AOP可以将这些重复性的代码包装到额外的一层 , 监听方法的执行 , 当方法被调用时 , 通用的日志记录层会拦截掉该方法 , 在该方法调用前后记录日志 , 这样可以让方法专注于自己的业务逻辑而无需关注其它不必要的信息 。
【代理?哦,就是那个赚差价中间商啊】每个有关数据库的操作都要保证一个事务内的所有操作 , 要么全部执行成功 , 要么全部执行失败 , 传统的事务失败回滚和成功提交是使用try...catch代码块完成的
SqlSessionsession=null;try{session=getSqlSessionFactory().openSession(false);session.update("...",newObject());//事务提交session.commit();}catch(Exceptione){//事务回滚session.rollback();throwe;}finally{//关闭事务session.close();}如果多个方法都需要写这一段逻辑非常冗余 , 所以Spring给我们封装了一个注解@Transactional , 使用它后 , 调用方法时会监视方法 , 如果方法上含有该注解 , 就会自动帮我们把数据库相关操作的代码包裹起来 , 最终形成类似于上面的一段代码原理 , 当然这里并不准确 , 只是给你们一个大概的总览 , 了解SpringAOP的本质在干什么 , 这篇文章讲解到这里 , 知识量应该也非常多了 , 好好消化上面的知识点 , 为后面的SpringAOP专题学习打下坚实的基础 。
推荐阅读
- 银河系|不用一万光年,银河系就是一座黑暗森林|近期科技趣评
- 糖尿病|肥胖的人才会出现糖尿病?血糖高就是糖尿病了?别被误导了
- 大火快炒即可,做法简单,春天的韭菜鲜嫩有味儿,还不容易长胖哦
- 一看就会的几道硬菜,学会了你就是大厨
- 网上超火的芝士爆浆紫薯饼,做法就是这么简单! 太好吃了
- 教你一周七天不重样,简单美味易上手的几道美食,最后有惊喜哦!
- 肾病|为什么肾病一发现就是晚期?
- 迷你香葱芝士火腿小方包,葱香浓郁,味道不错哦
- 空腹血糖|血糖超出这个值,就是糖尿病,血糖一直高,诱发五种并发症
- 花菜这样做,你吃过了吗?开胃又下饭,比吃肉还舒服哦
