动态代理

Mr.ZhangJava大约 14 分钟

动态代理

为什么基于JDK实现的动态代理要求原始类有接口?

前面我们讲到,反射、注解、动态代理是在框架开发中经常用到的3个高级Java语法。在上两节中,我们讲解了其中的反射和注解。本节,我们讲解剩下的动态代理。我们熟悉的Spring AOP、Dubbo RPC等框架都有用到动态代理。在平时的开发中,我们也经常利用动态代理,为代码添加额外的功能,比如日志、事务、鉴权限流、性能监控等。在面试中,我们也经常被问及动态代理相关的问题,比如为什么基于JDK实现的动态代理必须要求原始类有接口?带着这个问题,我们开始今天的学习。

一、什么是静态代理

在《设计模式之美》中,我们有讲到代理模式。代理模式能够在不改变原始类的代码的情况下,通过引入代理类来给原始类附加功能。代理模式分为静态代理和动态代理。我们先通过一个例子来看静态代理是如何工作的。

示例代码如下所示,我们有一个UserController类,其中包含register()和login()两个函数。

public interface IUserController {
  UserVo login(String telephone, String password);
  UserVo register(String telephone, String password);
}

public class UserController implements IUserController {
  @Override
  public UserVo login(String telephone, String password) {
    //...省略逻辑...
  }

  @Override
  public UserVo register(String telephone, String password) {
    //...省略逻辑...
  }
}

如果我们希望统计login()和register()这两个函数的执行时间,那么,简单的做法是:直接修改这两个函数的代码,在函数内部添加时间统计代码,但是,这会导致非业务代码跟业务代码耦合。为了代码的结构更加优美,我们可以使用代理模式,让非业务代码与业务代码解耦,如下所示,代理类UserControllerProxy和原始类UserController实现相同的接口IUserController。UserController类只负责业务功能。UserControllerProxy类负责在业务代码执行前后附加非业务代码,并通过委托的方式,调用原始类来执行业务代码。

public class UserControllerProxy implements IUserController {
  private static final Logger logger = LoggerFactory.getLogger(UserControllerProxy.class);
  private UserController userController;

  public UserControllerProxy(UserController userController) {
    this.userController = userController;
  }

  @Override
  public UserVo login(String telephone, String password) {
    long startTime = System.currentTimeMillis();
    UserVo userVo = userController.login(telephone, password);
    long costTime = System.currentTimeMillis() - startTime;
    logger.info("UserController#login cost time:" + costTime);
    return userVo;
  }

  @Override
  public UserVo register(String telephone, String password) {
    long startTime = System.currentTimeMillis();
    UserVo userVo = userController.register(telephone, password);
    long costTime = System.currentTimeMillis() - startTime;
    logger.info("UserController#register cost time:" + costTime);
    return userVo;
  }
}

因为原始类和代理类实现了相同的接口,所以,我们可以轻松将代码中的原始类对象,统一替换为代理类对象。如下代码所示。

// 原来的代码
IUserController userController = new UserController();

// 替换之后的代码
IUserController userController = new UserControllerProxy(new UserController());

不过,如果原始类并没有实现接口,并且原始类代码并不是我们开发维护的(比如它来自一个第三方的类库),我们也没办法直接修改原始类,给它重新定义一个接口。在这种情况下,我们该如何实现代理模式呢?对这种外部类的扩展,我们一般都是采用继承的方式来实现。这里也不例外。我们让代理类继承原始类,然后扩展附加功能。代码如下所示。

public class UserControllerProxy extends UserController {
  private static final Logger logger = LoggerFactory.getLogger(UserControllerProxy.class);

  @Override
  public UserVo login(String telephone, String password) {
    long startTime = System.currentTimeMillis();
    UserVo userVo = super.login(telephone, password);
    long costTime = System.currentTimeMillis() - startTime;
    logger.info("UserController#login cost time:" + costTime);
    return userVo;
  }

  @Override
  public UserVo register(String telephone, String password) {
    long startTime = System.currentTimeMillis();
    UserVo userVo = super.register(telephone, password);
    long costTime = System.currentTimeMillis() - startTime;
    logger.info("UserController#register cost time:" + costTime);
    return userVo;
  }
}

当然,基于继承实现的代理类,我们也可以轻松地讲代码中的原始类对象,替换为代理类对象, 如下所示。

// 原来的代码
UserController userController = new UserController();

// 替换之后的代码
UserController userController = new UserControllerProxy();

上述方式实现的代理叫做静态代理。虽然静态代理实现非常简单,但它会导致项目中的类成倍增加。比如,当项目中所有的Controller类都需要添加时间统计功能时,我们需要为了所有的Controller类都创建对应的代理类。显然,这样做费时费力,并且不够优雅。于是,动态代理便被发明,用来解决这个问题。

二、什么是动态代理

静态和动态两个概念在前面章节中也反复提到,一般来讲,静态指的是某个事件发生在编译时,动态指的是某个事件发生在运行。将这个规则应用到代理模式,那么,静态代理指的就是在编译时生成代理类的字节码(代理类的Class文件),动态代理指的就是在运行时生成代理类的字节码。代理类的字节码仅仅存在于内存中,并不会生成对应的class文件,从而避免了静态代理需要编写大量代理类的问题。

之所以可以实现动态代理,是因为JVM设计得非常灵活,只要是符合类的格式的字节码,都可以在运行时被JVM解析并加载,不管这个字节码是来自预先编译好的(class文件),还是在内存中临时生成的(典型应用:动态代理),又或者从网络加载而来的(典型应用:Applet)。这部分内容涉及到JVM的类加载机制,我们在后面的章节中再详细讲解。

实际上,一句话概括一下,动态代理就是动态地生成代理类的字节码。动态代理实现的方式有两类,一类是使用JDK提供的类来实现,一类是使用第三方的字节码类库来实现,比如CGLIB、BECL、ASM、Javassit等。字节码类库功能非常强大,可以动态修改字节码、生成字节码,所以,生成代理类的字节码也不在话下。不过,字节码比较底层,直接编辑字节码,对于程序员是一个很大挑战。因此,我们一般采用JDK提供的类或者CGLIB这种使用起来比较友好的字节码类库来实现动态代理。接下来,我们就介绍一下这两种常用的动态代理实现方式。

三、基于JDK实现动态代理

我们从基本用法、实现原理、性能分析三个方面,来分析一下基于JDK的动态代理实现方式。

1)基本用法

我们来看下如何使用JDK提供的类,为UserController类实现动态代理,代码如下所示。当我们为其他Controller类中的方法也添加时间统计代码时,我们可以复用CtrlProxyHandler类,并通过Proxy类的newProxyInstance()静态方法生成对应的代理类对象。

public class CtrlProxyHandler implements InvocationHandler {
  private Object origBean;

  public CtrlProxyHandler(Object origBean) {
    this.origBean = origBean;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    long startTime = System.currentTimeMillis();
    Object res = method.invoke(origBean, args);
    long costTime = System.currentTimeMillis() - startTime;
    System.out.println(origBean.getClass().getSimpleName() + "#"
        + method.getName() + " cost time: " + costTime);
    return res;
  }
}

public class JDKProxyDemo {
  public static void main(String[] args) {
    IUserController userController = new UserController();
    CtrlProxyHandler handler = new CtrlProxyHandler(userController);
    IUserController userControllerProxy = (IUserController) Proxy.newProxyInstance(
        handler.getClass().getClassLoader(), UserController.class.getInterfaces(), handler);
    userControllerProxy.login("139********", "******");
  }
}

从上述代码,我们可以发现,基于JDK来实现动态代理主要用到了java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy类。其中,InvocationHandler接口比较简单,只包含一个invoke()函数,如下所示。

img
img

简单了解InvocationHandler接口之后,我们再来看下Proxy类。Proxy类要比InvocationHandler复杂很多。使用Proxy类生成代理类和实例化对象的方法如下所示。

// Foo为接口
InvocationHandler handler = new MyInvocationHandler(...); // InvocationHandler
Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class); //生成代理类
Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler); //实例化对象

大部分情况下,我们只需要用到代理类的实例化对象,所以,上述代码中的后两行代码可以简化为如下一行代码。

Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[] { Foo.class }, handler);

2)实现原理

接下来,通过分析newProxyInstance()函数的源码,我们来看下基于JDK的动态代理的底层实现原理。newProxyInstance()函数定义如下所示。newProxyInstance()函数中包含3部分逻辑:生成动态代理类、加载动态代理类到JVM、实例化动态代理类对象。其中,参数loader表示类加载器,用来加载生成的动态代理类。参数interfaces表示接口。JDK依据接口生成动态代理类,接口中包含哪些方法,动态代理类中就包含哪些方法。参数h用于实例化动态代理类对象。

img
img

我们先来看下生成动态代理类的过程。如下代码所示,newProxyInstance()函数调用ProxyGenerator类(JDK提供的生成字节码的类),按照类的字节码格式,生成动态代理类的字节码,并存储到内存(proxyClassFile)中。

// 生成动态代理类的名称
final String proxyClassNamePrefix = "$Proxy";
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
// ProxyGenerator类似字节码类库,可以生成动态代理类的字节码
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
try {
    // 通过JVM的类加载器来加载动态代理类
    return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) { // 如果生成的动态代理类的字节码格式有误,则报错
    throw new IllegalArgumentException(e.toString());
}

从上述动态代理类的生成过程,我们可以发现,动态代理类具有哪些方法,只跟接口有关,跟原始类没有任何关系。这也是基于JDK实现的动态代理,要求原始类必须有接口定义才行的原因。除此之外,加载到JVM中的类都要有类名,这样才能在实例化对象时按照类名查找到对应的类的定义。静态代理类的类名是程序员指定的,动态代理类的类名是自动生成的。如上代码所示,动态代理类的类名由2部分构成:Proxy+自增编号。比如,如果项目中使用Proxy类生成了两个动态代理类,那么,它们的名称就分别为:Proxy0、Proxy1。

以上讲解的动态代理类生成的过程如下图所示。

img
img

上图只展示动态代理类大概的样子,那么,动态代理类具体长什么样子呢?每个函数都是如何实现的呢?我们可以将通过ProxyGenerator类生成的动态代理类的字节码,保存在文件中,然后再通过反编译工具,得到对应的Java文件。这样就能知道动态代理类具体长什么样子了。动态代理类字节码的保存方法如下代码所示,

public class ProxyUtils {
  public static void main(String[] args) {
    byte[] byteCodes = ProxyGenerator.generateProxyClass(
        "CtrlProxy", new Class[] { IUserController.class });
    try (FileOutputStream out = new FileOutputStream("/Users/wangzheng/CtrlProxy.class")) {
      out.write(byteCodes);
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

执行上述代码,我们得到CtrlProxy.class字节码文件,然后再利用反编译工具,反编译CtrlProxy.class字节码文件,于是,就得到对应的Java文件,如下所示。m3、m4对应IUserController接口中定义的方法,也就是login()和register()。m0、m1、m2为继承自顶级父类Object类的方法,也就是toString()、equals()、hashCode()。

public final class CtrlProxy extends Proxy implements IUserController {
  private static Method m1;
  private static Method m4;
  private static Method m2;
  private static Method m3;
  private static Method m0;

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

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

  public final UserVo register(String var1, String var2) throws  {
    try {
      return (UserVo)super.h.invoke(this, m4, new Object[]{var1, var2});
    } catch (RuntimeException | Error var4) {
      throw var4;
    } catch (Throwable var5) {
      throw new UndeclaredThrowableException(var5);
    }
  }

  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 UserVo login(String var1, String var2) throws  {
    try {
      return (UserVo)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);
    } catch (RuntimeException | Error var2) {
      throw var2;
    } catch (Throwable var3) {
      throw new UndeclaredThrowableException(var3);
    }
  }

  static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
      m4 = Class.forName("demo.proxy.IUserController").getMethod("register", 
                        Class.forName("java.lang.String"), Class.forName("java.lang.String"));
      m2 = Class.forName("java.lang.Object").getMethod("toString");
      m3 = Class.forName("demo.proxy.IUserController").getMethod("login", 
                        Class.forName("java.lang.String"), Class.forName("java.lang.String"));
      m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    } catch (NoSuchMethodException var2) {
      throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
      throw new NoClassDefFoundError(var3.getMessage());
    }
  }
}

上述代码中,类型为InvocationHandler的super.h的值,在实例化动态代理类对象时,赋值为自定义的InvocationHandler实现类对象(比如CtrlProxyHandler类对象)。如下简化之后的newProxyInstance()函数代码所示。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
    // 1) 生成动态代理类
    // 2) 加载动态代理类
    Class<?> cl = getProxyClass0(loader, intfs);
    
    // 3)实例化动态代理类对象
    final Class<?>[] constructorParams = { InvocationHandler.class };
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    return cons.newInstance(new Object[]{h});
}

当我们调用动态代理类上方法时,比如调用login()方法,login()方法会调用CtrlProxyHandler上的invoke()方法。invoke()方法执行一些附加逻辑,然后再拿传递过来的方法和参数,利用反射在原始类对象上执行,如下图所示。

img
img

3)性能分析

从上述对基于JDK实现动态代理的原理分析,我们可以发现,基于JDK实现动态代理,耗时的地方主要有两处:一是运行时动态生成代理类的字节码,二是利用反射执行方法。

对于反射执行方法的性能,我们在第24节讲解反射时已经分析过了。反射会额外增加一部分耗时,但如果方法中的逻辑比较复杂,执行时间比较长,那么相比而言,反射额外增加的耗时可以忽略。相反,如果方法中的逻辑比较简单,执行时间很短,那么反射额外增加的耗时就不能忽略了。对于动态生成代理类的字节码,可想而知,这部分耗时确实会比较多。对比静态代理,静态代理类的字节码是在编译时生成的,不占用运行时间。

四、基于CGLIB实现动态代理

基于CGLIB实现动态代理,跟基于JDK实现动态代理相比,基本用法和实现原理非常类似。前面已经详细讲解了基于JDK的动态代理实现方式,接下来,我们对基于CGLIB的动态代理实现方式就简单介绍一下。

使用CGLIB需要先在项目中引入CGLIB包,Maven配置如下所示

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

基于CGLIB实现动态代理,也要用到一个核心接口MethodInterceptor和一个核心类Enhancer,它们的用法跟JDK中的InvocationHandler接口和Proxy类相似,示例代码如下所示。这里我就不详细讲解用法了,留给你自己查阅。

public class ProxyFactory implements MethodInterceptor {
  private Object origBean;
  
  public ProxyFactory(Object origBean) {
    this.origBean = origBean;
  }

  @Override
  public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
      throws Throwable {
    long startTime = System.currentTimeMillis();
    Object res = method.invoke(origBean, args);
    long costTime = System.currentTimeMillis() - startTime;
    System.out.println(origBean.getClass().getSimpleName() + "#"
        + method.getName() + " cost time: " + costTime);
    return res;
  }
}

public class CGLIBProxyDemo {
  public static void main(String[] args) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(UserController.class);
    enhancer.setCallback(new ProxyFactory(new UserController()));
    UserController userControllerProxy = (UserController) enhancer.create();
    userControllerProxy.login("", "");
  }
}

在讲解静态代理时,我们提到,静态代理也有两种实现方式,一种是基于接口,一种基于继承。同理,动态代理也有基于接口和基于继承这两种实现方式。基于JDK的动态代理实现方式,使用接口来生成动态代理类,要求原始类必须定义接口,因此,是一种基于接口的动态代理实现方式。基于CGLIB的动态代理实现方式,并不依赖接口,通过继承原始类来生成动态代理类,因此,是一种基于继承的动态代理实现方式。

基于CGLIB的动态代理实现方式的实现原理,跟基于JDK的动态代理的实现方式的实现原理,大致是一样的,根据接口或原始类,在运行时,动态生成代理类字节码。在执行代理类对象上的方法时,委托给实现了InvocationHandler或者MethodInterceptor接口的类对象,使用反射执行方法,并附加执行额外代码。因此,基于CGLIB的动态代理实现方式,耗时的地方也是生成动态代理类字节码和利用反射执行方法。

实际上,为了优化性能,CGLIB可以不使用Java反射来执行方法,如下代码所示。MethodProxy的invokeSuper()方法底层依赖CGLIB自己实现的反射(也就是net.sf.cglib.reflect.FastClass)来执行方法。不过,即便如此,两种动态代理实现方式的性能仍然相差无几。甚至在高版本的JDK中,基于JDK实现的的动态代理,比基于CGLIB实现的动态代理,性能还要好。

public class ProxyFactory implements MethodInterceptor {
  private Object origBean;
  
  public ProxyFactory(Object origBean) {
    this.origBean = origBean;
  }

  @Override
  public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
      throws Throwable {
    long startTime = System.currentTimeMillis();
    //Object res = method.invoke(origBean, args); //Java反射
    Object res = methodProxy.invokeSuper(obj, args); //CGLIB反射
    long costTime = System.currentTimeMillis() - startTime;
    System.out.println(origBean.getClass().getSimpleName() + "#"
        + method.getName() + " cost time: " + costTime);
    return res;
  }
}

五、课后思考题

请描述一下Spring AOP的底层实现原理

Loading...