learn-tech/专栏/Netty核心原理剖析与RPC实践-完/27动态代理:为用户屏蔽RPC调用的底层细节.md
2024-10-16 06:37:41 +08:00

20 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        27  动态代理:为用户屏蔽 RPC 调用的底层细节
                        动态代理在 RPC 框架的实现中起到了至关重要的作用,它可以帮助用户屏蔽 RPC 调用时底层网络通信、服务发现、负载均衡等具体细节,这些对用户来说并没有什么意义。你在平时项目开发中使用 RPC 框架的时候,只需要调用接口方法,然后就拿到了返回结果,你是否好奇 RPC 框架是如何完成整个调用流程的呢?今天这节课我们就一起来完成 RPC 框架的最后一部分内容RPC 请求调用和处理,看看如何使用动态代理机制完成这个神奇的操作。

源码参考地址mini-rpc

动态代理基础

为什么需要代理模式呢代理模式的优势是可以很好地遵循设计模式中的开放封闭原则对扩展开发对修改关闭。你不需要关注目标类的实现细节通过代理模式可以在不修改目标类的情况下增强目标类功能的行为。Spring AOP 是 Java 动态代理机制的经典运用,我们在项目开发中经常使用 AOP 技术完成一些切面服务,如耗时监控、事务管理、权限校验等,所有操作都是通过切面扩展实现的,不需要对源代码有所侵入。

动态代理是一种代理模式,它提供了一种能够在运行时动态构建代理类以及动态调用目标方法的机制。为什么称为动态是因为代理类和被代理对象的关系是在运行时决定的,代理类可以看作是对被代理对象的包装,对目标方法的调用是通过代理类来完成的。所以通过代理模式可以有效地将服务提供者和服务消费者进行解耦,隐藏了 RPC 调用的具体细节,如下图所示。

接下来我们一起探讨下动态代理的实现原理,以及常用的 JDK 动态代理、Cglib 动态代理是如何使用的。

JDK 动态代理

JDK 动态代理实现依赖 java.lang.reflect 包中的两个核心类InvocationHandler 接口和Proxy 类。

InvocationHandler 接口

JDK 动态代理所代理的对象必须实现一个或者多个接口,生成的代理类也是接口的实现类,然后通过 JDK 动态代理是通过反射调用的方式代理类中的方法,不能代理接口中不存在的方法。每一个动态代理对象必须提供 InvocationHandler 接口的实现类InvocationHandler 接口中只有一个 invoke() 方法。当我们使用代理对象调用某个方法的时候,最终都会被转发到 invoke() 方法执行具体的逻辑。invoke() 方法的定义如下:

public interface InvocationHandler {

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

}

其中 proxy 参数表示需要代理的对象method 参数表示代理对象被调用的方法args 参数为被调用方法所需的参数。

Proxy 类

Proxy 类可以理解为动态创建代理类的工厂类,它提供了一组静态方法和接口用于动态生成对象和代理类。通常我们只需要使用 newProxyInstance() 方法,方法定义如下所示。

public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) {

Objects.requireNonNull(h);

Class<?> caller = System.getSecurityManager() == null ? null : Reflection.getCallerClass();

Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);

return newProxyInstance(caller, cons, h);

}

其中 loader 参数表示需要装载的类加载器 ClassLoaderinterfaces 参数表示代理类实现的接口列表,然后你还需要提供一个 InvocationHandler 接口类型的处理器,所有动态代理类的方法调用都会交由该处理器进行处理,这是动态代理的核心所在。

下面我们用一个简单的例子模拟数据库操作的事务管理,从而学习 JDK 动态代理的具体使用方式。首先我们定义数据库表 User 的接口以及实现类:

public interface UserDao {

void insert();

}

public class UserDaoImpl implements UserDao {

@Override

public void insert() {

    System.out.println("insert user success.");

}

}

接下来我们实现一个事务管理的工具类,在数据库操作执行前后执行事务操作,代码如下所示:

public class TransactionProxy {

private Object target;

public TransactionProxy(Object target) {

    this.target = target;

}

public Object genProxyInstance() {

    return Proxy.newProxyInstance(target.getClass().getClassLoader(),

            target.getClass().getInterfaces(),

            (proxy, method, args) -> {

                System.out.println("start transaction");

                Object result = method.invoke(target, args);

                System.out.println("submit transaction");

                return result;

            });

}

}

在 genProxyInstance() 方法中我们最主要的是实现 InvocationHandler 接口,在真实对象方法执行方法调用的前后可以扩展自定义行为,以此来增强目标类的功能。为了便于理解,上述例子中我们只简单打印了控制台日志,可以通过测试类看看 JDK 动态代理的实际效果:

public class TransactionProxyTest {

@Test

public void testProxy() {

    UserDao userDao = new UserDaoImpl();

    UserDao proxyInstance = (UserDao) new TransactionProxy(userDao).genProxyInstance();

    proxyInstance.insert();

}

}

程序运行结果如下:

start transaction

insert user success.

submit transaction

Cglib 动态代理

Cglib 动态代理是基于 ASM 字节码生成框架实现的第三方工具类库,相比于 JDK 动态代理Cglib 动态代理更加灵活,它是通过字节码技术生成的代理类,所以代理类的类型是不受限制的。使用 Cglib 代理的目标类无须实现任何接口,可以做到对目标类零侵入。

Cglib 动态代理是对指定类以字节码的方式生成一个子类,并重写其中的方法,以此来实现动态代理。因为 Cglib 动态代理创建的是目标类的子类,所以目标类必须要有无参构造函数,而且目标类不要用 final 进行修饰。

在我们使用 Cglib 动态代理之前,需要引入相关的 Maven 依赖,如下所示。如果你的项目中已经引入了 spring-core 的依赖,则已经包含了 Cglib 的相关依赖,无须再次引入。

<groupId>cglib</groupId>

<artifactId>cglib</artifactId>

<version>3.3.0</version>

下面我们还是使用上述数据库事务管理的例子,从而学习 JDK 动态代理的具体使用方式。UserDao 接口和实现类保持不变TransactionProxy 需要重新实现,代码如下所示:

public class CglibTransactionProxy implements MethodInterceptor {

private Object target;

public CglibTransactionProxy(Object target) {

    this.target = target;

}

public Object genProxyInstance() {

    Enhancer enhancer = new Enhancer();

    enhancer.setSuperclass(target.getClass());

    enhancer.setCallback(this);

    return enhancer.create();

}

@Override

public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

    System.out.println("start transaction");

    Object result = methodProxy.invokeSuper(object, args);

    System.out.println("submit transaction");

    return result;

}

}

Cglib 动态代理的实现需要依赖两个核心组件MethodInterceptor 接口和 Enhancer 类,类似于 JDK 动态代理中的InvocationHandler 接口和Proxy 类。

MethodInterceptor 接口

MethodInterceptor 接口只有 intercept() 一个方法,所有被代理类的方法执行最终都会转移到 intercept() 方法中进行行为增强,真实方法的执行逻辑则通过 Method 或者 MethodProxy 对象进行调用。

Enhancer 类

Enhancer 类是 Cglib 中的一个字节码增强器它为我们对代理类进行扩展时提供了极大的便利。Enhancer 类的本质是在运行时动态为代理类生成一个子类,并且拦截代理类中的所有方法。我们可以通过 Enhancer 设置 Callback 接口对代理类方法执行的前后执行一些自定义行为,其中 MethodInterceptor 接口是我们最常用的 Callback 操作。

Cglib 动态代理的测试类与 JDK 动态代理测试类大同小异,程序输出结果也是一样的。测试类代码如下所示:

public class CglibTransactionProxyTest {

public static void main(String[] args) {

    UserDao userDao = new UserDaoImpl();

    UserDao proxyInstance = (UserDao) new CglibTransactionProxy(userDao).genProxyInstance();

    proxyInstance.insert();

}

}

学习完动态代理的基础后,我们接下来实现 RPC 框架中的请求调用和处理就易如反掌啦,首先我们先从服务消费者如何通过动态代理发起 RPC 请求入手。

服务消费者动态代理实现

在《服务发布与订阅:搭建生产者和消费者的基础框架》课程中,我们讲解了 @RpcReference 注解的实现过程。通过一个自定义的 RpcReferenceBean 完成了所有执行方法的拦截RpcReferenceBean 中 init() 方法是当时留下的 TODO 内容,这里就是代理对象的创建入口,代理对象创建如下所示.

public class RpcReferenceBean implements FactoryBean