【拿来吧你】JDK动态代理

java proxy

【Spring Boot】一个注解实现下载接口

【Java】异步回调转为同步返回

因为最近一段时间准备将这几年做的一些业务和技术做个沉淀,也自己造的一些轮子,发现时不时就会需要用到动态代理和反射,所以今天打算先对jdk的动态代理这部分内容做个简单的整理

介绍

先来说说怎么理解动态代理吧

首先在java中有一种模式叫代理模式,代理的定义是

代理是指以他人的名义,在授权范围内进行对被代理人直接发生法律效力的法律行为

对应到代理模式

  • 以他人名义 > 代理对象
  • 授权范围内 > 基于接口或类的规范限制(如无法代理接口或类中不存在的方法等)或是可实现的功能范围(如无法获得被代理方法的中间变量等)
  • 被代理人 > 被代理的对象
  • 法律 > java中的规范(如访问限制等)
  • 直接发生法律效力 > 直接影响方法执行逻辑(如在前后打印日志或是修改入参返回值,对方法的最终执行结果产生了影响)
  • 法律行为 > 及代理本身的行为(符合java规范)

所以代理模式就是在通过代理对象调用被代理对象的方法过程中改变原有方法的执行逻辑

好了,完全被自己绕晕了,其实代理能做到的功能完全可以枚举出来

  • 修改入参
  • 修改返回值
  • 异常处理
  • 日志打印
  • 完全重写

一般也就用到这些功能

而所谓的动态代理,就是这个代理对象是通过代码动态生成的,也就是用代码生成代码,经典套娃了属于是

使用场景

对于动态代理的使用场景,主要还是用于一些大型框架中

  • 其中一个场景就是Spring利用动态代理实现强大的切面功能

  • 另一个场景就是Feign在接口上添加注解来实现HTTP的调用,或是MyBatis在接口上添加注解来执行SQL等等

方式

主流的动态代理有2种:jdk动态代理和cglib动态代理

说说这两种方式的区别

  • jdk动态代理只能代理接口(统一继承Proxy类),cglib动态代理可以代理接口也可以代理类(通过生成子类,所以不能被final修饰)
  • jdk动态代理直接写Class文件,cglib借助ASM框架处理Class文件
  • jdk动态代理方法内调用其他方法无法被代理(通过某个实现类调用方法时),cglib动态代理方法内调用的其他方法也可以被代理

接下来详细聊聊jdk动态代理吧,由于篇幅原因cglib动态代理考虑另开一篇

JDK动态代理

基于jdk8

用法

用法就简单过一下吧

public interface JdkDynamicProxy {

    String methodToProxy();
}

JdkDynamicProxy target = new JdkDynamicProxy() {
    @Override
    public String methodToProxy() {
        return "Target method to proxy";
    }
};

JdkDynamicProxy proxy = (JdkDynamicProxy) Proxy
        .newProxyInstance(JdkDynamicProxy.class.getClassLoader(),
                new Class[]{JdkDynamicProxy.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, 
                                         Method method, 
                                         Object[] args)
                            throws Throwable {
                        System.out.println(method.getName());
                        return method.invoke(target, args);
                    }
                });
System.out.println(proxy.methodToProxy());

用法很简单,调用Proxy.newProxyInstance方法就能生成一个代理对象,然后就可以在被代理对象的方法调用的前后做一些有限的事情

参数

接下来说说这个方法的参数

先说第二个参数,传入一个类型为Class<?>[],名称为interfaces的对象,也就是你要代理的接口,因为java可以实现多个接口,所以是一个数组

再来说一下第三个参数InvocationHandler,这是一个接口,当你通过代理对象调用接口的方法时,就会回调该接口的invoke方法并回传代理对象,方法和方法入参

最后说第一个参数,需要传入一个ClassLoader,一般情况下你用这个项目里面任何一个能得到的类加载器都没问题,这个类加载器的主要作用就是校验其可以访问指定的接口以及加载这个代理类

//省略部分代码
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
    
    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        //校验类加载器可以访问指定的接口
        for (Class<?> intf : interfaces) {
            /*
             * Verify that the class loader resolves the name of this
             * interface to the same Class object.
             */
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
        }
        
        //使用类加载器定义类
        try {
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
             * A ClassFormatError here means that (barring bugs in the
             * proxy class generation code) there was some other
             * invalid aspect of the arguments supplied to the proxy
             * class creation (such as virtual machine limitations
             * exceeded).
             */
            throw new IllegalArgumentException(e.toString());
        }
    }
}

流程

接下来就说说生成代理对象的整个流程

克隆接口数组

final Class<?>[] intfs = interfaces.clone();

首先会对我们传入的第二个参数,也就是接口数组进行拷贝

这里我猜想可能是防止数组中的元素在生成代理的过程中被修改导致出现一些问题,不得不佩服果然是心思缜密啊

接口数量检查

if (interfaces.length > 65535) {
    throw new IllegalArgumentException("interface limit exceeded");
}

这边限制了一个接口的数量为65535,那么这个65535是怎么来的呢

如果大家对Class文件结构有一定了解的话就知道文件中会通过interfaces_count定义接口数量,该数据项占2个字节,所以最大能表示65535

查找缓存

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);

然后会在缓存中查找,这是肯定的,不然每次都重新生成一遍也太蠢了

这个缓存用的是WeakCache对象,通过两个key来定位一个value,就是通过我们传入的类加载器接口数组来定位一个动态生成的代理类,类似于Map<ClassLoader, Map<Class<?>[], Class<? entends Proxy>>>

// the key type is Object for supporting null key
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map
    = new ConcurrentHashMap<>();

而他使用的keyvalue都是WeakReference类型,防止当动态生成的类不再使用时导致内存泄漏的问题

第一个key,使用我们传入的ClassLoaderkey

//key为我们传入的ClassLoader
Object cacheKey = CacheKey.valueOf(key, refQueue);

private static final class CacheKey<K> extends WeakReference<K> {
    //代码省略
}

其中refQueueReferenceQueue类型(不了解的可以看一下WeakReference的构造器)而且都是同一个对象,ReferenceQueue是属于gc回收这部分的内容了,先不展开了吧

猜想由于ClassLoader可能是URLClassLoader甚至一些自定义的类加载器,就有可能导致内存泄漏,所以也用了弱引用

另外需要注意这个key也就是类加载器是可以为null的,猜测为null时被判定为bootstrap类加载器

但是如果我们传入的类加载器为null就可能会报错xxx is not visible from class loader,这是为什么呢

/*
 * Verify that the class loader resolves the name of this
 * interface to the same Class object.
 */
Class<?> interfaceClass = null;
try {
    interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
    throw new IllegalArgumentException(
        intf + " is not visible from class loader");
}

可以看到通过用我们传入的类加载器重新加载接口并判断和我们传入的接口是否相等来做校验

我们都知道两个Class是否相等是需要满足全限定名相等类加载器相等这两个条件的,而如果我们传入null也就是使用bootstrap类加载器,那么和接口的类加载器AppClassLoader是两个不同的类加载器,导致两个类不相等抛出异常

简单来说,就是直接使用接口的类加载器就完事儿了

那么这个类加载器到底应该怎么传呢

如果是我们自定义的接口,那么需要传入AppClassLoader(或者是加载该接口的类加载器,当然也可以是以对应类加载器为父类加载器的自定义类加载器)而不能传入null

如果我们要代理的接口是java.util.List这种,那就可以传null(本身就是由bootstrap类加载器加载)或者AppClassLoader(基于双亲委派模型能够加载java.util.List

这里就不展开类加载器了(强制拉回)

继续讲第二个key,第二个key使用ClassLoader和接口数组生成

// create subKey and retrieve the possible Supplier<V> stored by that
// subKey from valuesMap
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));

这里通过subKeyFactory来生成一个subKeysubKeyFactoryKeyFactory对象

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

不过KeyFactory完全没用到ClassLoader,所以其实就是接口数组作为key

动态生成代理类

如果缓存中不存在,那么就需要通过ProxyClassFactory来生成一个代理类

value = Objects.requireNonNull(valueFactory.apply(key, parameter));

valueFactory也就是ProxyClassFactory对象,我们来看看ProxyClassFactory是怎么生成代理类的

接口校验

首先会对我们传入的接口进行ClassLoader的校验

/*
 * Verify that the class loader resolves the name of this
 * interface to the same Class object.
 */
Class<?> interfaceClass = null;
try {
    interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
    throw new IllegalArgumentException(
        intf + " is not visible from class loader");
}

这块前面已经大致分析过了,即我们传入的ClassLoader需要是这些接口的类加载器

然后判断这些接口(由于接口也是通过Class表示所以需要额外校验)必须为接口

/*
 * Verify that the Class object actually represents an
 * interface.
 */
if (!interfaceClass.isInterface()) {
    throw new IllegalArgumentException(
        interfaceClass.getName() + " is not an interface");
}

最后这些接口不能重复

/*
 * Verify that this interface is not a duplicate.
 */
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
    throw new IllegalArgumentException(
        "repeated interface: " + interfaceClass.getName());
}
设置访问标志
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

首先会将代理类的访问标志设置为publicfinal

然后会判断需要代理的接口,如果存在不是public的接口,则把代理类的访问标志改为final,并且将代理类的包名设置为非public接口的包名,如果有多个非public接口,就需要判断这些非public的接口包名是否一样,否则抛出异常(因为如果不是public就无法被其他包访问到)

/*
 * Record the package of a non-public proxy interface so that the
 * proxy class will be defined in the same package.  Verify that
 * all non-public proxy interfaces are in the same package.
 */
for (Class<?> intf : interfaces) {
    int flags = intf.getModifiers();
    if (!Modifier.isPublic(flags)) {
        accessFlags = Modifier.FINAL;
        String name = intf.getName();
        int n = name.lastIndexOf('.');
        String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
        if (proxyPkg == null) {
            proxyPkg = pkg;
        } else if (!pkg.equals(proxyPkg)) {
            throw new IllegalArgumentException(
                "non-public interfaces from different packages");
        }
    }
}
确定最终包名
if (proxyPkg == null) {
    // if no non-public proxy interfaces, use com.sun.proxy package
    proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

如果上一步没有非public包名时,那么指定包名为com.sun.proxy,否则使用那个非public接口的包名

指定类名
/*
 * Choose a name for the proxy class to generate.
 */
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

其中proxyClassNamePrefix为常量$Proxy

// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";

所以最后生成的代理类的全名就是com.sun.proxy.$Proxy0com.sun.proxy.$Proxy1com.sun.proxy.$Proxy2以此类推(所有接口都是public的情况下)

生成类文件
/*
 * Generate the specified proxy class.
 */
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
    proxyName, interfaces, accessFlags);
    
public static byte[] generateProxyClass(final String name,
                                        Class<?>[] interfaces,
                                        int accessFlags) {
    ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
    final byte[] classFile = gen.generateClassFile();
    //省略部分代码
}

将代理类的全限定名,需要实现的接口,访问标志交给ProxyGenerator去生成类文件

接下来看看generateClassFile方法做了什么

添加Object方法
/*
 * Record that proxy methods are needed for the hashCode, equals,
 * and toString methods of java.lang.Object.  This is done before
 * the methods from the proxy interfaces so that the methods from
 * java.lang.Object take precedence over duplicate methods in the
 * proxy interfaces.
 */
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);

首先会添加hashCodeequalstoString这3个Object的方法

添加接口方法
/*
 * Now record all of the methods from the proxy interfaces, giving
 * earlier interfaces precedence over later ones with duplicate
 * methods.
 */
for (Class<?> intf : interfaces) {
    for (Method m : intf.getMethods()) {
        addProxyMethod(m, intf);
    }
}

接着将需要被代理的所有接口方法也添加进去

方法返回值校验
/*
 * For each set of proxy methods with the same signature,
 * verify that the methods' return types are compatible.
 */
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
    checkReturnTypes(sigmethods);
}

之前我们添加的所有方法都会保存在proxyMethods中,然后会校验所有同名同入参方法的返回值

由于java中不允许方法名相同,入参相同但是返回值不同的方法定义,比如

public interface Demo {

    String demo(String s);
    
    int demo(String s);
}

而我们平时写代码时,上面的写法就直接报红了

添加构造方法
methods.add(generateConstructor());

接着添加构造方法,generateConstructor里面的内容已经是用byte[]来拼Class文件的内容了,就不跟进去讲Class文件结构的内容了

添加静态方法变量
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
    for (ProxyMethod pm : sigmethods) {

        // add static field for method's Method object
        fields.add(new FieldInfo(pm.methodFieldName,
            "Ljava/lang/reflect/Method;",
             ACC_PRIVATE | ACC_STATIC));

        // generate code for proxy method and add it
        methods.add(pm.generateMethod());
    }
}

这里其实光看代码会有点困惑,不过如果看过最终生成的代理类就会比较好理解

这边会把所有的方法都添加为代理类的privatestatic属性字段,大家还记得InvocationHandlerinvoke方法传回来的其中一个参数就是Method对象,实际上这个Method对象就是这里添加的字段

而最后的generateMethod方法也是拼接Class文件内容

添加静态代码块的初始化方法
methods.add(generateStaticInitializer());

这里主要就是初始化上面定义的Method类型的字段,因为我们上面只是为每个方法都添加了一个Method类型的字段定义,但是这个字段并没有赋值,就是在静态代码块中对这些字段进行了赋值

校验字段和方法的数量
if (methods.size() > 65535) {
    throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
    throw new IllegalArgumentException("field limit exceeded");
}

和接口数量一样,方法数量和字段数量在Class文件中也是用2个字节表示的,所以都有65535的限制

拼接类文件
/* ============================================================
 * Step 3: Write the final class file.
 */

/*
 * Make sure that constant pool indexes are reserved for the
 * following items before starting to write the final class file.
 */
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
    cp.getClass(dotToSlash(intf.getName()));
}

/*
 * Disallow new constant pool additions beyond this point, since
 * we are about to write the final constant pool table.
 */
cp.setReadOnly();

ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

try {
    /*
     * Write all the items of the "ClassFile" structure.
     * See JVMS section 4.1.
     */
                                // u4 magic;
    dout.writeInt(0xCAFEBABE);
                                // u2 minor_version;
    dout.writeShort(CLASSFILE_MINOR_VERSION);
                                // u2 major_version;
    dout.writeShort(CLASSFILE_MAJOR_VERSION);

    cp.write(dout);             // (write constant pool)

                                // u2 access_flags;
    dout.writeShort(accessFlags);
                                // u2 this_class;
    dout.writeShort(cp.getClass(dotToSlash(className)));
                                // u2 super_class;
    dout.writeShort(cp.getClass(superclassName));

                                // u2 interfaces_count;
    dout.writeShort(interfaces.length);
                                // u2 interfaces[interfaces_count];
    for (Class<?> intf : interfaces) {
        dout.writeShort(cp.getClass(
            dotToSlash(intf.getName())));
    }

                                // u2 fields_count;
    dout.writeShort(fields.size());
                                // field_info fields[fields_count];
    for (FieldInfo f : fields) {
        f.write(dout);
    }

                                // u2 methods_count;
    dout.writeShort(methods.size());
                                // method_info methods[methods_count];
    for (MethodInfo m : methods) {
        m.write(dout);
    }

                                 // u2 attributes_count;
    dout.writeShort(0); // (no ClassFile attributes for proxy classes)

} catch (IOException e) {
    throw new InternalError("unexpected I/O Exception", e);
}

这里其实没啥好说的,这是Class文件结构的内容了,包括上面的generateConstructorgenerateMethodgenerateStaticInitializer都是类似的代码

重点讲一下这句代码

// u2 super_class;
dout.writeShort(cp.getClass(superclassName));

这里的superclassName为常量java/lang/reflect/Proxy

/** name of the superclass of proxy classes */
private final static String superclassName = "java/lang/reflect/Proxy";

所以所有的代理类的父类都是Proxy这个类,这也就是jdk动态代理只能代理接口的原因(java不允许多继承)

定义类
try {
    return defineClass0(loader, proxyName,
                        proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
    /*
     * A ClassFormatError here means that (barring bugs in the
     * proxy class generation code) there was some other
     * invalid aspect of the arguments supplied to the proxy
     * class creation (such as virtual machine limitations
     * exceeded).
     */
    throw new IllegalArgumentException(e.toString());
}

这个方法是native的,就不深入了

总结

是不是突然觉得可以手撸一套定制化的动态代理框架了(如果对Class文件比较熟悉,或者借助ASM

其实jdk动态代理的整个逻辑并没有多复杂,无非就是按照Class文件的结构要求拼接各个部分数据,但是在整个过程中做了很多校验的逻辑

相对应我们平时的开发,业务功能其实很多情况下都不复杂甚至还非常简单,但是业务定义之外的边缘数据的校验和适配也不能马虎,又或是对于一些并发等其他场景下的风险考虑,逻辑严密性比功能实现来的更为重要


其他的文章

【Spring Boot】一个注解实现下载接口

【Java】异步回调转为同步返回

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

推荐阅读更多精彩内容