写点什么

面试中经常问到的动态代理到底是什么

用户头像
废材姑娘
关注
发布于: 2021 年 02 月 23 日
面试中经常问到的动态代理到底是什么

面试官: 你了解 Spring AOP 吗?

我: 了解

面试官: 那你知道 Spring AOP 的实现原理是什么吗?

我: 动态代理

面试官:那 Java 的代理有几种不同的方式, 他们有什么区别呢?

我:不就是 Java 动态代理吗?

面试官: 出门右拐,不送, 谢谢


开篇

上面的面试场景是不是特别熟悉, 不瞒大家说,我就是其中的一员。动态代理到底是什么?网上也是各种说法不一, 直到我第三次学习了设计模式--代理模式。我才恍然大悟,这不就是 Spring AOP 的动态代理吗? 为什么这么说呢,请听我慢慢道来。

在《设计模式》这本说中对代理模式的定义是“为另一个对象提供一个替身或占位符,以控制对这个对象的访问”, 我学习极客时间的小争哥的《设计模式专栏》时, 对代理模式的定义是:“在不改变原始类的代码的情况下,通过引入代理类来给原始类附加功能”。所以通过定义我们可以看出这不就是和我们在写 Spring AOP 是干的事情是一样的吗, 我们会在 AOP 中进行权限校验, 也会在 AOP 中打印日志。这不就是代理模式的典型吗。所以掌握了代理模式的是想原理, 你就掌握了 Spring AOP 的实现原理。那代理模式有几种实现方式呢?对, 你说对了, 静态代理和动态代理,那二者有什么区别呢?让我们一起学习下。

为了说明不同的代理方式的不同实现, 我们先定义一个 UserServiceImpl 类,且该类实现接口 UserService, 具体代码如下:

public interface UserService {    String createUser(String username);}
复制代码


public class UserServiceImpl implements UserService {    @Override    public String createUser(String username) {        return "[create user]: " + username;    }}
复制代码

Java 静态代理

静态代理是通过对被代理类的方法进行包装改写的方式完成的, 主要的实现方式是实现接口或继承的方式实现, 下面我们就看一下两种方式的实现有啥不同的。

实现方式一: 实现接口
public class InterfaceStaticProxy implements UserService {    private final UserService userService;
public InterfaceStaticProxy(UserService echoService) { this.userService = echoService; }
@Override public String createUser(String username) { long startTime = System.currentTimeMillis(); String result = userService.createUser(username); long costTime = System.currentTimeMillis() - startTime; System.out.println("基于接口实现的静态代理, 创建用户耗时:" + costTime + " ms."); return result; }}
复制代码
实现方式二:继承

基于接口的实现方式适用于有接口的情况下, 如果我们使用的第三方库,而要代理的对象并没有定义接口则无法使用, 需要通过继承类来实现。

public class ExtendStaticProxy extends UserServiceImpl {    private final UserServiceImpl userService;
public ExtendStaticProxy(UserServiceImpl userService) { this.userService = userService; }
@Override public String createUser(String username) { long startTime = System.currentTimeMillis();
String result = super.createUser(username);
long costTime = System.currentTimeMillis() - startTime; System.out.println("基于继承实现的静态代理, 创建用户耗时:"+ costTime +"ms"); return result; }}
复制代码

以上两种方式的具体使用方式如下:

public class StaticProxyDemo {
public static void main(String[] args) { InterfaceStaticProxy interfaceStaticProxy = new InterfaceStaticProxy(new UserServiceImpl()); String userOne = interfaceStaticProxy.createUser("user one"); System.out.println(userOne);
ExtendStaticProxy extendStaticProxy = new ExtendStaticProxy(new UserServiceImpl()); String userTwo = extendStaticProxy.createUser("user two"); System.out.println(userTwo); }}
复制代码
​静态代理存在问题

会发现, 静态代理我们需要将被代理类中的所以方法都实现一边, 并且需要为被代理类格外定义一个类来代理, 这导致如果我们有很多个类需要代理,则会出现类的个数成倍的增长。为了解决这个问题就出现了动态代理。

Java 动态代理

动态代理不需要提前定义代理类,而是在运行时动态的为原始类创建代理类。 根据实现的机制不同,Java 的动态代理主要分为基于反射机制的动态代理(JDK 动态代理)和基于字节码的动态代理(如 cglib, javassist 和 Byte Buddy)。接下来,我们会逐一分析不同的方式实现的动态代理的原理, 并给出简单的 demo。

JDK 动态代理(基于反射原理实现)

原理: 反射 + 接口

针对 JDK 动态代理创建代理对象,需要使用 Proxy 类的 newInstance 方法。这个方法有三个参数:

    public static Object newProxyInstance(ClassLoader loader,                                          Class<?>[] interfaces,                                          InvocationHandler h) throws IllegalArgumentException {}
复制代码
  • ClassLoader loader: 类加载器,用于定义该代理类。

  • Class<?>[] interfaces: 一个 Class 对象数组,都是该代理类需要实现的接口。

  • InvocationHandler h: 一个调用处理器,实现该代理器只有一个方法,无论何时调用代理对象的方法,该方法都会被调用,并根据 Method 和原始调用参数, 调用合适的接口方法。


下面我们就看看 JDK 动态代理的实现, 首先最重要的是要实现 InvocationHandler,​具体代码如下:

public class DynamicProxyHandler implements InvocationHandler {
private final Object proxyObject;
public DynamicProxyHandler(Object proxyObject) { this. = proxyObject; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = method.invoke(proxyObject, args); long costTime = System.currentTimeMillis() - startTime; System.out.println("JDK 动态代理实现, 创建用户耗时:" + costTime + "ms"); return result; }}
复制代码

JDK 动态代理的具体使用方式如下:

public class JDKDynamicProxy {
public static void main(String[] args) { UserServiceImpl userService = new UserServiceImpl(); Class<?>[] interfaces = userService.getClass().getInterfaces(); DynamicProxyHandler proxyHandler = new DynamicProxyHandler(userService);
UserService proxyInstance = (UserService)Proxy.newProxyInstance(JDKDynamicProxy.class.getClassLoader(), interfaces, proxyHandler); String userThree = proxyInstance.createUser("user three"); System.out.println(userThree); }}
复制代码

JDK 代理的特性

  • 所有代理类都扩展于 Proxy 类。

  • 所有代理类都覆盖了 Object 类中的方法 toString, equals 和 hashCode.

  • 没有定义代理类的名字, 会生成一个以字符串 $Proxy 开头的类名

  • 对于特定的类加载器和预设的一组接口来说,只能生成一个代理类,也就是说如果使用同一个类加载器和接口数组调用两次 newProxyInstance()方法,只能得到同一个类的两个对象。

  • 代理类只能是 public 和 final

  • 被代理类必须实现了接口

cglib 动态代理(字节码增强)

Github 代码仓库: https://github.com/cglib/cglib

原理: 继承机制

学习完 JDK 动态代理后, 你可能会有一个疑问,既然有了 JDK 的动态代理, 为什么还需要 Cglib 动态代理呢?前面我们总结了 JDK 动态代理的特性时说到被代理的类必须实现了接口, 但是大家都知道,现实怎么会永远遂人愿呢。为了完成对没有实现接口的代理, 我们就需要从字节码提升的的手段, 在运行时进行代理类的创建。

 这里为了表明 cglib 动态代理不需要实现接口, 我们重新定义一个 ActivityService, 代码如下:

public class ActivityService {
public String crateActivity(String message) {
return "create activity" + message; }}
复制代码

那使用 cglib 实现的动态代理的实现如下:

public class CglibDynamicProxyDemo {
public static void main(String[] args) { Enhancer enhancer = new Enhancer();
// 使用类创建代理 Class<ActivityService> superClass = ActivityService.class; enhancer.setSuperclass(superClass); enhancer.setCallback(new MethodInterceptor() {
@Override public Object intercept(Object source, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { long startTime = System.currentTimeMillis(); // Source -> CGLIB 子类 // 目标类 -> ActivityService // 错误使用// Object result = method.invoke(source, args); // 正确的方法调用 Object result = methodProxy.invokeSuper(source, args); long costTime = System.currentTimeMillis() - startTime; System.out.println("[CGLIB 字节码提升]动态代理, 创建活动耗时:" + costTime + " ms."); return args[0]; } });
ActivityService o = (ActivityService)enhancer.create(); String userFour = o.crateActivity("activity one"); System.out.println(userFour);
}}
复制代码

总结

今天的 Java 代理相关的内容就讲完了, Spring AOP 的实现原理就是通过 JDK 动态代理和 cglib 动态代理来实现的。下次面试官再问题这个问题的时候, 你就不用紧张了, 至少可以战斗 300 回合。


发布于: 2021 年 02 月 23 日阅读数: 41
用户头像

废材姑娘

关注

废材姑娘 2018.01.24 加入

大家叫我双儿,梦想着成为韦小宝的老婆 欢迎关注我的个人公众号----废材姑娘,回复“双儿”加我微信,让我们一起探索多彩的世界。

评论

发布
暂无评论
面试中经常问到的动态代理到底是什么