【远程调用框架】如何实现一个简单的RPC框架(三)优化一:利用动态代理改变用户服务调用方式

【远程调用框架】如何实现一个简单的RPC框架(三)优化一:利用动态代理改变用户服务调用方式

原创 2017年03月29日 09:43:08spring

  • 514

【如何实现一个简单的RPC框架】系列文章:框架

【远程调用框架】如何实现一个简单的RPC框架(一)想法与设计 
【远程调用框架】如何实现一个简单的RPC框架(二)实现与使用 
【远程调用框架】如何实现一个简单的RPC框架(三)优化一:利用动态代理改变用户服务调用方式 
【远程调用框架】如何实现一个简单的RPC框架(四)优化二:改变底层通讯框架 
【远程调用框架】如何实现一个简单的RPC框架(五)优化三:软负载中心设计与实现 
第一个优化以及第二个优化修改后的工程代码可下载资源 如何实现一个简单的RPC框架ide

这篇博客,在(一)(二)的基础上,对初版本实现的服务框架进行改善,不按期更新,每次更新都会增长一个优化的地方。函数

 

 

一、优化一:利用动态代理改变用户服务调用方式

1.1 目的

改变用户使用LCRPC进行服务调用的方式,使得用户像访问本地接口同样访问远程服务。 
在第一个版本的服务框架开发完成后,若是用户但愿远程调用一个服务的某一个方法,为了获得正确结果,那么他必需要掌握的信息包括:方法的名称、方法的参数类型及个数、方法的返回值,以及服务发布者提供的二方包和LCRPC依赖。使用时,须要在spring配置文件中进行相似下面的配置:测试

<bean id="caculator" class="whu.edu.lcrpc.server.impl.LCRPCConsumerImpl">
    <property name="interfaceName" value="whu.edu.lcrpc.service.ICaculator" ></property>
    <property name="version" value="0.1"></property>
</bean>
  • 1
  • 2
  • 3
  • 4

假设咱们要调用的服务对应的接口为:ICaculator;当咱们想要调用这个接口的add方法时,须要调用LCRPCConsumerImpl提供的ServiceConsumer方法,该方法的签名为:优化

public Object serviceConsumer(String methodName, Object[] params)
  • 1

这意味着,用户在调用服务全部的方法时,都须要使用LCRPCConsumerImpl提供的ServiceConsumer方法,传入方法的名称,以及参数列表,而且在获得该函数的结果后显示将Object对象转换为该函数的返回类型。例如,但愿调用这个接口的add方法:this

List<Object> params = new ArrayList<>();
MyParamDO myParamDO = new MyParamDO();
myParamDO.setN1(1.0);
myParamDO.setN2(2.0);
params.add(myParamDO);
MyResultDO result = (MyResultDO) rpcConsumer.serviceConsumer("multiply",params);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样的使用方式,看起来有些麻烦。那么是否能够在使用LCRPC依赖进行远程服务调用时与访问本地接口没有区别,用户在调用方法时,直接使用服务发布者提供的二方包,直接调用二方包中接口的方法,例如上面的程序是否能够改为:spa

MyParamDO myParamDO = new MyParamDO();
myParamDO.setN1(1.0);
myParamDO.setN2(2.0);
MyResultDO result = caculator.add(myParamDO);
  • 1
  • 2
  • 3
  • 4

caculator为ICaculator类型对象,Spring配置文件中的配置不变。.net

其实,使用动态代理的方式彻底能够实现上述目的。设计

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服务框架进行改善了(第一个版本请参考博客【远程调用框架】如何实现一个简单的RPC框架(二)优化)。

  • step 1. 编写类MyinvocationHandler,实现接口InvocationHandler,且该实现类须要包括两个属性变量:interFaceName(所要代理的接口的全限定名)、version(服务版本号)。实现方法invoke,在该方法中获取方法的名称、参数列表,在此基础上拼装request请求对象,发送给服务端,接收响应,反序列化后返回。其实就是复用咱们第一个版本中的代码。该类代码以下:
@Data
public 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;
        Set<String> ips = 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值变了一下):
<bean id="caculator" class="whu.edu.lcrpc.server.impl.LCRPCConsumerImpl">
    <property name="interfaceName" value="whu.edu.lcrpc.service.ICaculator" ></property>
    <property name="version" value="0.1"></property>
</bean>
  • 1
  • 2
  • 3
  • 4

用户的使用方式以下:

@Resource
ICaculator caculator;
caculator.add(...)
  • 1
  • 2
  • 3

那么此时咱们如何将动态代理对象传给caculator,而且spring配置文件中bean的class值配置的是LCRPCCounsumerImpl,如何在spring生成bean的时候,生成的是响应接口的动态代理对象?然后将该动态代理对象传给caculator,使得用户能够直接调用caculator中的方法,而其实是调用的动态代理中的方法。 
Spring的FactoryBean接口,帮咱们实现了该要求。当某一个类实现了FactoryBean接口的时候,spring在建立该类型的bean时能够生成其余类型的对象返回。能够参考博客【Spring:FactoryBean接口】实现FactoryBean接口,Spring在初始化bean时有何不一样。利用这一点,咱们让LCRPCConsumerImpl实现FactoryBean接口,并在重写的getObject方法中生成相应接口的动态代理对象返回。修改后LCRPCConsumerImpl增长代码以下:

@Override
public Object getObject() throws Exception {
    //返回接口interfaceName的动态代理类
    return getProxy();
}

@Override
public Class<?> getObjectType() {
    try {
        return Class.forName(interfaceName).getClass();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;
}

@Override
public 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的配置文件以下:
<bean id="caculator" class="whu.edu.lcrpc.server.impl.LCRPCConsumerImpl">
    <property name="interfaceName" value="whu.edu.lcrpc.service.ICaculator" ></property>
    <property name="version" value="0.1"></property>
</bean>
  • 1
  • 2
  • 3
  • 4

class值为LCRPCConsumerImpl,可是spring返回的bean的类型为interfaceName属性对应接口的动态代理对象。

  • (2)ConsumerTest类修改成:
@Resource
ICaculator 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

此时能够发现,咱们在调用远程服务的时候,彻底就是利用服务发布者提供的二方包,调用其中的接口,跟本地调用彻底没有差异。 
执行主类没有改变,运行后的结果以下,与初版本相同。

这里写图片描述

相关文章
相关标签/搜索