1.静态代理:
- 目标:
- 在坦克对象已有的move业务中添加记录的功能,
- 记录坦克什么时候移动,什么时候结束
- 分析:
- 首先想到的可能会是继承,
- 但是继承耦合性太高了,
- 不同功能的实现需要多种子类单独实现,不利于扩展
- 有什么办法可以优化呢
- 代理的方式可以直接写在代理中,需要的时候新建代理,并把对象传过去。
- 首先想到的可能会是继承,
实现过程
- 代理和被代理对象都实现了Movable接口
- 在代理里边有一个被代理对象
- 代理有自己的move方法,加自己的逻辑进去,并调用被代理对象的move方法
如何实现多个代理的组合
- 直接嵌套,因为都是实现了Movable接口
动态代理:
- 静态只能代理Movable行为,我希望他可以代理各种各样的行为,怎么做?
- 代理类不是手写的,动态生成的。
实现过程
- 上代码,使用的时候看上去非常简单,但是解释起来非常麻烦
- 仍然定义Movable接口,被代理对象实现接口,实现move接口
- 使用Proxy类,他的包是java的reflect(反射:所谓的反射就是用二进制字节码分析类的属性和方法)
- 三个参数:
- ClassLoader:你要用哪个ClassLoader把将来的对象load到内存,跟代理对象同一个就可以了
- 接口数组:代理对象应该实现哪些接口?
- InvocationHandler:调用时候的处理器,被代理对象那个方法被调用的时候我们怎么处理?
- 三个参数:
- move最终调用invoke
- 结果
原理
- move方法经过一系列调用以后,调用了invoke方法
- 他是怎么调用的呢?
- 加一行代码试试
- 添加了一行代码,什么意思?
- jdk代理产生器里面有一个属性,叫保存生成文件,设置为true。
- 执行一下,生成了文件
- 这个就是中间动态生成的类,本来是看不到的,设置了刚才的属性,就会反编译生成出来
- 顶部的提示,说明这个文件是反编译出来的,是class文件类型
- 来看一下完整的代码
再一步一步分析这个类:
- 先看构造,LogHandler传进来,父类保存了
2. 他生成了move方法,实现了Movable接口,
- 为什么实现Movable接口,
- 因为调用动态生成的时候,第二个参数指定了要实现。
- 调用了父类h的invoke方法
- h是什么?
- 就是父类保存的传进来LogHandler,也就是第三个参数
- H就是传进去的第三个参数,最终调用的是他的invoke
-
invoke的参数代表什么
- 第一个参数:生成的代理对象,就是Movable m ;
- 第二个参数是调用的哪个方法
- 第三个是方法的参数
-
中间的那一行代码相当于调用了tank.move();(中间涉及到Asm——-下面会解释)
总结一下调用过程
- jdk动态代理全部过程:
- Client,相当于main所在的类,调用newProxyInstance()
- Proxy通过一系列操作生成了Proxy$0(下面细讲),
- 然后new了一个他的对象,并且将InvocationHandle作为参数传进去。
- 既然生成了,Client就可以调用他,调用move方法,在Proxy$0的move方法里面调用了invoke方法
- 实际调用的是invocationHandler的invoke方法
- 在move的前半截书写逻辑,后半截也可以书写逻辑,
- 然后调用的坦克的move方法
Asm
- Proxy$0.class是怎么生成的呢?
- 扒一下源码
- 最终add方法是由jdk的asm类库实现的:
- 所以想把动态代理弄明白的话,得把asm弄明白。
- Asm干嘛的?
- 他直接操纵二进制数据,
- class文件不是可以直接load到内存吗?
- 我们改class文件的话先要改源码,源码编译后改变
- 但是asm可以直接改二进制码,直接修改class文件。
- 用了asm以后,java才可以被称为动态语言。
- 所谓动态语言,就是运行时可以直接改变类里面的属性和方法
- 反射只能找出来,但是改不了。
- asm怎么操作二进制?
- 首先要明白二进制里面每一段代表都是什么?
- 这些东西每一个都是十六进制数,展开都是0101之类的,所以就是直接把0101的内容改掉。
- 改的时候需要知道哪个是方法名,哪个是返回值,哪个代表的类名,哪个是接口,直接把二进制码改了。
- 这个就是asm干的时候。
- 要想干这件事,首先得明白class类文件的结构是什么?(后续补充————————)
- 多数人都知道咖啡baby,任何class类开头都是这个,后面的内容有十种类型。
那现在好玩了,我不写java,直接通过编译器生成class文件,你就可以在jvm上运行,这就是scala、kotlin。所以JVM现在是一个大生态。
缺点
动态带的缺陷在于你那个类必须实现某个接口
扩展
还有比较牛逼的方法:
1.Instrument
- 本意是给琴调琴弦,java自带的
- 他是一个钩子函数,是一个拦截器,
- class加载到内存的过程中,他会拦截calss,
- 在拦截的时候,可以对class进行一些自己的定制。
- 相当于你可以直接修改二进制码
- 这个功能非常强大,比asm还要强大,他可以完全的控制二进制码
- Asm我们是通过jdk来用的,他api并不是说所有功能都能实现,新版本出来了api没跟上就实现不了,
- 但是这种方法是完全可以实现的
- 只是做的时候非常繁琐,你必须了解class文件里面每一个0,每一个1什么意思。
还有一个比较简单的
2.cglib
- 代码生成的lib
- 如果你用cglib生成代理的话,要比jdk的反射生成要简单很多
- tank不用实现任何借口,
Mian方法里面,enhance本意是增强
- 使用cglib生成代理需要添加依赖:
- cglib生成代理不需要接口?
- 那我怎么知道我在代理类里面实现了哪个方法呢?
- 从这里可以看出来,我们生成的动态代理类是Tank的一个子类,
- 在intercept方法被调用的时候,第一个参数跟jdk生成的参数是一样的。就是生成动态代理类的对象。
- 打印他的父类可以发现是Tank。
- 如果我们的Tank类是final 的,还能用cglib生成代理吗?
- 生成不了,所以cglib也有自己的缺陷,
- asm就可以,因为他直接操纵的二进制码,
- asm完全可以做到,
- 他将整个class文件读出来,里面是一堆方法一堆属性
- 然后它把这些方法属性读出来,可以重新写一个新的class文件,可以直接将final去掉
- cglib的底层也是用的asm
- Asm一般了解就可以了
对比
- 动态代理和静态代理的对比:
- 动态代理可以非常灵活地指定有多少种代理,哪种排第一位,哪种排第二位
- 比如现在有一个业务的逻辑都写好了,生产、库存、订单…
- 现在我有另外一个需求,我需要在所有操作上添加一个权限检查
- 现在怎么做?
- 一种是找到所有代码,里面都加上写一遍,
- 第二种是生成动态代理,相当于在一个切面植入
- 现在怎么做?
- 动态代理可以非常灵活地指定有多少种代理,哪种排第一位,哪种排第二位
补充:
- Android中常用的网络框架retrofit的核心就是动态代理:
- 参考视频:马士兵Java23种设计模式
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END