一文彻底搞懂动态代理机制

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

 

kotlin

代码解读

复制代码

@Service public class HealthService {     public void recordUserHealthData(HealthData data) {      healthRepository.recordUserHealthData(data);      logger.info("Record user health data successfully.");     }     … }

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

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

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

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

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

代理机制实现方式

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

静态代理

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

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

 

csharp

代码解读

复制代码

public interface Account{     void open(); }

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

 

typescript

代码解读

复制代码

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面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记的【点击此处即可】即可免费获取

typescript

代码解读

复制代码

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 静态代理类层结构图

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

动态代理

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

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

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

 

typescript

代码解读

复制代码

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 类,如下所示。

 

ini

代码解读

复制代码

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 动态代理的类层结构图

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

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

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

Dubbo 远程访问中的代理机制

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

 

scss

代码解读

复制代码

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 接口。

 

arduino

代码解读

复制代码

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 类。

 

kotlin

代码解读

复制代码

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 的代理对象最终完成代理方法的执行。

 

typescript

代码解读

复制代码

protected T newInstance(MapperProxy<T> mapperProxy) {     return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }

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

图 4 Mybatis 中代理类层结构图

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

总结

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

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它将数据和操作封装在对象中,通过对象之间的交互实现程序的设计和开发。下面是一些关键概念,帮助你更好地理解Python面向对象编程。 1. 类(Class):类是对象的蓝图或模板,描述了对象的属性和行为。它定义了对象的特征和方法。例如,我们可以定义一个名为"Car"的类来表示汽车,其中包含属性(如颜色、型号)和方法(如加速、刹车)。 2. 对象(Object):对象是类的实例,是具体的实体。通过实例化类,我们可以创建一个对象。例如,我们可以创建一个名为"my_car"的对象,它是基于"Car"类的实例。 3. 属性(Attribute):属性是对象的特征,用于描述对象的状态。每个对象都可以具有一组属性。例如,"Car"类的属性可以包括颜色、型号等。 4. 方法(Method):方法是对象的行为,用于定义对象的操作。每个对象都可以具有一组方法。例如,"Car"类的方法可以包括加速、刹车等。 5. 继承(Inheritance):继承是一种机制,允许我们创建一个新类(称为子类),从现有类(称为父类)继承属性和方法。子类可以扩展或修改父类的功能。继承可以实现代码重用和层次化设计。 6. 多态(Polymorphism):多态是一种特性,允许不同类的对象对同一方法做出不同的响应。多态提高了代码的灵活性和可扩展性。 7. 封装(Encapsulation):封装是一种将数据和操作封装在对象中的机制,隐藏了对象的内部实现细节,只暴露必要的接口给外部使用。这样可以保护数据的安全性,提供了更好的模块化和代码复用性。 通过理解这些概念,你可以更好地掌握Python面向对象编程。在实践中,你可以使用类来创建对象,操作对象的属性和调用对象的方法,通过继承和多态实现代码的灵活性和可扩展性,通过封装保护数据的安全性和提高代码的可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值