转载

JDK动态代理

有一个用户接口和实现类,我们需要在登录成功前插入账号密码的检测,登录成功后插入欢迎的提示(简单起见全部用文字打印到控制台).

public interface User {
   void login();
}
public class UserImpl implements User {
    @Override
    public void login() {
        System.out.println("登录成功");
    }
}
复制代码

我们首先很容易就会想到直接在源码上编写,实际中直接修改源码情况可能不被允许,符合设计模式中的开闭原则.那么我们可以用另一种方式,也就是静态代理来实现.

静态代理

方法很简单,有点类似设计模式中的适配器模式.定义一个UserStaticProxy实现User接口然后组合User,在login方法中添加对应代码即可.

class UserStaticProxy implements User{

   private User user;

    public UserStaticProxy(User user) {
        this.user = user;
    }

    @Override
    public void login() {
        System.out.println("账号密码检测");
        user.login();
        System.out.println("欢迎登录");
    }
    
}
复制代码

动态代理

我们知道静态代理可以解决我们的需求,但是我们需要增强功能都要自己手动编写一个代理类,重复代码过多.这时就有考虑能否让jvm为我们自动在运行时创建一个代理类,我们只需提供信息即可.也就是将代理类抽象出来.

动态代理可能需要的参数

我们从我们编写的静态代理类入手,发现它肯定是需要实现接口,也就是我们准备要代理的类UserImpl所实现的接口User. 还需要一个参数是我们要代理的类User.其它暂时看不出来.

使用jdk提供的代理

jdk提供的生成代理类的方法是调用Proxy的静态方法newProxyInstance.它需要三个参数.

  1. ClassLoader loader
  2. Class<?>[] interfaces
  3. InvocationHandler h
    第一个是类加载器,我们就使用User使用的类加载器即可,通过User.class.getClassLoader()获取类加载器,第二个参数很明显就是我们刚刚猜的需要传的接口,因为一个类可以实现多个接口,所以这个是接口数组,第三个参数是需要一个实现InvocationHandler接口的实现类,它包含一个invoke方法.这个我们先不管是什么,先给它一个默认的实现类.等会看源码就清楚了.
public interface InvocationHandler {
 public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
复制代码

然后代码就如下

public class UserDynamicProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }

 public static void main(String[] args) {
            User o =(User) Proxy.newProxyInstance(
                    User.class.getClassLoader(),
                    new Class[]{User.class},
                    new UserDynamicProxy()
            );
            o.login();
        }
复制代码

我们生成了代理类,然后调用了它的login方法,结果什么也没有输出.

生成代理类分析

我们有必要看一看生成的代理类长什么样.因为代理类是运行时生成的,我们看不到,所以在编译运行时添加VM参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 用来将生成的代理类生成出来. 我们运行程序在idea中 com.sun.proxy下会有我们的代理$proxy0,它的内部结构长这样,我删减一部分暂时不需要看的.

public final class $Proxy0 extends Proxy implements User {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
    public final void login() throws  {
        try {
            super.h.invoke(this, m3, (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"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("proxy.User").getMethod("login");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
复制代码

可以看到这个代理类实现了User接口,而且它还默认继承了Proxy.当我们调用代理类的login方法时,它去调用了 super.h.invoke(this, m3, (Object[])null),有没有很熟悉它调用的是父类的h属性的invoke方法. 我们在创建代理类的时候InvocationHandler不就是需要实现invoke方法吗.

我们看一下代理类的构造方法,它把InvocationHandler赋值给了父类的构造方法.

public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
复制代码

进入父类Proxy,查看到父类的属性h

protected InvocationHandler h;
复制代码

缕一下顺序,也就是当我们创建代理类时,它将InvocationHandler的实现类赋值给了父类的属性h,然后在我们调用login方法时,调用了父类h的invoke方法.也就是最终调用的我们的实现类UserDynamicProxy的invoke方法了.(绕了一大圈)

invoke方法

看一下代理类的invoke方法的三个参数,看名字我们就知道大概是啥

  1. Object proxy 代理类
  2. Method method 方法
  3. Object[] args 参数

然后再来看看代理类$Proxy0中传递过来的三个参数.

super.h.invoke(this, m3, (Object[])null); 
复制代码

第一个是这个代理类对象本身this,第二个是m3,这个m3对应是方法,我们看看$Proxy0 中的static块

static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("proxy.User").getMethod("login");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
复制代码

这里是反射的知识,反射不懂需要去补补了,即可不懂也应该能大概看懂,也就是m3就是User接口的方法login.第三个参数为null,因为我们的login方法没有参数,所以直接为null.当有参数时,也就我们调用 代理类.login(参数a),这个参数a会传给第三个参数.

继续缕一下,代理类调用login方法后会去调用invoke方法,同时invoke会携带好三个参数,代理类对象本身,对应的方法Method和方法的参数.

实现需求

好了,那么我们如何实现我们的需求.无非就是在invoke方法上编写代码,我们先要将login的登陆成功打印出来先吧,调用method.invoke调用方法,它需要两个参数,一个是调用方法的对象,一个是方法的参数.参数我们已经有了从invoke方法的第三个参数里拿,但是调用方法的对象我们没有.所以我们还需要组合一下User.然后构造时将UserImpl传递给属性o,用于method的invoke调用.然后只需要在method.invoke前后执行相应的逻辑即可.

public class UserDynamicProxy implements InvocationHandler {
    private User o;
    public UserDynamicProxy(User o) {
        this.o = o;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("账号密码检测");
        method.invoke(o,args);
        System.out.println("欢迎登录");
        return null;
    }

   public static void main(String[] args) {
            User o =(User) Proxy.newProxyInstance(
                    User.class.getClassLoader(),
                    new Class[]{User.class},
                    new UserDynamicProxy(new UserImpl())
            );
            o.login();
        }
输出结果:账号密码检测
        登录成功
        欢迎登录
复制代码

对比一下我们刚刚的静态代理的实现是不是发现很类似.

小结

我们发现$proxy0是继承Proxy的然后实现我们的User接口,也就是说jdk提供的动态代理,只能对实现接口的实现类进行代理,因为java无法多继承.而cglib可以对不实现接口的类进行代理.

原文  https://juejin.im/post/5df8f5145188251279566f27
正文到此结束
Loading...