JDK动态代理原理

引用

整理自阅读bravo的java小册专栏《JDK动态代理》笔记:

  • https://www.yuque.com/bravo1988/primary/admic4

从一个需求引入:为方法增加打印日志的功能

直接修改源代码的缺点

  1. 符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
  2. 修改量太大,当有多个类及方法时
  3. 存在重复代码
  4. 日志打印硬编码在代理类中,不利于后期维护(当需要撤销功能时)

静态代理实现

  • 解决缺点1,4
  • 静态代理则把日志代码抽取出来,放在代理类中,解决了增强代码和目标类的耦合,但又造成了增强代码和代理类的耦合,所以代理类无法复用,有多少个目标类就要写多少个代理类
  • 实现一个代理类,实现和目标类一样的接口。通过构造方法将目标类对象作为参数传入到代理类中,即在代理类的内部维护一个目标对象引用。现在可以重写增强方法,在增强方法里面只写增强代码,其它部分可以调用目标类的方法。

mark: 我们真正想要的其实不是代理类,而是最后的代理对象。

动态代理的实现

相比静态代理

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

复习知识,类对象以及类是如何创建的

  • Class对象只能由JVM创建。虽然不能new,但Java还是提供了其他方式让我们得到Class对象,底层会告诉JVM帮我们创建 a. Class.forName(xxx):Class clazz = Class.forName("com.bravo.Person"); b. xxx.class:Class clazz = Person.class; c. xxx.getClass():Class clazz = person.getClass(); 这3种方式都要求先有类,但我们并不想编写代理类

思考:如何自动生成代理对象? 要得到一个对象,首先要得到对应的Class对象(我们new一个对象,背后是JVM使用改类的Class对象的相应方法给我们创建并返回) 我们想不写代理类,直接得到Class对象,该如何做到呢? 上面复习类的创建方法,都需要先存在类。我们并不想写代理类,因为我们想要的至少代理类对象。该怎么做? 从接口入手。 回归本质,我们想要的是:增强代码+目标对象;代理类,或者代理对象,都只是实现的手段 所以现在,我们的思路通过接口来创建代理对象。

无论如何,要创建对象,需要类(接口实际是不能直接创建的)。如何获得类信息呢?

  • 目标类本身
  • 目标类实现的接口 这两种思路造成了两种不同的代理思路,一个被后人称为CGLib动态代理,另一个则被JDK收录,世人称为JDK动态代理。下面我们继续按照我们的思路,介绍JDK动态代理的实现。 接口Class对象和实现类的Class对象,除了实现类Class对象拥有构造器(以及从Object继承的方法),其它信息基本相似    

根据目前掌握的信息,引入JDK动态代理,只要给接口Class对象提供构造器,似乎就能解决了? 直接修改接口Class对象不符合开闭原则,JDK选择拷贝接口信息到另一个Class,并由那个Class提供构造器。是的,这就Proxy类。

       

Proxy.getProxyClass():返回代理类的Class对象。只要传入接口Class对象,getProxyClass( )方法即可返回代理对象,而不用实际编写代理类,即下图所示:    

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对象。这意味我们只需要通过反射,即可得到代理对象。 对比代理Classd对象和原来接口Class对象的信息:

 

现在我们有了构造器了,但似乎我们还没解决怎么“增强”这件事? 查看我们创建的代理类Class对象拥有构造器: 可以看到

  • com.sun.proxy.$Proxy0(java.reflect.InvocationHandler),它需要一个 InvocationHandler 参数。

  • 根据代理Class(Proxy类)的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()

    • invocationHandler的invoke方法根据方法名,调用目标对象对应的原方法,并且支持我们在上下文做增强。

所以,现在的关键先生是 invoke() 这个方法。让我们看看它: invoke()方法的参数有哪些:

  • Object proxy:是代理对象本身,而不是目标对象(不要调用,会无限递归,一般不会使用)
  • Method method:方法执行器
  • Obeject[] args:方法参数

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

  • 好歹有Method代理方法和args参数了,至于目标对象,自己解决试试:

至此,我们自己初步实现了动态代理。回答了上面如何 “增强”,以及JDK如何通过接口实现动态代理的机制。

再进一步,这样我们的增强代码是写死的,如何解耦代理对象与增强代码呢?

  • 想一下,我们初步实现的动态代理主要有两步:

    • 通过Proxy.getProxyClass(接口类加载器,接口类对象)得到代理类Class对象(有构造器方法)
    • 用其构造器方法newInstance(new InvocationHandle.class) 构造代理类对象。增强逻辑通过InvocationHandle实现
  • 我们把目标类抽取成参数,把增强代码抽取成,不就可以了吗:

    • getLogInvocationHandler(target)的target参数(即目标类对象)是必要的,但getProxt(target, invocationHandler)的target参数是没必要的。因为invocationHandler已经包含目标对象。另外,getProxy()其实只需要知道要实现的接口是什么,就能返回该接口的代理对象

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;
        }
    };
}

}


很完美了目前。让我们看看官方API:Proxy.newProxyInstance()
目前为止,我们学习都是Proxy.getProxyClass():
- 先获得proxyClazz
- 再根据proxyClazz.getConstructor()获取构造器
- 最后constructor.newInstance()生成代理对象
为了简化代码,我们把这三步封装为getProxy()方法。然而,其实JDK已经提供了一步到位的方法Proxy.newProxyInstance(),你会发现它的参数和我们上面最终封装的getProxy()是一样的
![](https://images.jquan.ink/images/blog-image/blogarticle/clipboard_20220102_122522.png)

```java
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;
            }
        };
    }
}

再看一个例子

public class ProxyTest {

    public static void main(String[] args) {

        //目标对象
        final Target target = new Target();

        //增强对象
        final Advice advice = new Advice();

        //返回值 就是动态生成的代理对象
        TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
                target.getClass().getClassLoader(), //目标对象类加载器
                target.getClass().getInterfaces(), //目标对象相同的接口字节码对象数组
                new InvocationHandler() {
                    //调用代理对象的任何方法  实质执行的都是invoke方法
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        advice.before(); //前置增强
                        Object invoke = method.invoke(target, args);//执行目标方法
                        advice.afterReturning(); //后置增强
                        return invoke;
                    }
                }
        );

        //调用代理对象的方法
        proxy.save();

    }

}

end
  • 作者:(联系作者)
  • 发表时间:2022-01-02 13:20
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  • 评论




    1 / 0