搞懂Java动态代理

简介:

前言

在日常开发中,我们经常接触到Spring AOP、Mybatis DAO接口映射、hsf consumer调用这些概念,而这些技术的实现又都绕不开动态代理。这篇文章,旨在彻底的搞懂动态代理是个什么鬼,JDK中如何实现动态代理这两个点。

java 静态代理

静态代理通常用于对原有业务逻辑的扩充。比如持有二方包的某个类,并调用了其中的某些方法。然后出于某种原因,比如记录日志、打印方法执行时间,但是又不好将这些逻辑写入二方包的方法里。所以可以创建一个代理类实现和二方方法相同的方法,通过让代理类持有真实对象,然后在原代码中调用代理类方法,来达到添加我们需要业务逻辑的目的。

这其实也就是代理模式的一种实现,通过对真实对象的封装,来实现扩展性。

一个典型的代理模式通常有三个角色,这里称之为代理三要素

共同接口

public interface Action {
    public void doSomething();
}

真实对象

public class RealObject implements Action{

    public void doSomething() {
        System.out.println("do something");
    }
}

代理对象

public class Proxy implements Action {
    private Action realObject;

    public Proxy(Action realObject) {
        this.realObject = realObject;
    }
    public void doSomething() {
        System.out.println("proxy do");
        realObject.doSomething();
    }
}

运行代码

    Proxy proxy = new Proxy(new RealObject());
    proxy.doSomething();

simple_proxy.png

这种代理模式也最为简单,就是通过proxy持有realObject的引用,并进行一层封装。

静态代理的优点和缺点

先看看代理模式的优点: 扩展原功能,不侵入原代码。

再看看这种代理模式的缺点:

假如有这样一个需求,有十个不同的RealObject,同时我们要去代理的方法是不同的,比要代理方法:doSomething、doAnotherThing、doTwoAnotherThing,添加代理前,原代码可能是这样的:

realObject.doSomething();
realObject1.doAnotherThing();
realObject2.doTwoAnother();

为了解决这个问题,我们有方案一:

为这些方法创建不同的代理类,代理后的代码是这样的:

proxy.doSomething();
proxy1.doAnotherThing();
proxy2.doTwoAnother();

当然,也有方案二:

通过创建一个proxy,持有不同的realObject,实现Action1、Action2、Action3接口,来让代码变成这样:

proxy.doSomething();
proxy.doAnotherThing();
proxy.doTwoAnother();

于是你的代理模型会变成这样:

dynamic_proxy.png

毫无疑问,仅仅为了扩展同样的功能,在方案一种,我们会重复创建多个逻辑相同,仅仅RealObject引用不同的Proxy。

而在方案二中,会导致proxy的膨胀,而且这种膨胀往往是无意义的。此外,假如方法签名是相同的,更需要在调用的时候引入额外的判断逻辑。

在这里,让我们忘掉方案二,再回头看一下方案一,想一下它是什么?解决了什么问题?是如何解决的?然后,我们来看动态代理。

java 动态代理

搞清楚静态代理的缺点十分重要,因为动态代理的目的就是为了解决静态代理的缺点。通过使用动态代理,我们可以通过在运行时,动态生成一个持有RealObject、并实现代理接口的Proxy,同时注入我们相同的扩展逻辑。哪怕你要代理的RealObject是不同的对象,甚至代理不同的方法,都可以动过动态代理,来扩展功能。

简单理解,动态代理就是我们上面提到的方案一,只不过这些proxy的创建都是自动的并且是在运行期生成的。

动态代理基本用法

使用动态代理,需要将要扩展的功能写在一个InvocationHandler 实现类里:

public class DynamicProxyHandler implements InvocationHandler {
    private Object realObject;

    public DynamicProxyHandler(Object realObject) {
        this.realObject = realObject;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理扩展逻辑
        System.out.println("proxy do");

        return method.invoke(realObject, args);
    }
}

这个Handler中的invoke方法中实现了代理类要扩展的公共功能。

到这里,需要先看一下这个handler的用法:

public static void main(String[] args) {
        RealObject realObject = new RealObject();
        Action proxy = (Action) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Action.class}, new DynamicProxyHandler(realObject));
        proxy.doSomething();
}

Proxy.newProxyInstance 传入的是一个ClassLoader, 一个代理接口,和我们定义的handler,返回的是一个Proxy的实例。

仔细体会这个过程,其实有点类似我们在静态代理中提到的方案一,生成了一个包含我们扩展功能,持有RealObject引用,实现Action接口的代理实例Proxy。只不过这个Proxy不是我们自己写的,而是java帮我们生成的,有没有一点动态的味道。

让我们再回顾一下代理三要素:真实对象:RealObject,代理接口:Action,代理实例:Proxy

上面的代码实含义也就是,输入 RealObject、Action,返回一个Proxy。妥妥的代理模式。

综上,动态生成+代理模式,也就是动态代理。
网上搜了不少文章,到了这里,接下来就是和cglib等动态代理实现方法做一下横向比较。本文不做横向比较,为了不偏离主题,接下来做纵向挖掘。

看一下源码

道理清楚了,但是这篇文章题目是搞懂,所以来看一下这个Proxy是如何自动被生成的。入口就在newProxyInstance方法,核心代码如下:

private static final Class<?>[] constructorParams =
        { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
{
    Class<?> cl = getProxyClass0(loader, intfs);
    ...
    final Constructor<?> cons = cl.getConstructor(constructorParams);

    if (!Modifier.isPublic(cl.getModifiers())) {
           AccessController.doPrivileged(new PrivilegedAction<Void>() {
              public Void run() {
             cons.setAccessible(true);
             return null;
          }
           });
    }
return cons.newInstance(new Object[]{h});
}

整体流程就是:

1、生成代理类Proxy的Class对象。

2、如果Class作用域为私有,通过 setAccessible 支持访问

3、获取Proxy Class构造函数,创建Proxy代理实例。

生成Proxy的Class文件

生成Class对象的方法中,先是通过传进来的ClassLoader参数和Class[] 数组作为组成键,维护了一个对于Proxy的Class对象的缓存。这样需要相同Proxy的Class对象时,只需要创建一次。

第一次创建该Class文件时,为了线程安全,方法进行了大量的处理,最后会来到ProxyClassFactory的apply方法中,经过以下流程:

1、校验传入的接口是否由传入的ClassLoader加载的。

2、校验传入是否是接口的Class对象。

3、校验是否传入重复的接口。

4、拼装代理类包名和类名,生成.class 文件的字节码。

5、调用native方法,传入字节码,生成Class对象。

proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);

看一下第四步生成.class文件字节码的过程,主要分为两个阶段:

addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);

for (int i = 0; i < interfaces.length; i++) {
    Method[] methods = interfaces[i].getMethods();
    for (int j = 0; j < methods.length; j++) {
         addProxyMethod(methods[j], interfaces[i]);
    }
}
methods.add(this.generateConstructor());

 for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
    for (ProxyMethod pm : sigmethods) { 
        fields.add(new FieldInfo(pm.methodFieldName,
                                   "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
        methods.add(pm.generateMethod());
    }
 }
methods.add(generateStaticInitializer());

第一个阶段的代码比较清晰,主要就是添加各种Method,比如toString()、equals,以及传入的代理接口中的方法。再添加一下构造方法以及静态初始化方法。这要构成了一个对象,存储生成Proxy的Class的一些信息。

到了这里,已经把要构造的Proxy的方法基本定义完成了,接下来就要生成这个.class文件了。

 ByteArrayOutputStream bout = new ByteArrayOutputStream();
 DataOutputStream dout = new DataOutputStream(bout);
 dout.writeInt(0xCAFEBABE);
 ...
 dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
 ...
 return bout.toByteArray();

看到这个CAFEBABE,就清楚第二阶段的内容了。CAFEBABE是Class文件的魔数,关于Class文件这个咖啡宝贝的魔数,相信做Java的人都知道。没错,第二阶段就是生成字节码。按JVM规范,写入Class文件中包括权限控制、方法表、字段表等内容,生成符合规范的Class文件。最后返回对应的字节码。

字节码生成以后,通过调用native方法defineClass解析字节码,就生成了Proxy的Class对象。

Proxy构造方法

看一下Proxy的构造方法字节码生成部分:

MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);
DataOutputStream out = new DataOutputStream(minfo.code);
code_aload(0, out);
code_aload(1, out);
out.writeByte(opc_invokespecial);
out.writeShort(cp.getMethodRef(superclassName,"<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
...

关键在于,生成了一个参数为InvocationHandler的构造方法,code加载的是jvm方法区中的代码,然后通过invokespecial指令调用了父类构造方法。

查看生成的Class文件

上面利用字节码生成技术产生Class文件的过程,看起来可能比较晦涩,其实我们可以查看这个产生的Proxy到底是个什么样子。

注意ProxyGenerator中有这样一个逻辑:

if(saveGeneratedFiles) {
    ...
    FileOutputStream file = new FileOutputStream(dotToSlash(name) + ".class");
    file.write(classFile);
    ...
 }

再看一下saveGeneratedFiles这个变量:

private final static boolean saveGeneratedFiles =
    java.security.AccessController.doPrivileged( 
    new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))
    .booleanValue();

这是一个final类型的变量,通过GetBooleanAction方法读取系统变量,获取系统设置。默认这个值是false,稍微看一下System这个类的源码,发现有可以设置系统变量的Api,然后在程序的main 函数设置一下这个变量:

System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

这个时候,再跑一遍程序,就可以看到生成的Proxy的Class文件了,直接双击利用 ide 反编译。


package com.sun.proxy;

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

public final class $Proxy0 extends Proxy implements Action {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

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


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

    ...

    static {
        try {
           ...
            m3 = Class.forName("Action").getMethod("doSomething", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

省略一些无关代码,可以看到两个重要的方法。

一个就是我们的代理方法doSomething、另一个就是构造方法。

这个$Proxy0 继承 Proxy并调用了父类的构造方法,回忆一下上文提到的invokeSpecial,怎么样,对上了吧。

看一下Proxy中这个构造方法:

    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }

在看一下$Proxy0 的代理方法:

super.h.invoke(this, m3, (Object[])null);

再来回顾一下生成Proxy实例的过程:

private static final Class<?>[] constructorParams =
        { InvocationHandler.class };
...
final Constructor<?> cons = cl.getConstructor(constructorParams);
...
return cons.newInstance(new Object[]{h});    

怎么样,是不是豁然开朗了?

其实newInstance生成Proxy实例时,通过$Proxy0的Class对象,选择了这个InvocationHandler为参数的构造方法,传入我们定义的InvocationHandler并生成了一个 Proxy0的实例!InvocationHandler 里有realObject的逻辑以及我们的扩展逻辑,当我们调用Proxy0的doSomething方法时,就会调用到我们InvocationHandler 里 实现的invoke方法。

对上面这个过程,做一张图总结一下:

flow.png

总结

到了这里,在回顾一下我们开头提到的那个蠢蠢的方案一。动态代理真的高深么?不,其实它也只是采用方案一来解决我们的问题而已,只不过是利用字节码技术,自动帮我们生成了代码,减少了我们的重复劳动。怪不得都说,一个优秀的程序员,可能也是一个“懒惰”的程序员~

目录
相关文章
|
1月前
|
Java
JAVA 静态代理 & 动态代理
【11月更文挑战第14天】静态代理是一种简单的代理模式实现,其中代理类和被代理类的关系在编译时已确定。代理类实现与被代理类相同的接口,并持有被代理类的实例,通过调用其方法实现功能增强。优点包括代码结构清晰,易于理解和实现;缺点是对于多个被代理类,需为每个类编写相应的代理类,导致代码量大增,维护成本高。动态代理则在运行时动态生成代理类,更加灵活,减少了代码冗余,但可能引入性能损耗和兼容性问题。
|
2月前
|
设计模式 Java API
[Java]静态代理与动态代理(基于JDK1.8)
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
36 0
[Java]静态代理与动态代理(基于JDK1.8)
|
2月前
|
Java
深入理解Java动态代理
深入理解Java动态代理
92 1
|
2月前
|
Java
Java代码解释静态代理和动态代理的区别
### 静态代理与动态代理简介 **静态代理**:代理类在编译时已确定,目标对象和代理对象都实现同一接口。代理类包含对目标对象的引用,并在调用方法时添加额外操作。 **动态代理**:利用Java反射机制在运行时生成代理类,更加灵活。通过`Proxy`类和`InvocationHandler`接口实现,无需提前知道接口的具体实现细节。 示例代码展示了两种代理方式的实现,静态代理需要手动创建代理对象,而动态代理通过反射机制自动创建。
|
2月前
|
设计模式 缓存 Java
从源码学习Java动态代理|8月更文挑战
从源码学习Java动态代理|8月更文挑战
|
5月前
|
缓存 Java 测试技术
day27:Java零基础 - 动态代理
【7月更文挑战第27天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
45 2
day27:Java零基础 - 动态代理
|
4月前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
295 0
|
4月前
|
设计模式 Java C++
揭秘!JDK动态代理VS CGLIB:一场关于Java代理界的‘宫心计’,你站哪队?
【8月更文挑战第24天】Java 动态代理是一种设计模式,允许在不改动原类的基础上通过代理类扩展功能。主要实现方式包括 JDK 动态代理和 CGLIB。前者基于接口,利用反射机制在运行时创建代理类;后者采用继承方式并通过字节码技术生成子类实现类的代理。两者在实现机制、性能及适用场景上有明显差异。JDK 动态代理适用于有接口的场景,而 CGLIB 更适合代理未实现接口的类,尽管性能更优但存在一些限制。开发者可根据需求选择合适的代理方式。
206 0
|
5月前
|
开发框架 Java Android开发
Java中的类反射与动态代理详解
Java中的类反射与动态代理详解
|
5月前
|
Java 数据安全/隐私保护
Java中的动态代理机制详解
Java中的动态代理机制详解