前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >10分钟带你彻底搞懂动态代理机制

10分钟带你彻底搞懂动态代理机制

原创
作者头像
写bug的高哈哈
发布2025-02-02 11:55:03
发布2025-02-02 11:55:03
840
举报

在面向对象的世界中,对象与对象之间的相互协作构成了系统的运行状态。通常,我们可以在一个对象中直接引用另一个对象来获取想要的功能,但有时候事情并没有那么简单。我们来看一段简单的代码示例:

代码语言:java
复制
@Service
public class HealthService {
    public void recordUserHealthData(HealthData data) {
    	healthRepository.recordUserHealthData(data);
    	logger.info("Record user health data successfully.");
    }
    …
}

上述代码很简单,是在 Service 层组件中调用数据访问层组件,并记录一个操作日志。现在假设这个 HealthService 中有很多方法,而对所有方法操作都需要添加日志。显然,在每个方法里都手工调用同一个日志方法不是一种很好的解决方案,会造成代码冗余,增加维护成本。

这个时候,代理机制就可以派上用场了。我们可以构建一个代理对象,然后由这个代理对象统一实现日志记录操作。

图 1 基于代理机制的对象交互示意图
图 1 基于代理机制的对象交互示意图

可以看到,通过代理机制,一个对象就可以在承接另一个对象功能的基础之上,同时添加新的功能。相比直接在原有对象中嵌入代码,代理机制为我们提供了更为优雅的解决方案。

那么,代理机制具体是如何实现的呢?让我们一起来看一下。

代理机制实现方式

代理机制一般有两种实现方式,一种是 静态代理机制,一种是 动态代理机制。一般来说静态机制设计和实现上比较容易理解,而动态机制则较为复杂,所以我们先来学习一下静态机制。

静态代理

静态代理机制在技术上比较简单,我们先看这样一个示例。

我们考虑有一个 Account 接口,包含一个用于开设账户的 open() 方法。

代码语言:java
复制
public interface Account{
    void open();
}

然后针对该接口有一个实现类 RealAccount,提供了对 open() 方法的模拟实现。

代码语言:java
复制
public class RealAccount implements Account {
    private String name;
    public RealAccount(String name) {
        this.name = name;
    }
    @Override
    public void open() {
        System.out.println("开账户: " + name);
    }
}

接下来就是代理类的实现,我们称之为 ProxyAccount,代码如下所示:

代码语言:java
复制
public class ProxyAccount implements Account {
    private Account account;
    private String name;
    public ProxyAccount(String name) {
        this.name = name;
    }
    @Override
    public void open() {
        checkName(name);
        if (account == null) {
           account = new RealAccount(name);
        }
        account.open();
    }
    private void checkName(String name) {
        System.out.println("验证用户名称: " + name);
    }
}

可以看到 ProxyAccount 同样实现了 Account 接口,并保存了一个 RealAccount 实例。这样,ProxyAccount 对象的 open() 方法既包含了 RealAccount 中的原有逻辑,又额外代理了验证用户名称的逻辑。它们之间的类层结构如下图所示。

图 2 静态代理类层结构图
图 2 静态代理类层结构图

跟动态代理相比,静态代理模式中的代理关系是 编码阶段就能决定的,所以容易管理,也不存在因为外部代理框架而造成的性能损耗。你在 Mybatis 的缓存和连接池等的实现机制中,都能看到静态代理模式的应用场景。

动态代理

介绍完静态代理,我们接着来看动态代理。在 Java 世界中,想要实现动态代理,可以使用 JDK 自带的代理机制。

现在假设同样存在静态代理中 Account 接口以及实现类 RealAccount,然后我们需要再调用其 open() 方法的前后记录操作日志。

在 JDK 自带的动态代理中存在一个 InvocationHandler 接口,想要实现动态代理,我们首先要做的就是提供该接口的一个实现类。

代码语言:java
复制
public class AccountHandler implements InvocationHandler{
    private Object obj;
    public AccountHandler(Object obj) {
        super();
        this.obj = obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] arg)
           throws Throwable {
        Object result = null;
        doBefore();
        result = method.invoke(obj, arg);
        doAfter();
        return result;
    }
    public void doBefore() {
        System.out.println("开户前");
    }
    public void doAfter() {
        System.out.println("开户后");
    }
}

InvocationHandler 接口中包含一个 invoke() 方法,我们必须实现这一方法。在该方法中,我们通常需要调用 method.invoke() 方法执行原有对象的代码逻辑,然后可以在该方法前后添加相应的代理实现。在上述代码中,我们只是简单打印了日志。

然后,我们编写测试类来应用上述 AccountHandler 类,如下所示。

代码语言:java
复制
public class AccountTest {
    public static void main(String[] args) {
        Account account = new RealAccount("xiaoyiran");
        InvocationHandler handler = new AccountHandler(account);
        Account proxy = (Account)Proxy.newProxyInstance(
               account.getClass().getClassLoader(),
               account.getClass().getInterfaces(),
               handler);
        proxy.open();
    }
}

这里,Proxy.newProxyInstance() 方法的作用就是生成 RealAccount 类的代理类。当该方法被调用时,RealAccount 类的实例就会注入到这个代理类中。然后当代理类的 open() 方法被调用时,AccountHandler 中 invoke() 方法就会被执行,从而执行代理方法。这里的类层结构是这样的。

图 3 基于 JDK 动态代理的类层结构图
图 3 基于 JDK 动态代理的类层结构图

仔细分析上述代码结构,可以看到针对某一个业务结构,我们分别提供了一个实现类以及对应的代理类。通过 JDK 提供的动态代理机制,我们可以把这些类整合在一起。而在代码实现上,我们也可以发现其遵循这样一个流程:设计和实现业务接口→实现 Handler 类→创建代理类,然后在 Handler 类中构建具体的代理逻辑。上述流程也是代表了一种标准的代理机制实现流程。我们可以联想一下,有很多基于 AOP 机制的拦截器,它们的实现机制实际上就是类似的原理。

代理机制在开源框架中的应用

关于 JDK 自带的动态代理机制,在 Dubbo 和 Mybatis 框架中都得到了应用,其中 Dubbo 主要使用动态代理实现远程方法的调用,而 Mybatis 则基于这一机制来完成数据访问。我们将分别对这两种场景展开讨论,先来看 Dubbo 远程访问中的代理机制。

Dubbo 远程访问中的代理机制

我们在 Dubbo 代码中找到了如下所示的 JdkProxyFactory 类,用来获取代理对象。

代码语言:java
复制
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
}

这里你可以看到 Proxy.newProxyInstance() 方法,这是典型的 JDK 动态代理的用法。根据传入的接口获得动态代理类,当调用这些接口的方法时都会转而调用 InvokerInvocationHandler(invoker)。我们来看一下 InvokerInvocationHandler 类。基于 JDK 动态代理的实现机制,可以想象 InvokerInvocationHandler 类必定实现了 InvocationHandler 接口。

代码语言:java
复制
public class InvokerInvocationHandler implements InvocationHandler {
    private final Invoker<?> invoker;
    public InvokerInvocationHandler(Invoker<?> handler) {
        this.invoker = handler;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        …
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }
}

可以看到,这里把方法的执行流程转向了 invoker.invoke() 方法,而这个 invoker 对象会负责执行具体的远程调用操作。

Mybatis 数据访问中的代理机制

使用过 Mybatis 的同学都知道,我们只需要定义 Mapper 层的接口而不需要对其进行具体的实现,该接口就能够正常完成 SQL 执行等一系列操作,这是怎么做到的呢?

实际上 Mybatis 能够做到这一点的背后就是使用了强大的代理机制,具体来说就是如下所示的 MapperProxy 类。

代码语言:java
复制
public class MapperProxy<T> implements InvocationHandler, Serializable {
	@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
	}
}

可以看到,这里对于执行 SQL 语句的方法而言,MapperProxy 会把这部分工作交给 MapperMethod 处理。MapperMethod 会进一步调用 Mybatis 中的 SqlSession 对象并执行具体的 SQL 语句。

目前为止,我们看到了 MapperProxy 类实现了 InvocationHandler 接口,但还没有看到 Proxy.newProxyInstance() 方法的调用,该方法实际上位于 MapperProxyFactory 类中,该类还存在 newInstance() 重载方法,通过传入 mapperProxy 的代理对象最终完成代理方法的执行。

代码语言:java
复制
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

作为总结,我们梳理了 Mybatis 中 Mapper 层动态代理相关类的类层结构。

图 4 Mybatis 中代理类层结构图
图 4 Mybatis 中代理类层结构图

可以看到,Mybatis 在实现 SQL 执行时使用的就是 JDK 中所提供了原生动态代理机制。通过合理划分各个组件的职责,Mybatis 设计了用来生成代理对象 MapperProxy 的代理工厂类 MapperProxyFactory,并最终把执行 SQL 的操作封装在了 MapperMethod 中。

总结

今天我们系统介绍了代理机制。在日常开发过程中,代理可以说是一种通用性非常高的实现机制,它是面向切面编程的基础,也在主流的开源框架中得到了广泛地应用。例如,Dubbo 在实现远程方法调用时就用到了动态代理,而 Mybatis 则基于它来完成数据访问。这些应用方式和实现过程值得我们学习和模仿。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代理机制实现方式
    • 静态代理
    • 动态代理
  • 代理机制在开源框架中的应用
    • Dubbo 远程访问中的代理机制
    • Mybatis 数据访问中的代理机制
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档