摘要
本文动态代理得意义, 主要介绍动态代理得实现原理以及由动态代理引申出来的一些知识点.
插曲
最近在研究 javamelody 实现的原理, 发现他对 JDBC 的监控就是通过动态代理实现的. 由于之前对于动态代理只是大概知道怎么回事, 没有细致的去研究, 所以上网百度了一下. 发现网上的东西要么注重原理而忽略应用场景导致空泛, 要么注重场景而忽略原理, 要么就是只有基于接口的动态代理而没有基于 cglib 的. 因此这里本文尽量做到大而全. 其实想总结一下的原因是公司进行代码 review 的时候, 老大提出同一个类中一个方法调用本类其他方法, 其他方法的事务不会生效, 本质上我是持怀疑态度的. 当时我是出于基于 Cglib 代理的角度考虑, 而实际不会生效是基于动态代理的方式, 采用 cglib 还是会生效, 后面会讲到. 本人作文比较推崇简约易懂的方式, 尽量避免过于斯文的名词出现.
一, 动态代理的意义
首先明白一点, 动态代理就是用来生成代理对象的. 我们知道传统的代理模式, 通常是先定义一个代理类, 该代理类需要持有目标对象 (也有叫被代理对象, 我觉得都行吧). 假设我们有 1000 个不同的目标对象 (这 1000 个对象不是同一个类), 那么我们需要预先定义 1000 个代理类, 这是我们不能容忍的. 于是乎, 动态代理就出现了, 它本质上是生成一个外表上和目标对象一样的代理对象, 然后当我们调用代理对象的方法的时候, 实际上它在他的方法里面去调用了目标对象对应的同名方法.
二, 动态代理设计的核心思想
其实不要把这些设计想得多么高尚, 假如我是动态代理设计的作者, 由动态代理的意义部分我们知道, 我们就是要想尽一切办法, 通过目标对象生成代理对象, 然后让代理对象的方法调用作用到目标对象的方法调用. 没错动态代理的核心思想就是这么简单. 比如目标类为 Person,Person 有一个方法叫做 purchase(), 此方法用于购物. 我们期望 purchase() 方法有代理类去做处理, 比如在购物前记录下购买了哪些东西. 我们知道在使用一个类之前, 是需要创建一个对象的, 我们就在创建的地方动手脚. 所以你看到了 JDK 动态 Proxy.newInstance() 的方式, 也领略过 Spring 的 Enhancer.create(). 个人比较喜欢 cglib 的优雅, 干净, 利落. 吐槽一下 JDK 的 InvocationHandler 像极了恶心的中间商. 下面是 JDK 动态代理 UML 示意图
三, JDK 动态代理
1, 原理
在了解动态代理之前, 我们需要了解 Java 字节码. 如果不熟悉 Java 字节码, 你可以理解为通过代码动态生成一个. java 文件, 然后将其编译为 class 文件加载到内存中. 接下来 JDK 中的动态代理要做的事情就是怎么去生成一个 ProxyPerson 字节码文件. 其实它就是在生成字节码的时候, 持有了 InvocationHandler 对象, 然后去实现了 ProxyPerson 对应的接口. 在该接口的所有实现方法中, 只做了一件事情就是调用 invocationHandler.invoke() 方法. 从代码层面来看如下所示:
- public class ProxyPerson implements Purchase{
- static{
- Method method;// 接口的方法
- Object[] args;// 接口参数
- }
- InvocationHandler handler;
- public ProxyPerson(InvocationHandler handler){
- this.handler = handler;
- }
- @overrde
- public purchage(){
- this,handler.invoke(this,method,args);
- }
- }
那么上面这段代码是在什么时候生成的呢?
Proxy.newProxyInstance()
在我们调用 JDK 上面的这个方法的时候, 底层就会去生成一个 ProxyPerson 字节码. 知道了原理我们来解答一下 JDK 动态代理为何只能基于接口代理而不能基于类呢?
1), 受限于字节码的生成方式, JDK 本身就是基于 InvocationHandler 去做的代理中转. 我们看到代理对象的方法调用于目标对象的调用没有半毛球关系, 调用目标对象是我们自己在 invoke 方法里面完成的.
2), 受限于同名的方法只能被向上转型成功的对象调用. 比如有两个类 Boy 与 Girl, 他们都实现了接口 Purchase, 如果我们先获取到 Girl 的 purchase() 方法 method, 我们通过 method.invoke(new Boy()) 这样必定会报错. 但是如果我们获取到 Purchase 接口 purchase() 方法 method, 我们通过 method.invoke(new Boy()) 这样是 ok 的, 因为 new Boy() 可以向上转型为 Purchase.
2, 应用
比如无论是传统的 MVC 模型还是 DDD 模型, 都离不开 Service. 我们知道 Service 方法使用 @Transactional 是可以开启事务控制的. 那么这种注解式事务是如何实现的呢? 其实在工程启动的时候, 我们就会有一个 Bean 的后置处理器去检查所有 Bean 一旦发现 Bean 的方法上有事务注解, 他就通过 Proxy.newInstance() 去创建一个代理对象, 将代理对象进行返回注入, 而抛弃原本应该注入到容器的对象. 所以我们看起来通过容器拿到的 Service 其实已经是代理对象了. 在调用目标对象前, 开启编程式事务即可.
四, cglib 动态代理
有了上面的知识, 我们要有对于 cglib 而言只是在生成字节码上面动手脚的觉悟. 下面直观感受与一下生成过程
- public static void main(final String[] args) {
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(Boy.class);
- enhancer.setCallback(new MethodInterceptor(){
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- System.out.println("proxy method"+ method.getName());
- if(method.getAnnotation(Transactional.class)!=null){
- System.out.println(method.getName()+"发现注解");
- }
- return methodProxy.invokeSuper(o,args);
- }
- });
- Boy proxy = (Boy) enhancer.create();
- proxy.test();
- }
- public static class Boy{
- public void run(){
- System.out.println("run...");
- }
- @Transactional
- public void walk(){
- System.out.println("walk...");
- }
- @Transactional
- public void test(){
- System.out.println("test...");
- walk();
- }
- }
可以看到 cglib 是基于继承的方式进行字节码动态生成. 它在子类的实现中, 只是调用了注入的 methodIntercptor.interceptor() 方法. 具体字节码实现细节, 这里不在深究. 我们在这里探讨一下, 为什么 cglib 可以使同一个 service 方法中的其他带有事务注解的事务生效? 因为基于继承的动态代理, 本质发起上调用的代理对象可以向上转型为原本的目标对象, 所以它可以直接通过代理对象去调目标对象方法.
posted on 2019-08-09 13:10 泥粑 阅读 (...) 评论 (...) 编辑 收藏
来源: https://www.cnblogs.com/enjoyall/p/11324671.html