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

目录
相关文章
|
18天前
|
缓存 安全 Java
Java中动态代理使用与原理详解
Java中动态代理使用与原理详解
57 0
|
18天前
|
Java
Java之动态代理的详细解析
2. 动态代理 2.1 好处: 无侵入式的给方法增强功能 2.2 动态代理三要素: 1,真正干活的对象
40 0
|
18天前
|
Java
【Java动态代理】—— 每天一点小知识
【Java动态代理】—— 每天一点小知识
SpringJDK动态代理实现,2024Java面试真题精选干货整理
SpringJDK动态代理实现,2024Java面试真题精选干货整理
|
18天前
|
Java API 开发者
解密Java反射机制与动态代理
解密Java反射机制与动态代理
18 0
|
18天前
|
设计模式 Java 测试技术
滚雪球学Java(25):动态代理
【4月更文挑战第14天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
14 0
滚雪球学Java(25):动态代理
|
18天前
|
监控 Java 数据安全/隐私保护
Java中的动态代理
Java中的动态代理
|
18天前
|
设计模式 缓存 算法
Java 的静态代理和动态代理
Java 的静态代理和动态代理
12 0
|
18天前
|
设计模式 Java 索引
由反射引出的Java动态代理与静态代理
由反射引出的Java动态代理与静态代理
16 0
|
18天前
|
Java API Spring
Java基础教程(13)-Java中的反射和动态代理
【4月更文挑战第13天】Java反射机制允许程序在运行时获取类的信息并调用其方法。Class类是基础,提供获取类属性和方法的能力。通过Class对象,可以操作实例字段和方法,如getField、getDeclaredField等。动态代理是Java提供的创建接口实例的机制,其中JDK动态代理需目标类实现接口,而Cglib则可代理未实现接口的类。动态代理涉及Proxy和InvocationHandler接口。