Java技术栈

www.javastack.cn

优秀的Java技术公众号

来源:juejin.im/post/5c4481a4f265da613438aec3

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

发现这些知识都了解一些。所以就想着试试自己实现一个简单的RPC框架,即巩固了基础的知识,也能更加深入的了解RPC原理。

当然一个完整的RPC框架包含了许多的功能,例如服务的发现与治理,网关等等,本篇只是简单的实现了一个调用的过程。

传参出参分析

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

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

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

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

第一类是服务端可以根据这个信息找到相应的接口实现类和方法

第二类是调用此方法传输的参数信息

那么我们就根据要传输的两类信息进行分析,什么信息能够找到相应的实现类的相应的方法?要找到方法必须要先找到类,这里我们可以简单的用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注解的代理类。推荐阅读:Spring零配置之@Configuration注解详解。

@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技术栈微信公众号,在后台回复关键字:Java,可以获取一份栈长整理的 Java 最新技术干货。

最近干货分享

公司不用 Spring Boot,果断离职了!

Java 12 骚操作, 文件比对居然还能这样玩

Java 12 骚操作, String居然还能这样玩

Spring Boot 1.x 正式退役,2.x大步向前

分享一份 2019 最新 Java 架构师学习资料

点击「阅读原文」加入栈长的战队~

从0 开始手写一个 RPC 框架,轻松搞定!的更多相关文章

  1. 手写一个RPC框架

    一.前言 前段时间看到一篇不错的文章<看了这篇你就会手写RPC框架了>,于是便来了兴趣对着实现了一遍,后面觉得还有很多优化的地方便对其进行了改进. 主要改动点如下: 除了Java序列化协议 ...

  2. 从 0 开始手写一个 Mybatis 框架,三步搞定!

    阅读本文大概需要 3 分钟. MyBatis框架的核心功能其实不难,无非就是动态代理和jdbc的操作,难的是写出来可扩展,高内聚,低耦合的规范的代码. 本文完成的Mybatis功能比较简单,代码还有许 ...

  3. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  4. 看年薪50W的架构师如何手写一个SpringMVC框架

    前言 做 Java Web 开发的你,一定听说过SpringMVC的大名,作为现在运用最广泛的Java框架,它到目前为止依然保持着强大的活力和广泛的用户群. 本文介绍如何用eclipse一步一步搭建S ...

  5. 自己手写一个SpringMVC 框架

    一.了解SpringMVC运行流程及九大组件 1.SpringMVC 的运行流程   · 用户发送请求至前端控制器DispatcherServlet · DispatcherServlet收到请求调用 ...

  6. 从零开始手写 dubbo rpc 框架

    rpc rpc 是基于 netty 实现的 java rpc 框架,类似于 dubbo. 主要用于个人学习,由渐入深,理解 rpc 的底层实现原理. 前言 工作至今,接触 rpc 框架已经有很长时间. ...

  7. 手写实现RPC框架(不带注册中心和带注册中心两种)

    实现自己的RPC框架如果不需要自定义协议的话那就要基于Socket+序列化. ProcessorHandler:主要是用来处理客户端的请求. package dgb.nospring.myrpc; i ...

  8. 手写一个HTTP框架:两个类实现基本的IoC功能

    jsoncat: 仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架 国庆节的时候,我就已经把 jsoncat 的 IoC 功能给写了,具体可以看这篇文章&l ...

  9. 【Spring系列】自己手写一个 SpringMVC 框架

    参考文章 一.了解SpringMVC运行流程及九大组件 1.SpringMVC的运行流程 1)用户发送请求至前端控制器DispatcherServlet 2)DispatcherServlet收到请求 ...

随机推荐

  1. JavaScript深入之从原型到原型链(转载)

    构造函数创建对象 我们先使用构造函数创建一个对象: function Person() { } var person = new Person(); person.name = 'Kevin'; co ...

  2. #1062 - Duplicate entry '0' for key 'PRIMARY'—— mysql的小问题

    问题:# 1062 -重复输入“0”. 原因:我估计可能是数据表中主键这一栏已经有一个为“0”了,一般出现这种问题是以int类型的字段在输入时没有输如数据,而int类型默认值为“0”,而你之前第一条数 ...

  3. FreeSWITCH Explained / Configuration / Proxy Media

    FreeSWITCH has 3 media handling modes: Default: media flows through FS, full processing options - RT ...

  4. 2018-5-5-UWP-和-WPF-对比

    title author date CreateTime categories UWP 和 WPF 对比 lindexi 2018-05-05 17:23:33 +0800 2018-04-16 20 ...

  5. WebAuthorize(中间件对所有请求进行拦截)core只有通过添加中间件过滤请求方式 而非继承然后写特性的那种方式

    一.WebAuthorize 1.项目名称 WebAuthorize 2.加个中间件 过滤请求. using Microsoft.AspNetCore.Builder; using Microsoft ...

  6. jpype测试报错,找不到类raise _RUNTIMEEXCEPTION.PYEXC("Class %s not found" % name)

    最近用jpype测试java代码 公司电脑跑着没有问题,家里电脑怎么也不行,python,jdk版本啥的都一样,但总是报找不到类名的错误 raise _RUNTIMEEXCEPTION.PYEXC(& ...

  7. Mybatis学习笔记大纲

    Mybatis学习笔记大纲: 一.MyBatis简介 二.MyBatis-HelloWorld 三.MyBatis-全局配置文件 四.MyBatis-映射文件 五.MyBatis-动态SQL 六.My ...

  8. sonar-runner命令模式运行sonar

    适用环境:该种配置的模式适用于本地调试模式 前提条件:在工程路径下创建sonar-project.properties文件 该客户端的路径在系统配置文件中进行了定义 alias sonar-runne ...

  9. Vue项目【饿了么App】mock数据【data.json】

    1.前后端分离式开发,约定好数据字段接口! 2.前端mock静态数据,开发完毕后,与后端进行数据联调! 3.vue.config.js 配置 devServer const appData = req ...

  10. 笨办法学Python(learn python the hard way)--练习程序39-40

    下面是练习39-练习40,基于python3 #ex39.py 1 ten_things = "Apples Oranges Crows Telephone Light Sugar" ...