作者 | 张凯
审校 | 重楼
动态代理技术与RPC(Remote Procedure Call)架构是现代分布式系统中常用的两项关键技术,二者结合可以极大地提高系统的灵活性和可扩展性。将动态代理技术与RPC架构结合使用,可以实现自动化的服务调用和增强功能。开发者可以专注于业务逻辑的实现,而不必担心底层细节。这种结合不仅提高了代码的复用性和可维护性,也增强了系统的监控和管理能力。本文将详细解析这两项技术的实现方式以及其背后的设计逻辑。
一、动态代理
1. 代理模式
代理模式为其他对象提供了一种代理以控制对这个对象的访问,根据代理类的创建时机和创建方式的不同,可以将其分为静态代理和动态代理两种形式:在程序运行前就已经存在的编译好的代理类称为静态代理,在程序运行期间根据需要动态创建代理类及其实例来完成具体的功能称为动态代理。
代理模式的目的是为真实业务对象提供一个代理对象以控制对真实业务对象的访问,代理对象的作用有:
- 代理对象存在的价值主要用于拦截对真实业务对象的访问;
- 代理对象可以和目标对象(真实业务对象)实现共同的接口或继承于同一个类;
- 代理对象是对目标对象的增强,以便对消息进行预处理和后处理。
2. 反射
动态代理的实现核心是反射,一切动态代理的代理操作都是反射实现的。所以要先对反射知识有一定的了解。
2.1 反射实现步骤
2.1.1通过反射获取对象的.class文件,也就是class对象(有三种方式)。
//方式一:使用Class.forName方法进行包名+类名的定位
Class c1=Class.forName("zk.reflect.Person");
//方式二:采用类名.class方法获取反射
Class c2=Person.class;
//方式三:采用对象名.class方法获取反射(运行过程中)
Person ps=new Person();
Class c3 = ps.getClass();
2.1.2通过反射获取的对象,获取对象类中的方法,使用invoke([实例化对象],[方法对应的参数])方法,进行方法使用。
//获取类的方法
Method[] m =c.getMethods(); //获得本类和其父类的全部public方法
Method[] m2=c.getDeclaredMethods(); //仅获取本类的全部方法(包括私有方法)
//获取指定方法,并使用
//通过实例对象获取反射
//Person p =new Person();
//获取Person类对象
//Class c=p.getClass();
Class c=Person.class;
Person p= (Person) c.newInstance();
//获取该类实例对象中的具体方法--第一个参数要写Person类的方法名称才能匹配上;后面参数是方法参数类型
Method m=c.getDeclaredMethod("eat",String.class);
//使用invoke方法对反射获取的方法进行激活---第一个参数是实例化对象,后面参数是方法对应参数值
String s= (String) m.invoke(p,"zhangkai");
System.out.println(s);
//获取指定方法,必须加入参数,没有加null;因为存在重载,只有方法名和参数个数两个才能精确定位方法
Method getid=c.getMethod("getid",null);
Method setid=c.setMethod("setid",,int.class);
3. 动态代理原理
对代理模式而言,具体主题类与其代理类一般是一一对应的,这也是静态代理的特点。但是,也存在这样的情况:有N个主题类,但是代理类中的“预处理、后处理”都是相同的,仅仅是调用主题不同。那么,若采用静态代理,那么必然需要手动创建N个代理类,这显然让人相当不爽。动态代理则可以简单地为各个主题类分别生成代理类,共享“预处理,后处理”功能,这样可以大大减小程序规模,这也是动态代理的一大亮点。(通俗上来说,动态代理不再是一个代理类代理一个委托类,而是像个大管家,指定那个委托对象,就代理谁的方法,只不过代理类中通用的逻辑会适用于每个委托类)
在动态代理中,代理类是在运行时期生成的。因此,相比静态代理,动态代理可以很方便地对委托类的相关方法进行统一增强处理,如添加方法调用次数、添加日志功能等等。动态代理主要分为JDK动态代理和CGLIB动态代理两大类。
下面以一个模拟动态代理案例来实现动态代理的实现思路:
设计两个Service类:User和Order,使用ProxyUtils类动态代理这两类的抽象接口。程序在调用过程中,通过接口直接到代理类中,由代理类实现接口实现类的功能以及代理类自身的一些增强功能和通用功能。
3.1 接口类
//User的一些方法
public interface UserService {
String login(String s,String p) throws InterruptedException;
void selectById(int id);
void delect();
}
//Order的一些方法
public interface OrderService {
String zhuyi();
void select();
String delectById(int id);
}
3.2 实现类
//UserServiceImpl实现类
public class UserServiceImpl implements UserService {
@Override
public String login(String name,String password) {
String resword="用户名或密码错误!";
if(name.equals("admin") && password.equals("123456")){
resword="登录成功";
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return resword;
}
@Override
public void selectById(int id) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(id+"的信息如下:----");
}
@Override
public void delect() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("信息正在删除。。。。。");
}
}
//OrderServiceImpl实现类
public class OrderServiceImpl implements OrderService{
@Override
public String zhuyi() {
return "订单功能区:---------";
}
@Override
public void select() {
System.out.println("全部订单如下所示--------");
}
@Override
public String delectById(int id) {
String s=id+"的信息正在删除。。。。。";
return s;
}
}
3.3动态代理类ProxyUtils
- 该类中的proxyService方法使用泛型可以让proxyService方法代理多种不同类的Service接口;
- 返回一个Proxy通过newProxyInstance()提供的实例对象;
- newProxyInstance()需要三部分的参数。第一部分是创建方法形参对象obj的类加载器对象;第二部分是创建方法形参对象obj的类接口对象(都为固定方法);第三部分是创建一个InvocationHandler()方法,意味调用类对象方法的方法;
- 代理类中的增强功能就可以写在InvocationHandler()方法中,本例是实现了一个方法运行时间的检测功能。
public class ProxyTest {
public static void main(String[] args) throws Exception {
//创建一个User类的代理对象,代理对象中也传入了一个新建的User实现类对象
UserService userService=ProxyUtils.proxyService(new UserServiceImpl());
String rs = userService.login("admin", "123456");
System.out.println(rs);
userService.selectById(2);
userService.delect();
//创建一个Order类的代理对象
OrderService orderService=ProxyUtils.proxyService(new OrderServiceImpl());
String ss = orderService.delectById(9);
System.out.println(ss);
}
}
以上就是动态代理案例的全部实现,核心的调用逻辑就是流程图中的调用逻辑,使用动态代理类不仅可以大幅提高代码的复用程度,而且还可以在被代理类的基础上实现一些公共的增强功能,这其实就是Spring中的AOP的核心实现原理。例如本例就实现了所有被代理Service的方法运行时间检测的功能,对于多个Service都实现检测功能,其实就是一种横向编程的思路。
二、RPC架构
1.RPC的出现原因
不同的业务拆分到多个应用中,让不同的应用分别承担不同的功能是解决这些问题的必杀技。将不同业务分拆到不同的应用后,不但可以大幅度提升系统的稳定性还有助于丰富技术选型,进一步保证系统的性能。总的来说,从单体应用到分布式多体应用是系统升级必经之路。
当一个单体应用演化成多体应用后,远程调用就粉墨登场了。在一个应用时,相互通信直接通过本地调用就可完成,而变为多体应用时,相互通信就得依赖远程调用了,这时一个高效稳定的RPC框架就显得非常必要了。简单的HTTP调用虽然也可以实现远程通信,但效果不够理想,原因有二:
- RPC远程调用像本地调用一样干净简洁,但其他方式对代码的侵入性就比较强;
- 一般使用RPC框架实现远程通信效率比其他方式效率要高一些。
2. RPC框架介绍
对于多体应用,由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写较多网络通信相关的代码,不仅复杂而且极易出错。若能实现一种机制,使得开发者能够像调用本地服务一般便捷地调用远程服务,同时对网络通信等底层细节保持透明,那么这将极大地释放程序员的开发负担,显著提升开发效率与生产力。比如,服务消费方在执行helloService.hi(“Panda”)时,实质上调用的是远端的服务。这种方式其实就是RPC(Remote Procedure Call),在各大互联网公司中被广泛使用,如阿里巴巴的HSF、Dubbo(开源)、Facebook的Thrift(开源)、Google GRPC(开源)、Twitter的Finagle(开源)等。
RPC的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。要让网络通信细节对使用者透明,我们需要对通信细节进行封装,下面是一个RPC的经典调用的流程,并且反映了所涉及到的一些通信细节:
(1)服务消费方(client)以本地调用方式调用服务;
(2).client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
(3)client stub找到服务地址,并将消息发送到服务端;
(4)server stub收到消息后进行解码;
(5)server stub根据解码结果反射调用本地的服务;
(6)本地服务执行并将结果返回给server stub;
(7)server stub将返回结果打包成消息并发送至消费方;
(8)client stub接收到消息,并进行解码;
(9)服务消费方得到最终结果。
RPC框架就是要将2~8这些步骤封装起来,让用户对这些细节透明,使得远程方法调用看起来像调用本地方法一样。
下面给出一个实现简易RPC架构的代码。
从该RPC框架的简易实现来看,RPC客户端逻辑是:1.首先创建Socket客户端并与服务端建立链接;2.然后使用Java原生的序列化/反序列化机制将调用请求发送给客户端,包括所调用方法的名称、参数列表将服务端的响应返回给用户即可。至此,一次简单PRC调用的客户端流程执行完毕。特别地,从代码实现来看,实现透明的PRC调用的关键就是动态代理,这是RPC框架实现的灵魂所在。
2.1.1服务端
服务端提供客户端所期待的服务,一般包括三个部分:服务接口,服务实现以及服务的注册暴露三部分。
//服务端接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}
//服务实现类
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) {
return "Hello " + name;
}
@Override
public String hi(String msg) {
return "Hi, " + msg;
}
}
//(重要)服务暴露
public class RpcProvider {
public static void main(String[] args) throws Exception {
HelloService service = new HelloServiceImpl();
// 自实现的RpcFramework,RPC框架使用export()方法将服务暴露出来,供客户端消费
RpcFramework.export(service, 1234);
}
}
2.1.2客户端
客户端消费服务端所提供的服务,一般包括两个部分:服务引用和服务接口两个部分。
//(服务引用)消费端通过RPC框架进行远程调用,这也是RPC框架功能之一
public class RpcConsumer {
public static void main(String[] args) throws Exception {
// 由RpcFramework类中refer()方法生成的HelloService接口的代理
HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
//使用代理对象进行实现类中hello()方法的调用
String hello = service.hello("World");
System.out.println("客户端收到远程调用的结果 : " + hello);
}
}
//与服务端共享同一个服务接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}
2.1.3 RPC框架类RpcFramework
public class RpcFramework {
/**
* 暴露服务
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
//如果服务和端口存在问题,抛出异常
if (service == null) {
throw new IllegalArgumentException("service instance == null");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port);
// 建立Socket服务端
ServerSocket server = new ServerSocket(port);
for (; ; ) {
try {
// 监听Socket请求
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
/* 获取请求流,Server解析并获取请求*/
// 构建对象输入流,从源中读取对象到程序中
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream());
try {
System.out.println("\nServer解析请求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName);
// 泛型与数组是不兼容的,除了通配符作泛型参数以外
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
System.out.println(
"parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments));
/* Server 处理请求,进行响应*/
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream());
try {
// service类型为Object的(可以发布任何服务),故只能通过反射调用处理请求
// 反射调用,处理请求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
//invoke方法调用对应方法,得到处理返回值保存在result中
Object result = method.invoke(service, arguments);
System.out.println("\nServer 处理并生成响应 :");
System.out.println("result : " + result);
//将请求处理结果写回socket信息
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务,返回代理对象
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception {
if (interfaceClass == null) {
throw new IllegalArgumentException("Interface class == null");
}
// JDK 动态代理的约束,只能实现对接口的代理
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
"The " + interfaceClass.getName() + " must be interface class!");
}
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("Host == null!");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println(
"Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port);
// JDK 动态代理,使用泛型实现广泛代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class<?>[] {interfaceClass}, new InvocationHandler() {
// invoke方法本意是对目标方法的增强,在这里用于发送RPC请求和接收响应
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
// 创建Socket客户端,并与服务端建立链接
Socket socket = new Socket(host, port);
try {
/* 客户端向服务端进行请求,并将请求参数写入流中*/
// 将对象写入到对象输出流,并将其发送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream());
try {
// 发送请求
System.out.println("\nClient发送请求 : ");
output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments));
/* 客户端读取并返回服务端的响应*/
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream());
try {
Object result = input.readObject();
//如果result是一个异常说明服务端返回没成功,客户端只能抛出异常
if (result instanceof Throwable) {
throw (Throwable)result;
}
System.out.println("\nClient收到响应 : ");
System.out.println("result : " + result);
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
});
//给消费端返回代理对象,供使用
return proxy;
}
2.2关于RPC框架的解析与说明
(1)RPC框架如何做到透明化远程服务调用
在封装通信细节以实现用户能够像本地调用服务一样便捷地访问远程服务的方案中,Java的动态代理技术无疑是一个有效的解决方案。具体而言,Java支持两种动态代理的实现方式:JDK动态代理和CGLIB动态代理。尽管基于字节码生成的CGLIB代理方法在功能上更为强大和高效,但由于其代码维护的复杂性,许多RPC框架的实现仍然优先选择使用JDK动态代理。
在此背景下,RPCFramework的invoke方法有效地封装了与远端服务之间的通信细节。当消费者通过RPCFramework获取服务提供者的接口后,在调用helloService.hi("Panda")方法时,将直接触发该invoke方法,进而执行与远程服务的交互。此种设计极大地简化了远程服务调用的复杂性,为开发者提供了便捷的使用体验。
(2)如何发布自己的服务
如何让用户高效地调用我们的服务?是否仅仅通过在代码中硬编码服务的IP地址和端口即可实现?实际上,在生产环境中,这种做法并不可行,因为服务的部署节点可能会频繁地上线和下线。举例而言,若发现现有的服务实例不够应付需求,需要增加新的服务节点时,调用者必须手动更新其配置以包含新的IP地址,甚至可能需要在负载均衡的上下文中实现轮询调用。与此同时,当某一台机器出现故障,导致服务不可用时,调用者又必须手动删除失效机器的IP。此过程显然是相当繁琐和低效的。
为了解决这一问题,应当采用一种能够实现自动通知的机制,使得服务节点的变更对调用方保持透明,从而免去硬编码服务提供者地址的需要。在实际生产中,许多RPC框架已经实现了此类自动告知功能。例如,阿里巴巴内部使用的RPC框架HSF通过ConfigServer来管理服务的注册与发现。此外,Zookeeper也广泛应用于实现服务的自动注册与发现。无论采用何种具体技术,这些解决方案通常基于发布/订阅模式,以有效管理服务的可用性与负载均衡。
(3)序列化与反序列化
在Java中,对象无法直接在网络中进行传输,因此在进行RPC请求时,客户端如何将请求发送给服务端,并如何接收服务端的响应,成为一个需要解决的问题。解决方案在于对Java对象进行序列化,以便将其转换为可传输的字节流,并在接收端通过反序列化过程将字节流还原为原始对象,从而便于后续的处理。
实际上,序列化和反序列化的技术存在多种实现方式,包括Java的原生序列化、JSON格式、阿里巴巴的Hessian序列化以及ProtoBuf序列化等。这些不同的序列化方式在传输效率上各有差异,同时也具备各自的特点和优势,适用于不同场景的需求。因此,选择适当的序列化方案对于提升RPC框架的性能和可用性至关重要。
三、总结
动态代理允许在运行时创建对象的代理,从而对方法调用进行拦截和处理。在Java等编程语言中,利用反射机制,可以动态创建代理类。这种方式使得开发人员可以在不修改已有代码的情况下,增强或改变方法的行为。本文通过构建实例的方式详细解释了其工作原理。RPC(远程过程调用)是一种允许程序在不同的地址空间(通常是不同机器上的进程)中调用子程序或服务的协议。通过RPC,开发者可以像调用本地方法一样,方便地调用远程服务,隐藏了网络通信的复杂性。
通过动态代理技术与RPC架构的结合,可以实现以下优势:
- 透明的远程调用:使用动态代理,开发者在调用远程服务时无需关心具体的网络请求和响应处理,可以像调用本地方法一样使用。
- 增强的功能:通过动态代理,能够在服务调用时加入额外的处理逻辑,如日志记录、方法监控、错误处理等。
- 解耦合:RPC客户端和服务端的实现可通过接口进行解耦,更新和维护服务时不会影响到调用方的代码。
- 优化和扩展:因为使用了动态代理,增加新的功能或改变业务逻辑时,只需更改代理的实现即可,而不需要修改每一个具体的服务调用。
动态代理技术和RPC架构的结合为分布式系统的开发提供了灵活和高效的解决方案。它们使得系统能够在复杂的基础设施中轻松处理远程服务的调用,同时还增强了代码的可重用性和可维护性。这种技术的广泛应用,无疑对现代软件架构的演变产生了深远的影响。
作者介绍
张凯,中国农业银行股份有限公司研发中心软件研发工程师,擅长SpringBoot+Vue全栈式开发,数据挖掘与建模,热爱编程和学习前沿技术信息了解内部的实现逻辑。