跳至主要內容

JDK动态代理

PPLong大约 9 分钟学习

JDK动态代理

Spring AOP 和注解中涉及到一些JDK动态代理的内容,想要理解一下在Java层的动态代理具体过程。

涵盖内容: 代理的例子、静态/动态代理概念、JDK中动态代理原理、Java层动态代理过程分析

引入

public class Bird{
    public void fly() {
        // core code
        System.out.println("fly");
    }

    public void sing() {
        System.out.println("sing");
    }
}

考虑有如上的实例类,在业务中我们需要对一个或者多个方法做出某些变化,为其添加一些额外的功能,那可能或怎么做?

1. 直接修改源方法

    public void fly() {
        System.out.println("start fly");
        // core code
        System.out.println("fly");
        System.out.println("end fly");
    }

    public void sing() {
        System.out.println("start sing");
        System.out.println("sing");
        System.out.println("end sing");
    }

这种方法是最直接的,但有很多问题:

  1. 不符合开闭原则,“对外开放对内封闭”;
  2. 如果要修改的方法有许多,那则会有很大的修改成本,重复代码过多

由此考虑通过代理(Proxy)来实现一种便捷的第三方的操作

静态代理

代理:其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

对上述例子的改进

将核心方法抽象成接口,原有业务类(或核心代码类)实现该接口(理解为主要执行类)。新建代理类同样实现该接口(保持方法不变性)并且持有原有业务类的实例(这里为了更好低抽象,可以将业务类的类型变为接口类型,而非其实体类)

public interface BirdAbility {
    void fly();
    void sing();
}
public class BirdImpl implements BirdAbility{
    @Override
    public void fly() {
        System.out.println("fly");
    }
    @Override
    public void sing() {
        System.out.println("sing");
    }
}
public class BirdProxy implements BirdAbility{
    // 使用接口作为类名
    BirdAbility target;
    public void setTarget(BirdAbility target) {
        this.target = target;
    }
    @Override
    public void fly() {
        System.out.println("fly start");
        target.fly();
        System.out.println("fly end");
    }

    @Override
    public void sing() {
        System.out.println("sing start");
        target.sing();
        System.out.println("sing end");
    }
}

通过静态代理,能让代码有一定的封闭性,开闭原则满足,但还是不能解决代码冗余和过多工作量的问题。此外由于规定的接口是BirdAbility,则其只能接受BirdAbility的实现类,而无法为其他类做代理,如果要为所有类添加该功能,则可能要写很多的代理类和接口。

如何少写代理类而完成代理任务?

动态代理

能否通过接口而直接动态创造一个代理类呢?

Proxy

getProxyClass()

Proxy类的getProxyClass(ClassLoader loader,Class<?>... interfaces)能够接受Class对象及一个定义该class的classloader,来创建一个加强版的代理类,拥有其所有方法和构造器$Proxy0

需要注意的这种方法创建的代理类只能通过传入InvocationHandler实例来构造,没有无参方法。

Class<?> birdProxyClass = Proxy.getProxyClass(BirdAbility.class.getClassLoader(), BirdAbility.class);
        System.out.println(birdProxyClass.getSimpleName());  // $Proxy0
        for (Constructor<?> constructor : birdProxyClass.getConstructors()) {
            System.out.println(constructor.toGenericString()); //public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler)
        }
        for (Method declaredMethod : birdProxyClass.getDeclaredMethods()) {
            System.out.println(declaredMethod.getName());
        }
        for (Field declaredField : birdProxyClass.getDeclaredFields()) {
            System.out.println(declaredField.getName());
        }
preview
preview
静态代理
静态代理
动态代理
动态代理

而$Proxy0类是动态生成的,如何保留并查看这个类的class?
在IDEA Run And Debug Configuration中添加一下VM Option即可-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true
或者System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

可以看这里,Proxy0实现了规定的接口并且继承Proxy类,基本所有的方法都插入了h.invoke,这里的h属性即是InvocationHandler,通过传入自定义的InvocationHandler实现类来执行invoke方法

public class Proxy implements java.io.Serializable {
	    protected InvocationHandler h;
    
        protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
}
public final class $Proxy0 extends Proxy implements BirdAbility {
    private static Method m0;
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m4;

    public $Proxy0(InvocationHandler param1) {
        super(var1);
    }

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

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

    public final String toString() {
        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 void sing() {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
	// 有参情况
   	public final int fly(int var1) {
        try {
            return (Integer)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

		// 初始化method参数, 通过反射方式与接口的方法绑定,这里还涉及到“绑定接口方法而能在实体类中调用该反射方法”
    static {
        try {
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("BirdAbility").getMethod("sing");
            m4 = Class.forName("BirdAbility").getMethod("fly");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

getProxyClass Deprecated

Proxy classes generated in a named module are encapsulatedand not accessible to code outside its module.{@link Constructor#newInstance(Object...) Constructor.newInstance}will throw {@code IllegalAccessException} when it is called on an inaccessible proxy class.

也就是在调用getProxyClass时可能会抛出IllegalAccessException异常,而newProxyInstance则不会,以后可改用该方法

InvocationHandler

InvocationHandler本身是一个接口

Invoke

从上匿名定义的 InvocationHandler实验可知,每次通过Proxy创建的代理类调用方法时,都会进入到invoke方法中,invoke能辨认当前代理对象、方法名、参数列表,并且返回值直接返回到方法中

目前为止,理一下思路:
Proxy创建动态代理类$Proxy➡️ Proxy对每个方法会执行InvocationHandler的Invoke方法➡️ 具体怎么实现由Invoke方法规定

那么如何将具体业务类与代理类进行一个关联呢?主要是传入当前代理类的Proxy

 public static Object getProxy(final Object target) {
        Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("invocation start -- method " +method.getName());
                Object result = method.invoke(target, args);
                System.out.println("invocation end -- method " +method.getName());
                return result;
            }
        });
        return proxy;
    }

源码

JDK源码版本:corretto-15.0.2

就以上述代码为例,分析一下newProxyInstance干了什么事?长篇源码警告 ⚠️
猜测:动态加载了Proxy动态生成的代理类,有了代理类在JVM中,那之后new一个实例都不是事,所以问题都落在了如何动态地将一个接口加载成其代理的动态类

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    Objects.requireNonNull(h);
    final Class<?> caller = System.getSecurityManager() == null? null : Reflection.getCallerClass();
    // Look up or generate the designated proxy class and its constructor.
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
    return newProxyInstance(caller, cons, h);
}

这里经过调试,发现类的加载操作在getProxyConstructor这一步中(当然注释也写出来了.......)

private static Constructor<?> getProxyConstructor(Class<?> caller,ClassLoader loader,Class<?>... interfaces) {
        // optimization for single interface
        if (interfaces.length == 1) {
            Class<?> intf = interfaces[0];
            if (caller != null) {
                checkProxyAccess(caller, loader, intf);
            }
            return proxyCache.sub(intf).computeIfAbsent(
                loader,
                (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()
            );
        } else {
            // interfaces cloned
            final Class<?>[] intfsArray = interfaces.clone();
            if (caller != null) {
                checkProxyAccess(caller, loader, intfsArray);
            }
            final List<Class<?>> intfs = Arrays.asList(intfsArray);
            return proxyCache.sub(intfs).computeIfAbsent(
                loader,
                (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()
            );
        }
    }

判断接口数目,如果接口数目为1,那就直接取其Class,检查一下代理权限,为什么要检查呢?注释中有写

To define a proxy class, it performs the access checks as in Class.forName (VM will invoke ClassLoader.checkPackageAccess):

  1. "getClassLoader" permission check if loader == null
  2. checkPackageAccess on the interfaces it implements

If an interface is non-public, the proxy class must be defined by the defining loader of the interface. If the caller's class loader is not the same as the defining loader of the interface, the VM will throw IllegalAccessError when the generated proxy class is being defined.

也就是如果一个接口不是公有的,那这个代理类必须必须被定义接口的加载器定义,如果caller的class与定义接口的加载器不一致,则就会抛出异常(暂不清楚这里caller的作用)

检查一下如果不为1的话就要使用interfaces.clone(),尚不清楚为什么要用clone

Constructor<?> build() {
    Class<?> proxyClass = defineProxyClass(module, interfaces);
    final Constructor<?> cons;
    try {
        cons = proxyClass.getConstructor(constructorParams);
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            cons.setAccessible(true);
            return null;
        }
    });
    return cons;
        }

关键的build方法加载Class的代码一眼可知 defineProxyClass

        private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) {
            //.....
            if (proxyPkg == null) {
                // all proxy interfaces are public
                proxyPkg = m.isNamed() ? PROXY_PACKAGE_PREFIX + "." + m.getName()
                                       : PROXY_PACKAGE_PREFIX;
            } else if (proxyPkg.isEmpty() && m.isNamed()) {
                throw new IllegalArgumentException(
                        "Unnamed package cannot be added to " + m);
            }
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg.isEmpty()
                                    ? proxyClassNamePrefix + num
                                    : proxyPkg + "." + proxyClassNamePrefix + num;

            ClassLoader loader = getLoader(m);
            trace(proxyName, m, loader, interfaces);
            // Generate the specified proxy class.
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(loader, proxyName, interfaces, accessFlags);
            try {
                Class<?> pc = JLA.defineClass(loader, proxyName, proxyClassFile, null, "__dynamic_proxy__");
                reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
                return pc;
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }

proxyPkg和num拼接起我们最终看到的包名proxyName com.sun.proxy.$Proxy0 ,这里统计proxy的数量的num用了Atomic原子类,为什么用这个类呢?利用自旋锁CAS来保证线程安全。
最主要的加载步骤来了,ProxyGenerator.generateProxyClass,其居然是直接加载!也就是通过运行时转换二进制的byte[]数组表示的class字节码

static byte[] generateProxyClass(ClassLoader loader,
                                 final String name,
                                 List<Class<?>> interfaces,
                                 int accessFlags) {
    ProxyGenerator gen = new ProxyGenerator(loader, name, interfaces, accessFlags);
    final byte[] classFile = gen.generateClassFile();

    if (saveGeneratedFiles) {
        java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Void>() {
                    public Void run() {
                        try {
                            int i = name.lastIndexOf('.');
                            Path path;
                            if (i > 0) {
                                Path dir = Path.of(dotToSlash(name.substring(0, i)));
                                Files.createDirectories(dir);
                                path = dir.resolve(name.substring(i + 1) + ".class");
                            } else {
                                path = Path.of(name + ".class");
                            }
                            Files.write(path, classFile);
                            return null;
                        } catch (IOException e) {
                            throw new InternalError(
                                    "I/O exception saving generated file: " + e);
                        }
                    }
                });
    }
    return classFile;
}

通过ProxyGenerator去创建class字节码文件

private byte[] generateClassFile() {
    	// ...
    	addProxyMethod(hashCodeMethod);
        addProxyMethod(equalsMethod);
        addProxyMethod(toStringMethod);
    
    	for (Class<?> intf : interfaces) {
            for (Method m : intf.getMethods()) {
                if (!Modifier.isStatic(m.getModifiers())) {
                    addProxyMethod(m, intf);
                }
            }
        }
    	 for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }

        generateConstructor();
    	// ...
        return toByteArray();
}

generateClassFile 指明了为什么Proxy生成的代理类会有hashCode 、 equals 和 toString方法,因为在生成的时候就已经静态地加上了,然后这里遍历每一个接口,对接口中不是静态方法的方法进行添加,最终toByteArray()整合字节码数据,返回到上一步方法中,会根据是否有存入到本地的设置来选择是否写入到disk。
回到Proxy中

Class<?> pc = JLA.defineClass(loader, proxyName, proxyClassFile, null, "__dynamic_proxy__");

途经System类中转,最终进入到会进入到ClassLoader类,进到native方法(也部分证明是从JVM层所提供的特性)

static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                        ProtectionDomain pd, String source);

总结一下,Load Class In JVM的流程图

Load Class In JVM
Load Class In JVM

总结

了解了JDK为接口提供的方便的代理操作,了解了如何动态加载代理类,对部分源码还存有疑惑,待经验技术再积累积累再回头看看。这部分源码涉及的注释比较详细,再次阅读时还要借助到注释

参考文章🔗

知乎-浅谈JDK动态代理(上)open in new window
知乎-浅谈JDK动态代理(中)open in new window