深入探究Java动态代理

深入探究Java动态代理

提起Java的动态代理,大家首先就会想到Spring的AOP,Spring在实现AOP的时候,确实有时候是使用Java动态代理机制来实现的,当使用的接口来生成Bean,并且开启了AOP之后,使用的就是动态代理来实现的AOP

1. Spring实现AOP

创建一个项目,并配置SpringAOP:

Bean:

package com.aaa;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-8;
 * Otherwise, Inter don't know who created it either
 */
public class Foo implements Inter {
    @Override
    public void fun(String name) {
        System.out.println(name);
    }
}

接口:

package com.aaa;

public interface Inter {
    void fun(String name);
}

前置通知类:

public class AS {
    public void befor(){
        System.out.println("before");
    }
}

在Spring的配置文件中配置Bean和AOP

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
     http://www.springframework.org/schema/aop 
     http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">


    <bean class="com.aaa.Foo" id="foo"/>
    <bean class="com.aaa.AS" id="as"/>

    <aop:config>
        <!-- 定义切面 -->
        <aop:aspect id="aspect1" ref="as">
            <aop:pointcut id="pt" expression="execution(* com.aaa.*.*(..))"/>
            <aop:before method="befor" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>
</beans>

最后来一个Main函数来测试运行

package com.aaa;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                "spring-config.xml");

        Inter i = (Inter) context.getBean("foo");
        i.fun("input");
    }
}

运行结果:


image

可以看到我们切点已经正常执行了,那么这个时候我们来看一下生成的Inter对象的类型,修改代码如下:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                "spring-config.xml");

        Inter i = (Inter) context.getBean("foo");
        i.fun("input");

        System.out.println("-------------------");

        System.out.println(i.getClass().getName());
    }
}

运行结果:

image

我们看到,实际上Spring为我们生成的对象并不是我们写的Foo类,当我们使用Spring并配置了AOP的时候,Spring就会为我们生成代理类型,这个com.sun.proxy.$Proxy2,就是JDK动态代理所生成的类型

2. JDK动态代理

那么就用JVM动态代理来实现一下切面的功能,保持Foo不变,先看代码:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-12;
 * Otherwise, I don't know who created it either
 */
public class InvocationHandlerImpl implements InvocationHandler {

    private Object target;

    private InvocationHandlerImpl(Object target) {
        this.target = target;
    }

    public static Object getProxy(Object target){
        InvocationHandlerImpl invocationHandler = new InvocationHandlerImpl(target);
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                invocationHandler);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Object result = method.invoke(target, args);
        System.out.println("after");
        return result;
    }
}

然后是Main方法,运行一下,看看效果:

public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Inter proxy = (Inter) InvocationHandlerImpl.getProxy(foo);
        proxy.fun("input");

        System.out.println("------------------");
        System.out.println(proxy.getClass().getName());
    }
}

运行效果:


image

发现和刚刚Sping实现的效果是一样的

3. 动态代理详解

要实现JDK动态代理,我们需要两个关键性的类:

  1. import java.lang.reflect.Proxy
  2. import java.lang.reflect.InvocationHandler

我们一个一个的详细来说明:

Proxy

Proxy就是我们生成动态代理使用的类,在需要生成代理对象的时候,我们需要以下几个东西:

  1. classLoader
  2. 接口
  3. invocationHandler对象

classLoader就不需要多说了,大多数 情况下,都可以从目标类获得
第二个参数是你要生成的代理对象所实现的接口类型,这个参数是一个Class类型的数组,如果你不想手动设置进去也可以,只要放入接口类型.class进去就好了,我们生成的代理对象就会去实现这个参数中的接口
第三个参数是一个InvocationHandler对象,也就是我们这个类的对象

InvocationHandler

这个接口可以认为是代理类型与原始类型之间的桥梁因为是动态代理,如何理解这个动态,对应到上面的代码就是InvocationHandlerImpl这个类写好了之后,无论传入什么对象,都能够给他生成对应的代理对象,而这些代理对象又会在调用原始对象的方法(即fun()方法)的时候都能够统一的执行AOP方法,那么为了实现这个目标,就需要有这么一个方法,就是invoke方法,无论调用代理对象的什么方法,都会执行一次invoke方法,这样,我们就可以在这个方法中加入前置通知,和后置通知了,
看这个方法中的代码:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("before");
    Object result = method.invoke(target, args);
    System.out.println("after");
    return result;
}

这里有3个参数,第一个参数proxy是代理对象,我们几乎是用不上它的,甚至可以说完全用不上它。。。
第二个参数是一个Method对象,用过反射的同学应当对这个对象不会感觉陌生,它就是要执行的目标方法,当我们调用代理对象的中的任何方法,都会把这个方法作为method参数传入invoke方法中
而最后一个参数就是这个方法的实参们了
那么这个方法的返回值,就是目标方法的返回值啦
在invoke方法中我们就可以决定我们的逻辑了:
输出的befor就相当于前置通知,在调用目标方法前执行的代码,在这里我们有机会去对参数进行修改
method.invoke就是真正的去执行目标方法,返回值就是result
最后的after就相当与后置通知,在这个里,我们有机会去修改目标方法的返回值
最后将返回值返回给调用方
整个调用过程的流程图如下:

image

4. 深入探究代理类

4.1 代理类的代码

其实写到这我们就已经可以使用JDK的动态代理了,但是有一个小缺陷,我们根本就不知道JDK给我们生成的代理类是什么样子的,所以我们需要想办法知道这个代理类究竟是什么样子的

动态代理类的具体实现在ProxyGenerator.java这个类中,我们要想看到生成的代理类究竟是个什么样子的,就需要使用这个类,使用ProxyGenerator手动生成一个代理类的字节码:

import sun.misc.ProxyGenerator;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-14;
 * Otherwise, I don't know who created it either
 */
public class Generator {
    public static void generator(Object target){
        byte[]  b= ProxyGenerator.generateProxyClass(target.getClass().getSimpleName(),target.getClass().getInterfaces());
        try (FileOutputStream out = new FileOutputStream("./"+target.getClass().getSimpleName()+".class")){
            out.write(b);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

我们将生成传入对象的代理类,并且将它存在项目的根路径下,文件名就是代理类的名字,运行:

public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Generator.generator(foo);
    }
}
image

反编译字节码,让我们看看它里面究竟写了什么,当然我用的开发工具是idea,双击打开看到的就是反编译之后的java代码了。。。。

反编译之后的结果:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import com.aaa.Inter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class Foo extends Proxy implements Inter {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public Foo(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void fun(String var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.aaa.Inter").getMethod("fun", Class.forName("java.lang.String"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}


一点一点的看,首先这个代理继承了Proxy实现了Inter接口,这个接口就是我们在生成代理类时传递进去的接口,然后这个代理类有4个Method类型的全局变量,这4个全局变量就是我们这个类中的各个方法,在最下方的静态代码块中进行赋值的,equals,fun和toString方法是默认会重写的方法,fun方法是我们从接口那继承来的,这些方法的逻辑都是一样的,本质上就一行代码:

super.h.invoke(this, m3, new Object[]{var1});

就是调用父类中h对象的invoke方法,去到他的父类中,发现这个如下代码:

/**
 * the invocation handler for this proxy instance.
 * @serial
 */
protected InvocationHandler h;

其实就是我们自己写的InvocationHandler,那么也就是说这个代理类啥也不干,就是在从接口那拿来的方法中去调用invocation方法,在这里我们能清晰的看到invoke方法的第一个参数其实就是这个代理类自己

4.2 invoke方法的第一个参数

实际上,我们绝大多数情况下都用不到这个对象,但是如果我们使用链式编程的话,就有可能需要用到这个对象了,例如我们的接口是这样的:

public interface Inter {
    void fun(String name);
    
    //链式编程示例
    Inter step1();
    Inter step2();
}

Foo类实现Inter接口,是这样的:

public class Foo implements Inter {

    @Override
    public void fun(String name) {
        System.out.println(name);
    }

    @Override
    public Inter step1() {
        System.out.println("第一步");
        return this;
    }

    @Override
    public Inter step2() {
        System.out.println("第二步");
        return this;
    }
}

保持我们的InvocationHandlerImpl不变,来调用一下代理对象看看会发生什么:

public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Inter inter = (Inter) InvocationHandlerImpl.getProxy(foo);
        inter.step1().step2();
    }
}
image

发现结果只有第一个方法能执行我们的before和after,后面的都不生效了,这是因为在invoke方法中我们返回的对象是执行目标类的目标方法的返回值,在执行第一次之后就被替换成了目标对象了,再也不是代理类型了,如果我们想让我们的动态代理正确的被执行,就需要返回代理类型的对象:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("before");
    Object result = method.invoke(target, args);
    Class<?>[] interfaces = result.getClass().getInterfaces();
    System.out.println("after");
    if (result == target){
        return proxy;
    }
    return result;
}

重点是返回值前的判断,result==target其实就是判断这个方法是否返回的是this,如果返回的是this,就证明这里需要链式编程,这个方法就需要返回代理类型的对象了,运行结果如下:

image

可以看到两个方法都能正常执行before和after

题外话

在调查invoke方法的第一个参数究竟有什么用的时候,看到了stackoverflow上的一篇帖子:这里面的意思与我上述表示的差不多,但是它的示例代码是这样写的:

接口:

private interface Account {
    public Account deposit (double value);
    public double getBalance ();
}

handler:

private class ExampleInvocationHandler implements InvocationHandler {

    private double balance;

    @Override
    public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {

        // simplified method checks, would need to check the parameter count and types too
        if ("deposit".equals(method.getName())) {
            Double value = (Double) args[0];
            System.out.println("deposit: " + value);
            balance += value;
            return proxy; // here we use the proxy to return 'this'
        }
        if ("getBalance".equals(method.getName())) {
            return balance;
        }
        return null;
    }
}

注意他在invoke方法中是通过判断接口中的方法名来选择是返回代理对象,还是常规对象,我觉得这是不合理的,因为我们之所以用动态代理而不用静态代理,很大程度上是因为我们要织入的方法是不希望和实际的对象绑定在一起的,就像我们在实际中,很多时候代理都是来开启事物,关闭事物的,就是说无论是实现了什么接口的代理对象,它应当都按照统一的逻辑去执行AOP,但是按照这个写法,我们的Handler就与一个固定的接口绑定在一起了,要是这样的话,我们直接写一个代理类岂不是更方便,直接使用静态代理就行了,没必要再去使用动态代理了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容