【远程调用框架】如何实现一个简单的RPC框架(三)优化一:利用动态代理改变用户服务调用方式
原创 2017年03月29日 09:43:08
- 514
【如何实现一个简单的RPC框架】系列文章:
第一个优化以及第二个优化修改后的工程代码可下载资源
这篇博客,在(一)(二)的基础上,对第一版本实现的服务框架进行改善,不定期更新,每次更新都会增加一个优化的地方。
1、优化一:利用动态代理改变用户服务调用方式
1.1 目的
改变用户使用LCRPC进行服务调用的方式,使得用户像访问本地接口一样访问远程服务。
在第一个版本的服务框架开发完成后,如果用户希望远程调用一个服务的某一个方法,为了得到正确结果,那么他必须要掌握的信息包括:方法的名称、方法的参数类型及个数、方法的返回值,以及服务发布者提供的二方包和LCRPC依赖。使用时,需要在spring配置文件中进行类似下面的配置:- 1
- 2
- 3
- 4
假设我们要调用的服务对应的接口为:ICaculator;当我们想要调用这个接口的add方法时,需要调用LCRPCConsumerImpl提供的ServiceConsumer方法,该方法的签名为:
public Object serviceConsumer(String methodName, Object[] params)
- 1
这意味着,用户在调用服务所有的方法时,都需要使用LCRPCConsumerImpl提供的ServiceConsumer方法,传入方法的名称,以及参数列表,并且在得到该函数的结果后显示将Object对象转换为该函数的返回类型。例如,希望调用这个接口的add方法:
List
- 1
- 2
- 3
- 4
- 5
- 6
这样的使用方式,看起来有些麻烦。那么是否可以在使用LCRPC依赖进行远程服务调用时与访问本地接口没有区别,用户在调用方法时,直接使用服务发布者提供的二方包,直接调用二方包中接口的方法,例如上面的程序是否可以改成:
MyParamDO myParamDO = new MyParamDO();myParamDO.setN1(1.0);myParamDO.setN2(2.0);MyResultDO result = caculator.add(myParamDO);
- 1
- 2
- 3
- 4
caculator为ICaculator类型对象,Spring配置文件中的配置不变。
其实,使用动态代理的方式完全可以实现上述目的。
1.2 方法
方法:动态代理
关于动态代理的知识读者可以自行查阅网上诸多资料,也可以阅读《疯狂Java讲义》第18章对动态代理的介绍。 使用JDK为我们提供的Proxy和InvocationHandler创建动态代理。主要步骤包括: step 1. 实现接口InvocationHandler,实现方法invoke,执行代理对象所有方法执行时将会替换成执行此invoke方法,因此我们可以将真正的操作在该函数中实现(例如本服务框架中:拼装请求参数序列化后发送给服务端,得到结果后解析,即远程服务调用的过程)。 step2. 利用Proxy的new ProxyInstance生成动态代理对象。例如:InvocationHandler handler = new MyInvocationhandler(...);Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.calss},hanlder);
- 1
- 2
此时,调用f的所有方法执行的均是handler中的invoke方法。
了解了实现动态代理的思路后,我们可以对我们自己编写的RPC服务框架进行改善了(第一个版本请参考博客)。
- step 1. 编写类MyinvocationHandler,实现接口InvocationHandler,且该实现类需要包括两个属性变量:interFaceName(所要代理的接口的全限定名)、version(服务版本号)。实现方法invoke,在该方法中获取方法的名称、参数列表,在此基础上拼装request请求对象,发送给服务端,接收响应,反序列化后返回。其实就是复用我们第一个版本中的代码。该类代码如下:
@Datapublic class MyInvocationHandler implements InvocationHandler { private String interfaceName;//接口的全限定名 private String version;//服务版本号 private IConsumerService consumerService;//初始化客户端辅助类 public MyInvocationHandler(String interfaceName, String version){ this.interfaceName = interfaceName; this.version = version; consumerService = new ConsumerServiceImpl(); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //方法的名称 String methodName = method.getName(); //方法的返回类型 Class returnType = method.getReturnType(); //若服务唯一标识没有提供,则抛出异常 if (interfaceName == null || interfaceName.length() == 0 || version == null || version.length() == 0) throw new LCRPCServiceIDIsIllegal(); //step1. 根据服务的唯一标识获取该服务的ip地址列表 String serviceID = interfaceName + "_" + version; Setips = consumerService.getServiceIPsByID(serviceID); if (ips == null || ips.size() == 0) throw new LCRPCServiceNotFound(); //step2. 路由,获取该服务的地址,路由的结果会返回至少一个地址,所以这里不需要抛出异常 String serviceAddress = consumerService.getIP(serviceID,methodName,args,ips); //step3. 根据传入的参数,拼装Request对象,这里一定会返回一个合法的request对象,所以不需要抛出异常 LCRPCRequestDO requestDO = consumerService.getRequestDO(interfaceName,version,methodName,args); //step3. 传入Request对象,序列化并传入服务端,拿到响应后,反序列化为object对象 Object result = null; try { result = consumerService.sendData(serviceAddress,requestDO); }catch (Exception e){ //在服务调用的过程种出现问题 throw new LCRPCRemoteCallException(e.getMessage()); } if (result == null)throw new LCRPCRemoteCallException(Constant.SERVICEUNKNOWNEXCEPTION); //step4. 返回object对象 return result; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- step 2. 我们希望在使用时spring配置文件中的配置不变,依旧是(把bean的id值变了一下):
- 1
- 2
- 3
- 4
用户的使用方式如下:
@ResourceICaculator caculator;caculator.add(...)
- 1
- 2
- 3
那么此时我们如何将动态代理对象传给caculator,并且spring配置文件中bean的class值配置的是LCRPCCounsumerImpl,如何在spring生成bean的时候,生成的是响应接口的动态代理对象?而后将该动态代理对象传给caculator,使得用户可以直接调用caculator中的方法,而实际上是调用的动态代理中的方法。
Spring的FactoryBean接口,帮我们实现了该要求。当某一个类实现了FactoryBean接口的时候,spring在创建该类型的bean时可以生成其他类型的对象返回。可以参考博客。利用这一点,我们让LCRPCConsumerImpl实现FactoryBean接口,并在重写的getObject方法中生成相应接口的动态代理对象返回。修改后LCRPCConsumerImpl增加代码如下:@Overridepublic Object getObject() throws Exception { //返回接口interfaceName的动态代理类 return getProxy();}@Overridepublic Class getObjectType() { try { return Class.forName(interfaceName).getClass(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null;}@Overridepublic boolean isSingleton() { return false;}private Object getProxy() throws ClassNotFoundException { Class clz = Class.forName(interfaceName); return Proxy.newProxyInstance(clz.getClassLoader(),new Class[]{clz},new MyInvocationHandler(interfaceName,version));}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
getProxy方法利用Proxy和我们自己实现的MyInvocationHandler类返回了某个接口的动态代理对象。
至此,我们在LCRPC服务框架部分的改造已经完成了。用户现在可以在客户端像调用本地接口一样,访问某一个远程服务了。关于具体的使用请参考1.3节内容。1.3 使用
还是在上一版本客户端测试代码的基础上,我们还是要调用服务发布者发布的计算器服务。
- (1)如果用户希望调用接口ICaculator对应的服务,则spring的配置文件如下:
- 1
- 2
- 3
- 4
class值为LCRPCConsumerImpl,但是spring返回的bean的类型为interfaceName属性对应接口的动态代理对象。
- (2)ConsumerTest类修改为:
@ResourceICaculator caculator;public void add(){ System.out.println("add:" + caculator.add(1,2));}public void minus(){ System.out.println("minus:" + caculator.minus(1,2));}public void multiply(){ MyParamDO p1 = new MyParamDO(); p1.setN1(1); p1.setN2(2); System.out.println("multiply:" + caculator.multiply(p1));}public void divide(){ MyParamDO p1 = new MyParamDO(); p1.setN1(1); p1.setN2(2); System.out.println("divide:" + caculator.divide(p1));}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
此时可以发现,我们在调用远程服务的时候,完全就是利用服务发布者提供的二方包,调用其中的接口,跟本地调用完全没有差别。
执行主类没有改变,运行后的结果如下,与第一版本相同。