Spring底层原理分析-八(JDK代理原理)

136 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

JDK代理简单实现

  这里我们模拟JDK代理实现的方式来对其原理进行解析,它的底层是使用了ASM的技术来处理的,这里先不讨论这个。我们用java代码的方式尝试还原整体逻辑。

public class A08 {
    interface Foo {
        void foo();
    }
    static class Target implements Foo {
        @Override
        public void foo() {
            System.out.println("foo执行");
        }
    }
    public static void main(String[] args) {
        $Proxy0 proxy0 = new $Proxy0();
        proxy0.foo();
    }

}
public class $Proxy0 implements A08.Foo{
    @Override
    public void foo() {
        System.out.println("before....");
        A08.Target target = new A08.Target();
        target.foo();
        System.out.println("after.....");
    }
}

  这里案例代码和上节中的案例比较相似,不过我们这里自己建立了一个代理类,起名格式也是模仿代理类的命名,和目标类实现了同一个接口,并实现其中的方法做增强。这里没有特别的地方,执行main方法后就会打印出$Proxy0类中的foo方法逻辑。
  这里只是一个很简单的实现,针对这种简单的实现,我们引出一个问题,就是将来有可能会有众多的增强逻辑,肯定不能全靠这种方式写到代理类中。那么我们就要使用某种方法来解决这个问题,这里引出上节创建JDK代理时的一个重要参数:InvocationHandler。

自定义InvocationHandler

public class A08 {
    interface Foo {
        void foo();
    }
    interface InvocationHandler {
        void invoke();
    }
    static class Target implements Foo {
        @Override
        public void foo() {
            System.out.println("foo执行");
        }
    }
    public static void main(String[] args) {
        $Proxy0 proxy0 = new $Proxy0(() -> {
            System.out.println("before.....");
            new Target().foo();
            System.out.println("after.....");
        });
        proxy0.foo();
    }
}
public class $Proxy0 implements Foo{
    private InvocationHandler invocationHandler;
    public $Proxy0(InvocationHandler invocationHandler) {
        this.invocationHandler = invocationHandler;
    }
    @Override
    public void foo() {
        invocationHandler.invoke();
    }
}

  如上代码,我们将最上面的简单案例修改了一下,我们模仿JDK代理的方式建了一个自定义的且同名的InvocationHandler接口,里面也有一个invoke方法。然后我们在代理类中增加该接口的参数和构造器。我们在创建$Proxy0类的时候通过该构造器传入一个InvocationHandler接口的实现,实现里写入了具体的增强逻辑。通过这样一套流程的调用,我们就摆脱了在代理类中写增强逻辑。
  引出新问题,如果我们在Foo接口中添加多个方法,如果代理类中使用同样的方法来调用,那么,由于InvocationHandler实现方法中写死的调用foo方法,那么不管有多少方法,最终调用的都只是foo方法。

Method获取

  结合上一步的问题,我们回想上节InvocationHandler的invoke方法的三个参数中,有一个method参数,还有个method的参数的参数。想到这里,就明白这两个参数就是为了解决上面所说的问题。

public class A08 {
    interface Foo {
        void foo();
        void bar();
    }
    interface InvocationHandler {
        void invoke(Method method, Object[] object) throws Throwable;
    }
    static class Target implements Foo {
        @Override
        public void foo() {
            System.out.println("foo执行");
        }
        @Override
        public void bar() {
            System.out.println("bar执行");
        }
    }
    public static void main(String[] args) {
        $Proxy0 proxy0 = new $Proxy0((method, args1) -> {
            System.out.println("before.....");
            method.invoke(new Target(), args1);
            System.out.println("after.....");
        });
        proxy0.foo();
        proxy0.bar();
    }
}
public class $Proxy0 implements Foo {
    private InvocationHandler invocationHandler;
    public $Proxy0(InvocationHandler invocationHandler) {
        this.invocationHandler = invocationHandler;
    }
    @Override
    public void foo() {
        try {
            Method foo = Foo.class.getMethod("foo");
            invocationHandler.invoke(foo, new Object[0]);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    @Override
    public void bar() {
        try {
            Method foo = Foo.class.getMethod("bar");
            invocationHandler.invoke(foo, new Object[0]);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

  我们根据所说逻辑再次修改后,得到了上面的代码。在代理类中的每个方法中,获取接口方法本身,传到invoke方法中,这时我们就不是通过创建目标对象来进行方法调用了,而是根据传回的方法,进行反射调用。

代理实现优化

  我们发现我们上面写的代码内容和JDK本身的还是有些区别,我们再做一步优化,直接上代码。

public class A08 {
    interface Foo {
        void foo();
        int bar();
    }
    static class Target implements Foo {
        @Override
        public void foo() {
            System.out.println("foo执行");
        }
        @Override
        public int bar() {
            System.out.println("bar执行");
            return 0;
        }
    }
    public static void main(String[] args) {
        $Proxy0 proxy0 = new $Proxy0((proxy, method, args1) -> {
            System.out.println("before.....");
            Object invoke = method.invoke(new Target(), args1);
            System.out.println("after.....");
            return invoke;
        });
        proxy0.foo();
        proxy0.bar();
    }
}
public class $Proxy0 extends Proxy implements Foo {
    public $Proxy0(InvocationHandler h) {
        super(h);
    }
    static Method foo;
    static Method bar;
    static {
        try {
            foo = Foo.class.getMethod("foo");
            bar = Foo.class.getMethod("bar");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }
    @Override
    public void foo() {
        try {
            h.invoke(this, foo, new Object[0]);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
    @Override
    public int bar() {
        try {
            Object invoke = h.invoke(this, bar, new Object[0]);
            return (int) invoke;
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
}

  改变还是比较大的,改动步骤如下:

  • 对其中一个方法,这里选取bar方法,来增加返回值,一系列代码同步修改为Object接收返回值的方式。
  • 然后为了避免每次调用方法都要执行getMethod()方法,我们使用静态成员变量和静态代码块使其只会被初始化执行一次,每次调用的时候直接用即可。
  • 根据JDK代理方法的参数,我们增加了代理类本身的参数的回传,在代理类中可以用this表示,可以在一些特殊业务中使用。
  • 删除了自定义的InvocationHandler,直接使用的jar包中的接口类,翻看源码会发现和我们自定义的是一致的,这里可以直接替换代理类中的引用路径即可。
  • 删除了代理类中的InvocationHandler成员变量,选用继承Proxy类,该类中已经有对该参数的定义和构造,我们修改构造器使用super调用父类即可,下面所有的使用直接使用父类中定义的h即可。

ASM

  文章最初提到JDK代理使用的ASM字节码技术来处理的代理类生成,这里对ASM不做深究,感兴趣的小伙伴可以继续翻阅相关的文档。我们这里只说一下,由于是使用该技术直接生成字节码,所以,代理类没有经过任何的源码、编译等阶段。

inflation机制

  反射是比正常编译慢的过程,至于慢的过程总结以下几点。

  • java的invoke方法是传object和object[]数组的,由于过多的通用性质,基本参数类型都需要拆箱和装箱,产生大量额外的对象和内存开销,进而频繁的促发GC。
  • 编译器难以对动态调用的代码提前做优化,比如方法内联。
  • 反射需要按名检索类和方法,这样也会有一定的时间开销。

  正是这么多的不便,那么就需要一定的优化手段。我们从Proxy.newProxyInstance入手查看源码。

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

  查看return cons.newInstance(new Object[]{h});

@CallerSensitive
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

  查看T inst = (T) ca.newInstance(initargs);

public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.c.getDeclaringClass())) {
        ConstructorAccessorImpl var2 = (ConstructorAccessorImpl)(new MethodAccessorGenerator()).generateConstructor(this.c.getDeclaringClass(), this.c.getParameterTypes(), this.c.getExceptionTypes(), this.c.getModifiers());
        this.parent.setDelegate(var2);
    }

    return newInstance0(this.c, var1);
}

  重点就在这里,我们看到有个数值的比较,ReflectionFactory.inflationThreshold()小于多少就会执行if内的方法。我们先看if之外的处理方式

private static native Object newInstance0(Constructor<?> var0, Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException;

  native修饰的方法,底层由非java语言编写。这个if判断的逻辑整体的意思就是,如果反射调用不超过16次(数值是默认值,可以通过jvm参数控制),那么就走native的方式调用。如果超过,就会生成代理类GeneratesConstructorAccessor,该类中的调用方式已经不再是反射了,而是通过类本身来去调用具体的执行方法,这个类似于Cglib的一些逻辑,具体可以通过下一篇的Cglib里再次讨论这个问题。这个机制本身就叫做inflation机制。 image.png