Java 动态代理

简介:

0x1 简介

相信大家对代理都不陌生,就算在项目中没有实际用到过,那你肯定也听说过代理模式,在开源组件中使用也非常广泛,如:Spring AOP功能。那今天为什么还特地来讲Java动态代理模式呢,因为近期在看Hadoop RPC源代码时发现,Hadoop RPC 就采用动态代理模式来实现,为了更好的理解Hadoop RPC底层实现,先来温故一下动态代理技术。

如果你对Java动态代理技术非常了解,并知道其底层实现原理,那你可以不用看这篇文章。
如果你对Java动态代理技术有一定了解,并知道如何使用,但不知道底层实现原理,那这篇文章值得你花时间看一下。
如果你对Java动态代理技术不了解,那你也可以通过对这篇文章的学习后,让你可以在项目中使用Java动态代理技术。

先思考一下 什么是代理?
参考百度汉语解释:http://hanyu.baidu.com/zici/s?wd=%E4%BB%A3%E7%90%86&query=%E4%BB%A3%E7%90%86&srcid=28232&from=kg0&from=kg0
代理是指:受委托代表当事人进行某种活动。如:你委托律师打官司、市长委托秘书做某些事情等等。

Java中分静态代理和动态代理,本文主要讲解动态代理的实现原理。相对动态代理来讲,静态代理的实现更加简单易懂,为了更好的理解动态代理,我们首先来看一下静态代理的实现例子。

0x2 静态代理介绍

接口:

/**
 * 登录服务接口
 */
public interface LoginService {
    void login(String username, String password);
}

目标实现类:

/**
 * 登录服务实现类
 */
public class LoginServiceImpl implements LoginService {
    @Override
    public void login(String username, String password) {
        System.out.printf("当前登录用户为:%s \n" ,username);
        //TODO 登录处理
    }
}

代理类:

/**
 * 登录服务代理类
 */
public class LoginServiceProxy implements LoginService{
    private LoginService loginService;

    public LoginServiceProxy(LoginService target){
        this.loginService = target;
    }

    @Override
    public void login(String username, String password) {
        System.out.println("登录开始,并记录当前时间...");
        this.loginService.login(username, password);
        System.out.println("登录结束,并记录当前时间...");
    }
}

测试类:

public class Main {
    public static void main(String[] args) {
        LoginServiceImpl target = new LoginServiceImpl();
        LoginServiceProxy proxy = new LoginServiceProxy(target);
        proxy.login("Joseph", "abc123");
    }
}

运行结果:

登录开始,并记录当前时间...
当前登录用户为:Joseph 
登录结束,并记录当前时间...

从运行结果可以看出,我们给目标方法调用增加了开始和结束日志打印,而这样的场景也是代理模式中比较常见的。

优缺点总结:
优点:不用修改目标对象功能的前提下,实现功能扩展,如:上例子中针对登录动作前后做了日志记录。
缺点:

  • 代理类必须实现接口,导致代理类太多;
  • 一旦接口发生变更,代理类也必须同步更新,导致维护成本会比较高;

0x3 动态代理介绍

动态代理中的动态体现在哪里呢?
静态代码是事先通过Java代码已经实现好,而动态是在运行期通过JDK API在内存中构建代理对象。通过动态代理可以很方便的实现为目标对象添加功能,如:为目标对象的所有方法都增加方法耗时记录、方法调用日志记录等功能。

动态代理实现原理介绍:
Java 的动态机制有两个重要的接口或类:

  • InvocationHandler(接口)
  • Proxy(类)

下面来看一下JDK API中的介绍:
InvocationHandler接口:

/**
 * {@code InvocationHandler} is the interface implemented by
 * the <i>invocation handler</i> of a proxy instance.
 *
 * <p>Each proxy instance has an associated invocation handler.
 * When a method is invoked on a proxy instance, the method
 * invocation is encoded and dispatched to the {@code invoke}
 * method of its invocation handler.
 *
 * @author      Peter Jones
 * @see         Proxy
 * @since       1.3
 */
public interface InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

一个代理实例调用处理的接口实现,所有代理实例必须实现InvocationHandler接口,当在代理实例上调用方法时,方法调用将会被派发给InvocationHandler实现的invoke方法处理。下面分析一下invoke方法的参数及返回值:
参数:
proxy:指我们的代理对象,即通过JDK API自动生成的对象
method:我们所要调用真实对象的某个方法的Method对象
args:调用真实对象某个方法时接受的参数

返回值:
Object:调用代理对象方法的返回值

注:光通过参数的解释可能并不是很明白,没关系,下面会通过实例对参数进行详细介绍。

Proxy类:
Proxy类提供了很多方法,但我们平时经常用到的是newProxyInstance 方法,所以重点介绍此方法,先看一方法参数的介绍:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) 
throws IllegalArgumentException

参数:
loader:由指定的ClassLoader对象来加载生成的代理对象
interfaces:一个接口数组,指代理对象需要实现的接口列表,如果指定了接口,那么代理对象将实现指定的接口,这样我们就可以通过接口来调用方法
h:一个InvocationHander对象,表示调用代理对象方法的时候会调用哪一个InvocationHander对象的invoke方法

返回值:
Object:动态生成的代理对象

下面通过一个实例来讲解上面提到的InvocationHandler 接口和Proxy类的功能,例子实现的功能跟上面静态代理的功能一致,方便进行对比:
动态代理实例代码
接口:

public interface LoginService {
    void login(String username, String password);
}

实现类:

public class LoginServiceImpl implements LoginService {
    @Override
    public void login(String username, String password) {
        System.out.printf("当前登录用户为:%s \n" ,username);
        //TODO 登录处理
    }
}

代理类:

public class LoginServiceProxy implements InvocationHandler {
    private Object obj;

    public LoginServiceProxy(Object obj){
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("登录开始,并记录当前时间...");
        Object result = method.invoke(obj, args);
        System.out.println("登录结束,并记录当前时间...");
        return result;
    }
}

测试类:

public class Main {
    public static void main(String[] args) {
        LoginService loginService = (LoginService) Proxy.newProxyInstance(LoginService.class.getClassLoader(), new
                Class[]{LoginService.class}, new LoginServiceProxy(new LoginServiceImpl()));
        loginService.login("Joseph", "test");
    }
}

运行结果:

登录开始,并记录当前时间...
当前登录用户为:Joseph 
登录结束,并记录当前时间...

从上面代码及运行结果来,有没有发现动态代理和静态代理有哪些不同点呢?粗略看着没什么区别,目前来看的确是没多大区别,也没看出动态代理的优势在哪,下面我们详细分析一下。
首先,我们来假设一个场景,比如:不改变原有方法源码的情况下,你想记录项目中部分类的方法调用耗时(前提是你需要记录的类是有接口实现的)。

假如需要记录方法耗时的类有10个,那么用静态代码方法你需要实现10个代理类,因为静态代理必须去实现原有类的接口来实现,如果用动态代理就非常简单,只需要实现一个代理类(实现InvocationHandler 接口)就可以完成10个类的方法耗时记录,大降低了开发的代码量。

到这里,也许有人会问:动态代理是实现如何的呢?
动态代码的关键就在于动态两个字,意思就是JVM会在运行时动态去创建代理类,看上面测试类:代码块,其中Proxy.newProxyInstance就实现了动态创建代理的功能,为了更好的理解这个功能,我们把测试类稍微做改动,如下:

public class Main {
    public static void main(String[] args) {
        LoginService loginService = (LoginService) Proxy.newProxyInstance(LoginService.class.getClassLoader(), new
                Class[]{LoginService.class}, new LoginServiceProxy(new LoginServiceImpl()));
        System.out.println(loginService.getClass().getName());
        loginService.login("Joseph", "test");
    }
}

加了第5代码,我们再来看看运行结果:

com.sun.proxy.$Proxy0
登录开始,并记录当前时间...
当前登录用户为:Joseph 
登录结束,并记录当前时间...

有人会问怎么打印出来是$Proxy0,没错,这就是JVM自动生成的代理类。那么,新的问题来了:
1、$Proxy0代理类和LoginService接口有什么关系?
2、$Proxy0代理类和LoginServiceImpl 实现类有什么关系?
3、$Proxy0代理类和LoginServiceProxy 类有什么关系?

我们带着这三个问题继续思考,怎么才能很好的解答这三个问题呢,源码、源码、源码,重要的事情说三遍。下面我们看看创建代码的源码,从Proxy.newProxyInstance入口开始,

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);
    }
}

重点关注第17行代码,其余代码不做分析,继续进入getProxyClass0方法看源码:

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // 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);
}

这个方法非常简单,先做接口列表长度判断,超出65535个接口直接抛出异常,不过哪类会实现这么多接口?我们不关心这个,直接看第10返回代码,从缓存中获取对象,看一下proxyClassCache 缓存的定义,

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

重点关注ProxyClassFactory 代理类工厂类中的apply方法,

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
    //代码省略
    
    /*
     * Generate the specified proxy class.
     */
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
    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());
    }
}

由于源代码较多,直接省略无关代码,直接看第8行核心代码,通过ProxyGenerator.generateProxyClass方法生成代理类的字节数组,再调用第10defineClass0本地方法返回Class对象。
上术代码中最关键的是ProxyGenerator.generateProxyClass方法,今天不讲这个方法的实现,我们利用这个方法来生成二进制文件来分析代码类的源码,我们再改一下测试类代码,如下:

public class Main {
    public static void main(String[] args) throws IOException {
        LoginService loginService = (LoginService) Proxy.newProxyInstance(LoginService.class.getClassLoader(), new
                Class[]{LoginService.class}, new LoginServiceProxy(new LoginServiceImpl()));
        System.out.println(loginService.getClass().getName());
        loginService.login("Joseph", "test");

        //生成代理类二进制文件
        String className = "ProxyTest";
        byte[] data = ProxyGenerator.generateProxyClass(className, new Class[]{LoginService.class});
        FileOutputStream out = new FileOutputStream(className + ".class");
        out.write(data);
        out.close();
    }
}

运行后,会在工程目录下生成ProxyTest.class文件,

直接用idea打开,源码如下:

public final class ProxyTest extends Proxy implements LoginService {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public ProxyTest(InvocationHandler var1) throws  {
        super(var1);
    }

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

    public final String toString() throws  {
        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 login(String var1, String var2) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1, var2});
        } catch (RuntimeException | Error var4) {
            throw var4;
        } catch (Throwable var5) {
            throw new UndeclaredThrowableException(var5);
        }
    }

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

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.joseph.java.proxy.dynamicproxy.LoginService").getMethod("login", new Class[]{Class.forName("java.lang.String"), Class.forName("java.lang.String")});
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

通过生成的源代码就不难回答上面提出的3个问题了,
1、代理类实现了LoginSerivce接口,并实现login方法
2、代理类和真正的接口实现类LoginServiceImpl 没有直接关系
3、LoginServiceProxy类作为构造函数参数传入代理类中,并在业务方法login中调用LoginServiceProxy类的invoke方法

到这里已经把Java动态代理的原理讲完了,从怎么使用到动态代理创建的过程,但生成动态代理类字节数组的源码没有详细分析,这个留给大家自己去分析源码,ProxyGenerator.generateProxyClass这个方法中,主要思想就是反映机制来生成类及方法。

0x4 总结

Java动态代理相比静态代理比较方便灵活、具有扩展性,使用起来也没有难度,所以建议在项目中使用动态代理。
那动态代理有没有不足的地方呢?
动态代理是针对接口实现的,不能对类进行代理,我也不知道这算不算缺点,至少是一种限制吧。

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