浅谈JDK动态代理

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 浅谈JDK动态代理

“动态代理”四个字一出来,估计很多初学者已经开始冒冷汗。它之所以给人感觉很难,有三点原因:

  • 代码形式很诡异,让人搞不清调用逻辑
  • 用到了反射,而很多初学者不了解反射
  • 包含代理模式的思想,本身比较抽象

尽管动态代理看起来似乎有一定难度,但却必须拿下。因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理 + 责任链,环环相扣。所以说,搞编程的,拼到到最后还是看基本功,要么是语言基础、要么是计算机基础。

一个小需求:给原有方法添加日志打印

假设你刚进入一个项目组,项目中存在一个Calculator类,代表一个计算器,它可以进行加减乘除操作:

public class Calculator {
  // 加
  public int add(int a, int b) {
    int result = a + b;
    return result;
  }
  // 减
  public int subtract(int a, int b) {
    int result = a - b;
    return result;
  }
  // 乘法、除法...
}

现在老大给你提了一个需求:在每个方法执行前后打印日志。

你有什么好的方案?

方案一:直接修改

很多人最直观的想法是直接修改Calculator类:

public class Calculator {
  // 加
  public int add(int a, int b) {
    System.out.println("add方法开始...");
    int result = a + b;
    System.out.println("add方法结束...");
    return result;
  }
  // 减
  public int subtract(int a, int b) {
    System.out.println("subtract方法开始...");
    int result = a - b;
    System.out.println("subtract方法结束...");
    return result;
  }
  // 乘法、除法...
}

上面的方案是有问题的:

  • 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
  • 如果Calculator类内部有几十个、上百个方法,修改量太大
  • 存在重复代码(都是在核心代码前后打印日志)
  • 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了,于是你又要打开Calculator花十分钟删除日志打印的代码(或回滚分支)!

所以,此种方案PASS!

方案二:静态代理实现日志打印

“静态代理”四个字包含了两个概念:静态、代理。我们先来了解什么叫“代理”,至于何为“静态”,需要和“动态”对比着讲。

代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。

常用的代理方式可以粗分为:静态代理动态代理

静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。

是不是有点晕?是的,我最讨厌这种干巴巴的描述。简而言之,就是这样:

按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:

  • 将Calculator抽取为接口
  • 创建目标类CalculatorImpl实现Calculator
  • 创建代理类CalculatorProxy实现Calculator

抽取接口

/**
 * Calculator接口
 */
public interface Calculator {
  int add(int a, int b);
  int subtract(int a, int b);
}

原目标类实现接口

/**
 * 目标类,实现Calculator接口(如果一开始就面向接口编程,其实是不存在这一步的,CalculatorImpl原本就实现Calculator接口)
 */
public class CalculatorImpl implements Calculator {
  // 加
  public int add(int a, int b) {
    int result = a + b;
    return result;
  }
  // 减
  public int subtract(int a, int b) {
    int result = a - b;
    return result;
  }
  // 乘法、除法...
}

新增代理类并实现接口

/**
 * 静态代理类,实现Calculator接口
 */
public class CalculatorProxy implements Calculator {
    // 代理对象内部维护一个目标对象引用
  private Calculator target;
        
    // 通过构造方法,传入目标对象
  public CalculatorProxy(Calculator target) {
    this.target = target;
  }
    // 调用目标对象的add,并在前后打印日志
  @Override
  public int add(int a, int b) {
    System.out.println("add方法开始...");
    int result = target.add(a, b);
    System.out.println("add方法结束...");
    return result;
  }
    // 调用目标对象的subtract,并在前后打印日志
  @Override
  public int subtract(int a, int b) {
    System.out.println("subtract方法开始...");
    int result = target.subtract(a, b);
    System.out.println("subtract方法结束...");
    return result;
  }
  // 乘法、除法...
}

测试案例

使用代理对象完成加减乘除,并且打印日志:

public class Test {
  public static void main(String[] args) {
    // 把目标对象通过构造器塞入代理对象
    Calculator calculator = new CalculatorProxy(new CalculatorImpl());
    // 代理对象调用目标对象方法完成计算,并在前后打印日志
    calculator.add(1, 2);
    calculator.subtract(2, 1);
  }
}

静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了上一种方案4大缺点中的第1、4两点:

  • 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭(✅,如果一开始就面向接口编程,这一步其实是不需要的)
  • 如果Calculator类内部有几十个、上百个方法,修改量太大(❎,目标类有多少个方法,代理类就要重写多少个方法)
  • 存在重复代码(都是在核心代码前后打印日志)(❎,代理类中的日志代码是重复的)
  • 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了(✅,别用代理类就好了)

静态代理的问题

上面的代码中,为了给目标类做日志增强,我们编写了代理类,而且准备了一个构造器接收目标对象。代理代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!

如果现在我们系统需要全面改造,要给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类...

自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!

你细品上面加粗的这句话,是不是好像一句废话?没有类哪来的对象?!

其实我的意思是,能否让JVM根据接口自动生成代理对象呢?

比如,有没有一个方法,我传入接口+增强的代码(比如打印日志),它就给我自动返回代理对象呢?这样就能省去编写代理类这个无用的“中介”了,没有中间商赚差价,岂不爽哉?

仔细想一下,代理类或者代理对象重要吗?它其实只是个空壳,最重要的其实是 增强代码 + 目标对象。我们对代理对象的要求很低,只需要与目标对象拥有相同的方法即可。如此一来,别人调用proxy.add()得到的效果和调用target.add()是一样的,甚至因为两者都实现了相同接口,用接口类型接收后,calculator.add()根本分不出是代理还是原对象。

所以本质上,代理对象只要有方法申明即可,甚至不需要方法体,或者只要一个空的方法体即可,反正我们会把目标对象返回去。

那么,如何知道一个类有哪些方法信息呢?如果能得到类的方法信息,我们或许可以直接造一个代理对象。

有两个途径:

  • 目标类本身
  • 目标类实现的接口

这两个思路造就了两种不同的代理机制,一个被后人称为CGLib动态代理,另一个则被JDK收录,世人称之为JDK动态代理。本文重点介绍JDK动态代理。

我们先来验证一下,接口是否真的包含我们需要的方法信息:

public class ProxyTest {
    public static void main(String[] args) {
        /**
         * Calculator接口的Class对象
         * 得到Class对象的三种方式:
         * 1.Class.forName(xxx)
         * 2.xxx.class
         * 3.xxx.getClass()
         * 注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象
         */
        Class<Calculator> calculatorClazz = Calculator.class;
        //Calculator接口的构造器信息
        Constructor<?>[] calculatorClazzConstructors = calculatorClazz.getConstructors();
        //Calculator接口的方法信息
        Method[] calculatorClazzMethods = calculatorClazz.getMethods();
        //打印
        System.out.println("------接口Class的构造器信息------");
        printClassInfo(calculatorClazzConstructors);
        System.out.println("\n");
        System.out.println("------接口Class的方法信息------");
        printClassInfo(calculatorClazzMethods);
        System.out.println("\n");
    /**
     * Calculator实现类的Class对象
     */
    Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
        //Calculator实现类的构造器信息
        Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
        //Calculator实现类的方法信息
        Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
        //打印
        System.out.println("------实现类Class的构造器信息------");
        printClassInfo(calculatorImplClazzConstructors);
        System.out.println("\n");
        System.out.println("------实现类Class的方法信息------");
        printClassInfo(calculatorImplClazzMethods);
    }
    public static void printClassInfo(Executable[] targets) {
        for (Executable target : targets) {
            // 构造器/方法名称
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括号
            sBuilder.append('(');
            Class<?>[] clazzParams = target.getParameterTypes();
            // 拼接参数
            for (Class<?> clazzParam : clazzParams) {
                sBuilder.append(clazzParam.getName()).append(',');
            }
            //删除最后一个参数的逗号
            if (clazzParams.length != 0) {
                sBuilder.deleteCharAt(sBuilder.length() - 1);
            }
            //拼接右括号
            sBuilder.append(')');
            //打印 构造器/方法
            System.out.println(sBuilder.toString());
        }
    }
}

得到以下结论:

  • 接口Class对象没有构造方法,所以Calculator接口不能直接new对象
  • 实现类Class对象有构造方法,所以CalculatorImpl实现类可以new对象
  • 接口Class对象有两个方法add()、subtract()
  • 实现类Class对象除了add()、subtract(),还有从Object继承的方法

也就是说,接口Class的对象和实现类的Class对象除了构造器,其他信息基本相似(目标类由于继承了Object,所以内部包含了Object的方法,与代理无关)。

至此,我们至少知道从接口获取方法信息是可能的!接下来的努力方向就是:怎么根据一个接口得到代理对象。

引入JDK动态代理

通过刚才的实验,我们不仅知道了接口确实包含我们所需要的方法信息,还知道了接口为什么不能直接new对象:接口缺少构造器信息。那么,是否存在一种机制,能给接口安装上构造器呢?或者,不改变接口本身,直接拷贝接口的信息到另一个Class,然后给那个Class装上构造器呢?

很显然,不论是从开闭原则还是常规设计考虑,直接修改接口Class的做法相对来说不是很合理。JDK选择了后者:拷贝接口Class的信息,产生一个新的Class对象。

也就是说,JDK动态代理的本质是:用Class造Class,即用接口Class造出一个代理类Class。

具体API就不带大家找了,直接看:

 

Proxy.getProxyClass():返回代理类的Class对象。

也就说,只要传入接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?

没错,好家伙,直接跳过了代理类的编写!

public class ProxyTest {
    public static void main(String[] args) {
        /*
         * 参数1:Calculator的类加载器(当初把Calculator加载进内存的类加载器)
         * 参数2:代理对象需要和目标对象实现相同接口Calculator
         * */
        Class<?> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
        //以Calculator实现类的Class对象作对比,看看代理Class是什么类型
        System.out.println(CalculatorImpl.class.getName());
        System.out.println(calculatorProxyClazz.getName());
        //打印代理Class对象的构造器
        Constructor<?>[] constructors = calculatorProxyClazz.getConstructors();
        System.out.println("----构造器----");
        printClassInfo(constructors);
        System.out.println("\n");
        //打印代理Class对象的方法
        Method[] methods = calculatorProxyClazz.getMethods();
        System.out.println("----方法----");
        printClassInfo(methods);
        System.out.println("\n");
    }
    public static void printClassInfo(Executable[] targets) {
        for (Executable target : targets) {
            // 构造器/方法名称
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括号
            sBuilder.append('(');
            Class<?>[] clazzParams = target.getParameterTypes();
            // 拼接参数
            for (Class<?> clazzParam : clazzParams) {
                sBuilder.append(clazzParam.getName()).append(',');
            }
            //删除最后一个参数的逗号
            if (clazzParams.length != 0) {
                sBuilder.deleteCharAt(sBuilder.length() - 1);
            }
            //拼接右括号
            sBuilder.append(')');
            //打印 构造器/方法
            System.out.println(sBuilder.toString());
        }
    }
}

 

大家还记得原先接口Class的打印信息吗?

 

没错,Proxy.getProxyClass()返回的Class对象是有构造器的!

开头说了,动态代理的使命有两个:

  • 自动生成代理对象,让程序员免受编写代理类的痛苦
  • 将增强代码与代理类(代理对象)解耦,从而达到代码复用

现在我们已经得到了代理Class,只需通过反射即可得到代理对象。

动态代理的使命有两个:

  • 自动生成代理对象,让程序员免受编写代理类的痛苦
  • 将增强代码与代理类(代理对象)解耦,从而达到代码复用

JDK动态代理的底层逻辑

接下来,我们打算通过代理Class对象得到代理对象。

仔细观察之前的打印信息:

代理Class有一个构造器,需要传入InvocationHandler,虽然我们不知道这是啥,但可以试着传一下(JDK源码自带InvocationHandler):

public class ProxyTest {
    public static void main(String[] args) throws Exception {
        /*
         * 参数1:类加载器,随便给一个
         * 参数2:需要生成代理Class的接口,比如Calculator
         * */
        Class<?> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
        // 得到唯一的有参构造 $Proxy(InvocationHandler h),和反射的Method有点像,可以理解为得到对应的构造器执行器
        Constructor<?> constructor = calculatorProxyClazz.getConstructor(InvocationHandler.class);
        // 用构造器执行器执行构造方法,得到代理对象。构造器需要InvocationHandler入参
        Calculator calculatorProxyImpl = (Calculator) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return 10086;
            }
        });
        // 看,有同名方法!
        System.out.println(calculatorProxyImpl.add(1, 2));
    }
}

完美,我们顺利得到了梦寐以求的代理对象。

但,InvocationHandler是干嘛的呢?从实验结果看,会发现每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法:

怎么做到的呢?

上面不是说了吗,根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()!也就是说,动态代理为了实现代理对象和增强代码的解耦,把增强代码也抽取出去了,让InvocationHandler作为它与目标对象的桥梁。

JDK动态代理最终生成的其实是Class<Proxy>,最终代理对象是proxy对象,而且实现了Calculator接口。

注意,静态代理的做法是把目标对象传入代理对象,而动态代理则把增强代码传入代理对象。那么,目标对象怎么办,这样一来虽然能执行增强代码,但执行不到目标方法了!

别慌!来看看invoke()方法的参数有哪些:

  • Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归,一般不会使用)
  • Method method:方法执行器,用来执行方法(有点不好解释,Method只是一个执行器,传入目标对象就执行目标对象的方法)
  • Obeject[] args:方法参数

为什么参数中没有目标对象呢?其实用大腿想想就能明白:JDK要也懵逼呀,你就传给我一个接口,我给你整出这么一堆东西已经仁至义尽,况且一个接口可以被多个类实现,我哪知道你将来要用这个InvocationHandler给哪个目标对象增强呀!所以JDK怎么可能提前预留目标对象的参数类型呢?

算了,知足吧,好歹有Method代理方法和args参数了,至于目标对象,咱自己解决吧:

至此,我们初步实现动态代理。

如何复用增强代码

抽取方法,简化操作

上面这种方式,太low了,不忍直视...改进一下:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
        // 传入目标对象
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
    }
    /**
     * 传入目标对象,获取代理对象
     *
     * @param target
     * @return
     * @throws Exception
     */
    private static Object getProxy(final Object target) throws Exception {
        // 参数1:随便找个类加载器给它 参数2:需要代理的接口
        Class<?> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        return constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        });
    }
}

改进后的代码可读性大大增强,简单明了。

解耦代理对象与增强代码

上面的代码还有问题:虽然传入任意对象我们都可以返回增强后的代理对象,但增强代码是写死的。如果我需要的增强不是打印日志而是其他操作呢?难道重新写一个getProxy()方法吗?所以,我们应该抽取InvocationHander,将增强代码和代理对象解耦(其实重写getProxy()和抽取InvocationHander本质相同,但后者细粒度小一些)。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入目标对象+增强代码,得到代理对象
        Calculator calculatorProxy = (Calculator) getProxy(target, logInvocationHandler);
        calculatorProxy.add(1, 2);
    }
    /**
     * 传入目标对象+增强代码,获取代理对象
     *
     * @param target
     * @param handler
     * @return
     * @throws Exception
     */
    private static Object getProxy(final Object target, final InvocationHandler handler) throws Exception {
        // 参数1:随便找个类加载器给它 参数2:需要代理的接口
        Class<?> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        return constructor.newInstance(handler);
    }
    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

优化代码语义

上面的代码抽取了两个方法,仔细观察你会发现,getLogInvocationHandler(target)的target参数是必要的,但getProxy(target, invocationHandler)的target参数是没必要的:

  • 首先,getProxy()其实只需要知道要实现的接口是什么,就能返回该接口的代理对象不是吗?
  • 其次,invocationHandler已经包含目标对象

所以,代码最后可以优化成这样:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入接口+增强对象(含目标对象),得到代理对象
        Calculator calculatorProxy = (Calculator) getProxy(
                logInvocationHandler,                 // 增强对象(包含 目标对象 + 增强代码)
                target.getClass().getClassLoader(),   // 随便传入一个类加载器
                target.getClass().getInterfaces()     // 需要代理的接口
        );
        calculatorProxy.add(1, 2);
    }
    /**
     * 传入接口+增强(已经包含了目标对象),获取代理对象
     *
     * @param handler
     * @param classLoader
     * @param interfaces
     * @return
     * @throws Exception
     */
    private static Object getProxy(final InvocationHandler handler, final ClassLoader classLoader, final Class<?>... interfaces) throws Exception {
        // 参数1:随便找个类加载器给它 参数2:需要代理的接口
        Class<?> proxyClazz = Proxy.getProxyClass(classLoader, interfaces);
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        return constructor.newInstance(handler);
    }
    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

更好用的API:Proxy.newProxyInstance()

目前为止,我们学习都是Proxy.getProxyClass():

  • 先获得proxyClazz
  • 再根据proxyClazz.getConstructor()获取构造器
  • 最后constructor.newInstance()生成代理对象

为了简化代码,我们把这三步封装为getProxy()方法。然而,其实JDK已经提供了一步到位的方法Proxy.newProxyInstance(),你会发现它的参数和我们上面最终封装的getProxy()是一样的:

这说明什么?这说明我们的封装思路已经赶上JDK那帮老秃驴了!

废话不多说,把getProxy()删了,直接用Proxy.newProxyInstance()。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
        Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),   // 随便传入一个类加载器
                target.getClass().getInterfaces(),    // 需要代理的接口
                logInvocationHandler                  // 增强对象(包含 目标对象 + 增强代码)
        );
        calculatorProxy.add(1, 2);
    }
    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

完美。

至此,我们又完成了动态代理的第二个目标:代码复用。不仅目标对象与增强代码解耦,代理对象也和增强代码解耦了。

探究代理Class和代理对象

如果你只是想学会动态代理,上面的内容足够了。但我相信,对于JDK生成的代理Class对象和最终生成的代理对象,大家都有点云里雾里,想刨根问底。

所以,这一小节我们一起来聊聊Proxy。

Proxy.getProxyClass()会先克隆接口信息得到新的Class对象,然后进行后续的一系列处理。

最终得到的Class对象其实是Class<Proxy>,也就意味着代理对象是Proxy的实例:

部分同学可能会对实例类型的判断有点懵,这里再举一个例子:

myProxy既是AnyClass类型,又是Collection类型。extends相当于亲爹,只能有一个,而implements相当于认干爹,可以多个。但不论是哪种形式,都是对象的爹呀!

JDK动态代理小结

我想了个很骚的比喻,希望能解释清楚:

接口Class对象是大内太监,里面的方法和字段比做他的一身武艺,但是他没有XDD(构造器),所以不能new实例,一身武艺后继无人。

那怎么办呢?

正常途径(静态代理):

写一个类,实现该接口。这个就相当于大街上拉了一个人,认他做干爹。一身武艺传给他,只是比他爹多了XDD,可以new实例。只要再传入目标对象,就能得到增强后的代理对象。

非正常途径(动态代理):

通过妙手圣医Proxy的克隆大法(Proxy.getProxyClass()),克隆一个Class,但是有小DD。所以这个克隆人Class可以创建实例,也就是代理对象。代理Class其实就是附有构造器的接口Class,一样的类结构信息,却能创建实例。

JDK根据接口生成的其实是Proxy的Class对象,然后根据ProxyClass得到proxy代理对象。proxy代理对象实现了接口,同时也是Proxy类型的。proxy对象的原理是:内部维护一个InvocationHandler,而InvocationHandler是对增强代码的抽象。通过抽取InvocationHandler,将代理对象和增强代码解耦。

但此时代理对象内部只有InvocationHandler,没有目标对象,如何最终调用目标方法呢?只需要把目标对象传给InvocationHandler就好啦。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        // 1.得到目标对象
        CalculatorImpl target = new CalculatorImpl();
        // 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
        InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
        // 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
        Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),   // 随便传入一个类加载器
                target.getClass().getInterfaces(),    // 需要代理的接口
                logInvocationHandler                  // 增强对象(包含 目标对象 + 增强代码)
        );
        calculatorProxy.add(1, 2);
    }
    /**
     * 日志增强代码
     *
     * @param target
     * @return
     */
    private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        };
    }
}

其实就是调用链路拉长了,原本代理对象直接调用目标对象,现在是代理对象调InvocationHandler,InvocationHandler再调目标对象。Proxy代理对象内部有InvocationHandler对象,而InvocationHandler对象内部有我们塞进去的目标对象,所以最终通过代理对象可以调用到目标对象,并且得到了增强。所以,代理模式就是俄罗斯套娃...

最后我们还对代码做了优化,当然有些人可能还是觉得第一版好,哈哈。

 

 

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
11月前
|
缓存 Java 数据库连接
分析JDK动态代理的实现
分析JDK动态代理的实现
65 0
|
3月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
5天前
|
设计模式 Java API
[Java]静态代理与动态代理(基于JDK1.8)
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
[Java]静态代理与动态代理(基于JDK1.8)
|
16天前
|
Java
【编程进阶知识】静态代理、JDK动态代理及Cglib动态代理各自存在的缺点及代码示例
本文介绍了三种Java代理模式:静态代理、JDK动态代理和Cglib动态代理。静态代理针对特定接口或对象,需手动编码实现;JDK动态代理通过反射机制实现,适用于所有接口;Cglib动态代理则基于字节码技术,无需接口支持,但需引入外部库。每种方法各有优缺点,选择时应根据具体需求考虑。
16 1
|
3月前
|
Java API 开发者
Jdk动态代理为啥不能代理Class?
该文章主要介绍了JDK动态代理的原理以及为何JDK动态代理不能代理Class。
Jdk动态代理为啥不能代理Class?
|
3月前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
159 0
|
3月前
|
设计模式 Java C++
揭秘!JDK动态代理VS CGLIB:一场关于Java代理界的‘宫心计’,你站哪队?
【8月更文挑战第24天】Java 动态代理是一种设计模式,允许在不改动原类的基础上通过代理类扩展功能。主要实现方式包括 JDK 动态代理和 CGLIB。前者基于接口,利用反射机制在运行时创建代理类;后者采用继承方式并通过字节码技术生成子类实现类的代理。两者在实现机制、性能及适用场景上有明显差异。JDK 动态代理适用于有接口的场景,而 CGLIB 更适合代理未实现接口的类,尽管性能更优但存在一些限制。开发者可根据需求选择合适的代理方式。
154 0
|
5月前
|
设计模式 Java 程序员
java动态代理(JDK和cglib)
java动态代理(JDK和cglib)
30 0
|
5月前
|
设计模式 Java 程序员
java动态代理(JDK和cglib)
java动态代理(JDK和cglib)
40 0
|
5月前
|
缓存 Java Maven
JDK 动态代理
JDK 动态代理
27 0