徒手撸一个简单的RPC框架(附源码)

作者:不学无数的程序员 juejin.im/post/5c4481a4f265da613438aec3 文章编辑:Java知音

之前在RPC 框架底层到底什么原理得知了RPC(远程过程调用)简单来说就是调用远程的服务就像调用本地方法一样,其中用到的知识有序列化和反序列化、动态代理、网络传输、动态加载、反射这些知识点。

发现这些知识都了解一些。所以就想着试试自己实现一个简单的RPC框架,即巩固了基础的知识,也能更加深入的了解RPC原理。当然一个完整的RPC框架包含了许多的功能,例如服务的发现与治理,网关等等。本篇只是简单的实现了一个调用的过程。

blog.csdn.net/u013592964/article/details/80441205

传参出参分析

一个简单请求可以抽象为两步

那么就根据这两步进行分析,在请求之前我们应该发送给服务端什么信息?而服务端处理完以后应该返回客户端什么信息?

在请求之前我们应该发送给服务端什么信息?

由于我们在客户端调用的是服务端提供的接口,所以我们需要将客户端调用的信息传输过去,那么我们可以将要传输的信息分为两类

  • 第一类是服务端可以根据这个信息找到相应的接口实现类和方法
  • 第二类是调用此方法传输的参数信息

那么我们就根据要传输的两类信息进行分析,什么信息能够找到相应的实现类的相应的方法?要找到方法必须要先找到类,这里我们可以简单的用Spring提供的Bean实例管理ApplicationContext进行类的寻找。

所以要找到类的实例只需要知道此类的名字就行,找到了类的实例,那么如何找到方法呢?在反射中通过反射能够根据方法名和参数类型从而找到这个方法。那么此时第一类的信息我们就明了了,那么就建立相应的是实体类存储这些信息。

@Data
public class Request implements Serializable {
    private static final long serialVersionUID = 3933918042687238629L;
    private String className;
    private String methodName;
    private Class<?> [] parameTypes;
    private Object [] parameters;
}

服务端处理完以后应该返回客户端什么信息?

上面我们分析了客户端应该传输什么信息给服务端,那么服务端处理完以后应该传什么样的返回值呢?这里我们只考虑最简单的情况,客户端请求的线程也会一直在等着,不会有异步处理这一说,所以这么分析的话就简单了,直接将得到的处理结果返回就行了。

@Data
public class Response implements Serializable {
    private static final long serialVersionUID = -2393333111247658778L;
    private Object result;
}

由于都涉及到了网络传输,所以都要实现序列化的接口

如何获得传参信息并执行?-客户端

上面我们分析了客户端向服务端发送的信息都有哪些?那么我们如何获得这些信息呢?首先我们调用的是接口,所以我们需要写自定义注解然后在程序启动的时候将这些信息加载在Spring容器中。有了这些信息那么我们就需要传输了,调用接口但是实际上执行的确实网络传输的过程,所以我们需要动态代理。那么就可以分为以下两步

  • 初始化信息阶段:将key为接口名,value为动态接口类注册进Spring容器中
  • 执行阶段:通过动态代理,实际执行网络传输

初始化信息阶段

由于我们使用Spring作为Bean的管理,所以要将接口和对应的代理类注册进Spring容器中。而我们如何找到我们想要调用的接口类呢?我们可以自定义注解进行扫描。将想要调用的接口全部注册进容器中。

创建一个注解类,用于标注哪些接口是可以进行Rpc的

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcClient {
}

然后创建对于@RpcClient注解的扫描类RpcInitConfig,将其注册进Spring容器中

public class RpcInitConfig implements ImportBeanDefinitionRegistrar{

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider provider = getScanner();
        //设置扫描器
        provider.addIncludeFilter(new AnnotationTypeFilter(RpcClient.class));
        //扫描此包下的所有带有@RpcClient的注解的类
        Set<BeanDefinition> beanDefinitionSet = provider.findCandidateComponents("com.example.rpcclient.client");
        for (BeanDefinition beanDefinition : beanDefinitionSet){
            if (beanDefinition instanceof AnnotatedBeanDefinition){
                //获得注解上的参数信息
                AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
                String beanClassAllName = beanDefinition.getBeanClassName();
                Map<String, Object> paraMap = annotatedBeanDefinition.getMetadata()
                        .getAnnotationAttributes(RpcClient.class.getCanonicalName());
                //将RpcClient的工厂类注册进去
                BeanDefinitionBuilder builder = BeanDefinitionBuilder
                        .genericBeanDefinition(RpcClinetFactoryBean.class);
                //设置RpcClinetFactoryBean工厂类中的构造函数的值
                builder.addConstructorArgValue(beanClassAllName);
                builder.getBeanDefinition().setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
                //将其注册进容器中
                registry.registerBeanDefinition(
                        beanClassAllName ,
                        builder.getBeanDefinition());
            }
        }
    }
    //允许Spring扫描接口上的注解
    protected ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
            }
        };
    }
}

由于上面注册的是工厂类,所以我们建立一个工厂类RpcClinetFactoryBean继承Spring中的FactoryBean类,由其统一创建@RpcClient注解的代理类

如果对FactoryBean类不了解的可以参见FactoryBean讲解

https://juejin.im/post/5c3716355188252631635eb1

@Data
public class RpcClinetFactoryBean implements FactoryBean {

    @Autowired
    private RpcDynamicPro rpcDynamicPro;

    private Class<?> classType;


    public RpcClinetFactoryBean(Class<?> classType) {
        this.classType = classType;
    }

    @Override
    public Object getObject(){
        ClassLoader classLoader = classType.getClassLoader();
        Object object = Proxy.newProxyInstance(classLoader,new Class<?>[]{classType},rpcDynamicPro);
        return object;
    }

    @Override
    public Class<?> getObjectType() {
        return this.classType;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}

注意此处的getObjectType方法,在将工厂类注入到容器中的时候,这个方法返回的是什么Class类型那么注册进容器中就是什么Class类型。

然后看一下我们创建的代理类rpcDynamicPro

@Component
@Slf4j
public class RpcDynamicPro implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       String requestJson = objectToJson(method,args);
        Socket client = new Socket("127.0.0.1", 20006);
        client.setSoTimeout(10000);
        //获取Socket的输出流,用来发送数据到服务端
        PrintStream out = new PrintStream(client.getOutputStream());
        //获取Socket的输入流,用来接收从服务端发送过来的数据
        BufferedReader buf =  new BufferedReader(new InputStreamReader(client.getInputStream()));
        //发送数据到服务端
        out.println(requestJson);
        Response response = new Response();
        Gson gson =new Gson();
        try{
            //从服务器端接收数据有个时间限制(系统自设,也可以自己设置),超过了这个时间,便会抛出该异常
            String responsJson = buf.readLine();
            response = gson.fromJson(responsJson, Response.class);
        }catch(SocketTimeoutException e){
            log.info("Time out, No response");
        }
        if(client != null){
            //如果构造函数建立起了连接,则关闭套接字,如果没有建立起连接,自然不用关闭
            client.close(); //只关闭socket,其关联的输入输出流也会被关闭
        }
        return response.getResult();
    }

    public String objectToJson(Method method,Object [] args){
        Request request = new Request();
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        String className = method.getDeclaringClass().getName();
        request.setMethodName(methodName);
        request.setParameTypes(parameterTypes);
        request.setParameters(args);
        request.setClassName(getClassName(className));
        GsonBuilder gsonBuilder = new GsonBuilder();
        gsonBuilder.registerTypeAdapterFactory(new ClassTypeAdapterFactory());
        Gson gson = gsonBuilder.create();
        return gson.toJson(request);
    }

    private String getClassName(String beanClassName){
        String className = beanClassName.substring(beanClassName.lastIndexOf(".")+1);
        className = className.substring(0,1).toLowerCase() + className.substring(1);
        return className;
    }
}

我们的客户端已经写完了,传给服务端的信息我们也已经拼装完毕了。剩下的工作就简单了,开始编写服务端的代码。

服务端处理完以后应该返回客户端什么信息?-服务端

服务端的代码相比较客户端来说要简单一些。可以简单分为下面三步

  • 拿到接口名以后,通过接口名找到实现类
  • 通过反射进行对应方法的执行
  • 返回执行完的信息

那么我们就根据这三步进行编写代码

拿到接口名以后,通过接口名找到实现类

如何通过接口名拿到对应接口的实现类呢?这就需要我们在服务端启动的时候将其对应信息加载进去

@Component
@Log4j
public class InitRpcConfig implements CommandLineRunner {
    @Autowired
    private ApplicationContext applicationContext;

    public static Map<String,Object> rpcServiceMap = new HashMap<>();

    @Override
    public void run(String... args) throws Exception {
        Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(Service.class);
        for (Object bean: beansWithAnnotation.values()){
            Class<?> clazz = bean.getClass();
            Class<?>[] interfaces = clazz.getInterfaces();
            for (Class<?> inter : interfaces){
                rpcServiceMap.put(getClassName(inter.getName()),bean);
                log.info("已经加载的服务:"+inter.getName());
            }
        }
    }

    private String getClassName(String beanClassName){
        String className = beanClassName.substring(beanClassName.lastIndexOf(".")+1);
        className = className.substring(0,1).toLowerCase() + className.substring(1);
        return className;
    }
}

此时rpcServiceMap存储的就是接口名和其对应的实现类的对应关系。

通过反射进行对应方法的执行

此时拿到了对应关系以后就能根据客户端传过来的信息找到相应的实现类中的方法。然后进行执行并返回信息就行

public Response invokeMethod(Request request){
        String className = request.getClassName();
        String methodName = request.getMethodName();
        Object[] parameters = request.getParameters();
        Class<?>[] parameTypes = request.getParameTypes();
        Object o = InitRpcConfig.rpcServiceMap.get(className);
        Response response = new Response();
        try {
            Method method = o.getClass().getDeclaredMethod(methodName, parameTypes);
            Object invokeMethod = method.invoke(o, parameters);
            response.setResult(invokeMethod);
        } catch (NoSuchMethodException e) {
            log.info("没有找到"+methodName);
        } catch (IllegalAccessException e) {
            log.info("执行错误"+parameters);
        } catch (InvocationTargetException e) {
            log.info("执行错误"+parameters);
        }
        return response;
    }

现在我们两个服务都启动起来并且在客户端进行调用就发现只是调用接口就能调用过来了。

总结

到现在一个简单的RPC就完成了,但是其中还有很多的功能需要完善,例如一个完整RPC框架肯定还需要服务注册与发现,而且双方通信肯定也不能是直接开启一个线程一直在等着,肯定需要是异步的等等的各种功能。

后面随着学习的深入,这个框架也会慢慢增加一些东西。不仅是对所学知识的一个应用,更是一个总结。有时候学一个东西学起来觉得很简单,但是真正应用的时候就会发现各种各样的小问题。

比如在写这个例子的时候碰到一个问题就是@Autowired的时候一直找不到SendMessage的类型,最后才发现是工厂类RpcClinetFactoryBean中的getObjectType中的返回类型写错了,我之前写的是

    public Class<?> getObjectType() {
        return this.getClass();;
    }

这样的话注册进容器的就是RpcClinetFactoryBean类型的而不是SendMessage的类型。

源码

https://github.com/modouxiansheng/Doraemon

本文分享自微信公众号 - 好好学java(SIHAIloveJAVA)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一枝花算不算浪漫

策略模式+注解 干掉业务代码中冗余的if else...

之前写过一个工作中常见升级模式-策略模式 的文章,里面讲了具体是怎样使用策略模式去抽象现实中的业务代码,今天来拿出实际代码来写个demo,这里做个整理来加深自己...

13640
来自专栏Java技术栈

年轻人的第一个自定义 Spring Boot Starter!

陆陆续续,零零散散,栈长已经写了几十篇 Spring Boot 系列文章了,其中有介绍到 Spring Boot Starters 启动器,使用的、介绍的都是第...

10010
来自专栏吉林乌拉

Spring使用注解声明事务

在上一篇中我们已经简单的介绍了用xml的方式声明事务,spring中除了上述方式外,还可以直接使用注解的方式管理事务,也就是通过@Transactional注解...

13230
来自专栏玩转JavaEE

Spring Boot2 系列教程(六)自定义 Spring Boot 中的 starter

我们使用 Spring Boot,基本上都是沉醉在它 Stater 的方便之中。Starter 为我们带来了众多的自动化配置,有了这些自动化配置,我们可以不费吹...

9430
来自专栏玩转JavaEE

Spring Boot 整合 OAuth2,松哥手把手教你!

OAuth2 ,绝对让很多小伙伴头大。不像其他的技术点,随便写几行代码,就能跑一个 Demo 出来,OAuth2 你想跑一个 Demo 出来,都得写半天代码。

24850
来自专栏upuptop的专栏

SpringBoot集成ES实现存储、查询

spring-boot-starter-data-elasticsearch:是springboot整合es的一个快速开发包。用过JPA的朋友应该知道,spri...

1.1K20
来自专栏大数据实战演练

实操:No beans of 'FastDFS Client' type found 的解决方法

required a bean of type 'org.springframework.fasfdfs.server.FastDFSClient' that ...

12820
来自专栏一块自留地

Spring Ioc源码分析 之 Bean的加载(五):实例化Bean

在doCreateBean()代码 <2> 处,有一行代码instanceWrapper = createBeanInstance(beanName, mbd,...

13040
来自专栏闻人的技术博客

掌握 Spring 之事件处理

1 前言2.1 Spring 标准事件处理2.1.1 注解驱动的事件侦听引入 @EventListener事件的传递2.1.2 侦听器优先级2.2 自定义事件2...

13540
来自专栏闻人的技术博客

译 - Spring 核心技术之 Spring 容器扩展点

本文内容选自 Spring Framework 5.1.6.RELEASE 官方文档中 core 部分的 1.8 小节,简单介绍了如何利用 Spring 容器扩...

11630

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励