项目3:从0开始的RPC框架(扩展版)

作者 : admin 本文共8022个字,预计阅读时间需要21分钟 发布时间: 2024-06-10 共2人阅读

一. 全局配置加载

1. 需求分析

通常情况下,在RPC框架运行的会涉及到多种配置信息,比如注册中心的地址、序列化方式、网络服务端接口号等。

在简易版框架中,硬编码了这些配置,也就是都写死了,在真实的应用环境中是不利于维护和后期扩展的。同时RPC框架需要被其它项目引入,作为服务提供者和消费者沟通的桥梁,所以应当允许引入框架的项目通过编写配置文件来自定义配置。一般情况下,服务提供者与消费者需要编写相同的RPC配置。

综上,我们需要一套全局配置加载功能。能够让RPC框架轻松地从配置文件中读取配置,并且维护一个全局配置对象,便于框架快速获取到一致的配置。

2. 设计方案

(1)配置项

从最简单出发,先提供几个基础配置项:

  • name 名称
  • version 版本号
  • serverHost 服务器主机名
  • serverPort 服务器端口号

之后随着框架功能的扩展再不断增加新配置即可,比如注册中心地址、服务接口、序列化方式等。

可参考:Dubbo RPC框架的配置项。包括应用配置、协议配置、注册中心等。

(2)读取配置文件

配置文件的读取,使用Java的Properties类自行编写。通常情况下,读取的配置文件名称为application.properties,还可以通过指定文件名称后缀的方式来区分多环境,比如application-prod.properties表示生产环境,application-test.properties表示测试环境。

3. 具体实现

(1)项目初始化

创建khr-rpc-core模块,扩展版RPC项目都基于此模块进行。直接复制粘贴easy模块包并改名。

引入日志库(ch.qos.logback)和单元测试(junit)依赖,并将consumer和provider模块引入的RPC依赖都替换成khr-rpc-core。

(2)配置加载

创建配置类RpcConfig:

用于保存配置信息。

项目3:从0开始的RPC框架(扩展版)插图

可以给属性指定一些默认值,

package com.khr.krpc.config;

import lombok.Data;

/**
 * RPC框架配置
 */
@Data
public class RpcConfig {

    /**
     * 名称
     */
    private String name = "k-rpc";

    /**
     * 版本号
     */
    private String version = "1.0";

    /**
     * 服务器主机名
     */
    private String serverHost = "localhost";

    /**
     * 服务器端口号
     */
    private String serverPort = "8080";
}

创建工具类ConfigUtils: 

用于读取配置文件并返回配置对象,可以简化调用。配置类应当尽量通用,不和业务强绑定。

项目3:从0开始的RPC框架(扩展版)插图(1)

之后调用ConfigUtils的静态方法loadConfig就能读取配置了。

package com.khr.krpc.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.setting.dialect.Props;

/**
 * 配置工具类
 */
public class ConfigUtils {

    /**
     * 加载配置对象
     *
     * @param tClass
     * @param perfix
     * @param 
     * @return
     */
    public static  T loadConfig(Class tClass,String perfix){
        return loadConfig(tClass,perfix,"");
    }

    /**
     * 加载配置对象,支持区分环境
     *
     * @param tClass
     * @param perfix
     * @param environment
     * @param 
     * @return
     */
    public static  T loadConfig(Class tClass,String perfix,String environment){
        StringBuilder configFileBuilder = new StringBuilder("application");
        if (StrUtil.isNotBlank(environment)){
            configFileBuilder.append("-").append(environment);
        }
        configFileBuilder.append(".properties");
        Props props = new Props(configFileBuilder.toString());
        return props.toBean(tClass,perfix);
    }
}

创建RpcConstant接口:

用于存储RPC框架相关的常量。比如默认配置文件的加载前缀为rpc。

项目3:从0开始的RPC框架(扩展版)插图(2)

package com.khr.krpc.constant;

/**
 * RPC相关常量
 */
public interface RpcConstant {

    /**
     * 默认配置文件加载前缀
     */
    String DEFAULT_CONFIG_PREFIX = "rpc";
}

可以读取到类似下面的配置:

rpc.name=krpc
rpc.version=2.0
rpc.serverPort=8081

(3)维护全局配置对象

RPC框架中需要维护一个全局的配置对象。在引入RPC框架后并启动项目时,从配置文件中读取配置并创建对象实例,之后就可以集中地从这个对象中获取配置信息,而不需要每次加载配置时再重新读取并创建对象,减少了性能开销。

使用了设计模式中的单例模式。通常情况下会使用holder来维护全局配置对象实例,在本项目中使用RpcApplication类作为RPC项目的启动入口,并维护项目全局用到的变量。

项目3:从0开始的RPC框架(扩展版)插图(3)

package com.khr.krpc;

import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.constant.RpcConstant;
import com.khr.krpc.utils.ConfigUtils;
import lombok.extern.slf4j.Slf4j;

/**
 * RPC框架应用
 * 相当于holder,存放了项目全局用到的变量。双检锁单例模式实现。
 */
@Slf4j
public class RpcApplication {

    private static volatile RpcConfig rpcConfig;

    /**
     * 框架初始化,支持传入自定义配置
     *
     * @param newRpcConfig
     */
    public static void init(RpcConfig newRpcConfig){
        rpcConfig = newRpcConfig;
        log.info("rpc init, config = {}",newRpcConfig.toString());
    }

    /**
     * 初始化
     */
    public static void init(){
        RpcConfig newRpcConfig;
        try {
            newRpcConfig = ConfigUtils.loadConfig(RpcConfig.class,RpcConstant.DEFAULT_CONFIG_PREFIX);
        }catch (Exception e){
            //配置加载失败,使用默认值
            newRpcConfig =new RpcConfig();
        }
        init(newRpcConfig);
    }

    /**
     * 获取配置
     *
     * @return
     */
    public static RpcConfig getRpcConfig(){
        if (rpcConfig == null){
            synchronized (RpcApplication.class){
                if (rpcConfig == null){
                    init();
                }
            }
        }
        return rpcConfig;
    }
}

 双检锁单例模式的经典实现,支持在获取配置时才调用init方法实现懒加载。

为了便于扩展,还支持自己传入配置对象,如果不传入的话,默认调用前面写好的ConfigUtils来加载配置。

之后一行代码即可正确加载配置:

RpcConfig rpc = RpcApplication.getRpcConfig();

4. 测试

(1)测试配置文件读取

在example-consumer模块的resources目录下编写配置文件application.properties,

项目3:从0开始的RPC框架(扩展版)插图(4)

rpc.name=krpc
rpc.version=2.0
rpc.serverPort=8081

创建ConsumerExample作为扩展版RPC项目的示例消费者类,测试配置文件读取,

package com.khr.example.consumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.proxy.ServiceProxyFactory;
import com.khr.krpc.utils.ConfigUtils;

/**
 * 服务消费者示例
 */
public class ConsumerExample {

    public static void main(String[] args){

        RpcConfig rpc = ConfigUtils.loadConfig(RpcConfig.class,"rpc");
        System.out.println(rpc);
    }
}

读取结果为,

项目3:从0开始的RPC框架(扩展版)插图(5)

已成功读到配置文件中的内容。 

(2)测试全局配置对象加载

在example-provider模块中创建ProviderExample服务提供者示例类,能够根据配置动态地在不同端口启动Web服务

package com.khr.example.provider;

import com.khr.example.common.service.UserService;
import com.khr.krpc.RpcApplication;
import com.khr.krpc.registry.LocalRegistry;
import com.khr.krpc.server.HttpServer;
import com.khr.krpc.server.VertxHttpServer;

/**
 * 服务提供者示例
 */
public class ProviderExample {

    public static void main(String[] args){
        //RPC框架初始化
        RpcApplication.init();

        //注册服务
        LocalRegistry.registry(UserService.class.getName(),UserServiceImpl.class);

        //启动web服务
        HttpServer httpServer = new VertxHttpServer();
        httpServer.doStart(Integer.parseInt(RpcApplication.getRpcConfig().getServerPort()));
    }
}

启动结果为,

项目3:从0开始的RPC框架(扩展版)插图(6)

 项目3:从0开始的RPC框架(扩展版)插图(7)

已成功在8080端口启动。 

至此,扩展功能,全局配置加载完成,后续可能会根据新增的功能逐步修改全局配置信息。

二. Mock服务

1. 需求分析

(1)什么是Mock?

Mock是指模拟对象或模拟数据,通常用于代码测试,方便开发者跑通业务流程。

(2)为什么要支持Mock?

RPC框架的核心功能是调用其它远程服务,但在实际的开发和测试环境中,有时可能会因为各种不可控因素导致无法访问远程服务,比如网络延迟、远程服务宕机等。因此需要使用Mock服务进行模拟调用,以进行接口测试。

比如用户想调用订单服务,但是该服务还未上线,流通就跑不通,所以不如先设置一个模拟对象orderService,调用它的order方法时,任意返回一个默认值,以进行接口测试。

此外,Mock服务开发成本不高,并且RPC框架支持Mock后,开发者不必依赖真实的远程服务,轻松调用服务接口跑通流程,提高使用体验。并且支持用最简单的方式,一个配置就让开发者使用Mock服务。

2. 设计方案

之前在简易版RPC框架中曾利用动态代理的方式创建对象,同理,通过动态代理创建一个调用方法时返回固定值的对象,即模拟对象。

3. 具体实现

(1)全局配置类新增Mock字段

支持开发者通过修改配置文件的方式开启Mock,默认值为false,

package com.khr.krpc.config;

import lombok.Data;

/**
 * RPC框架配置
 */
@Data
public class RpcConfig {

    ...

    /**
     * 模拟调用
     */
    private boolean mock = false;
}

(2)创建MockServiceProxy类

用于生成Mock代理服务。提供一个根据服务接口类型返回固定值的方法。

项目3:从0开始的RPC框架(扩展版)插图(8)

通过getDefaultObject方法,根据代理接口的class返回不同的默认值,比如boolean类型返回false,对象类型返回null,long类型返回0L等。

package com.khr.krpc.proxy;

import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * Mock 服务代理(JDK动态代理)
 */
@Slf4j
public class MockServiceProxy implements InvocationHandler{

    /**
     * 调用代理
     *
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)throws Throwable{
        //根据方法的返回值类型,生成特定的默认值对象
        Class methodReturnType = method.getReturnType();
        log.info("moke invoke {}",method.getName());
        return getDefaultObject(methodReturnType);
    }

    /**
     * 生成指定类型的默认值对象
     *
     * @param type
     * @return
     */
    private Object getDefaultObject(Class type){
        //基本类型
        if (type.isPrimitive()){
            if (type == boolean.class){
                return false;
            } else if (type == short.class){
                return (short) 0;
            } else if (type == int.class){
                return 0;
            } else if(type == long.class){
                return 0L;
            }
        }
        //对象类型
        return null;
    }
}

(3)在ServiceProxyFactory服务代理工厂类中新增Mock代理对象的方法getMockProxy

通过读取已定义的全局配置Mock来区分创建哪种代理对象,

package com.khr.krpc.proxy;

import java.lang.reflect.Proxy;
import com.khr.krpc.RpcApplication;

/**
 * 服务代理工厂(用于创建代理对象)
 */

public class ServiceProxyFactory {

    /**
     * 根据服务类获取代理对象
     *
     * @param serviceClass
     * @param 
     * @return
     */
    public static  T getProxy(Class serviceClass){
        if (RpcApplication.getRpcConfig().isMock()){
            return getMockProxy(serviceClass);
        }

        return (T) Proxy.newProxyInstance(
                serviceClass.getClassLoader(),
                new Class[]{serviceClass},
                new ServiceProxy());
    }

    /**
     * 根据服务类获取Mock代理对象
     *
     * @param serviceClass
     * @param 
     * @return
     */
    public static  T getMockProxy(Class serviceClass){
        return (T) Proxy.newProxyInstance(
                serviceClass.getClassLoader(),
                new Class[]{serviceClass},
                new MockServiceProxy());
    }
}

4. 测试

在common模块的UserService中新增一个具有默认实现的新方法,通过调用该方法来测试Mock代理服务是否生效,即查看调用的是模拟服务还是真实服务,

package com.khr.example.common.service;

import com.khr.example.common.model.User;

/**
 * 用户服务
 */
public interface UserService {
    /**
     * 获取用户
     *
     * @param user
     * @return
     */
    User getUser(User user);

    /**
     * 测试方法 - 获取数字
     */
    default short getNumber(){
        return 1;
    }
}

在application.properties配置文件中,将mock设置为true,即开启模拟服务,

rpc.name=krpc
rpc.version=2.0
rpc.mock = true

在ConsumerExample类中新增调用userService.getNumber的测试代码,

package com.khr.example.consumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.proxy.ServiceProxyFactory;
import com.khr.krpc.utils.ConfigUtils;

/**
 * 服务消费者示例
 */
public class ConsumerExample {

    public static void main(String[] args){

        //获取代理
        UserService userService = ServiceProxyFactory.getProxy(UserService.class);
        User user = new User();
        user.setName("KHR");
        //调用
        User newUser = userService.getUser(user);
        if (newUser != null){
           System.out.println(newUser.getName());
        } else {
            System.out.println("user == null");
        }
        long number = userService.getNumber();
        System.out.println(number);
    }
}

因为开启了Mock服务,所以会去调用MockServiceProxy模拟服务代理,因为getNumber方法返回值是1,即int类型,所以Mock服务的返回值为0,也就是输出结果为0。

项目3:从0开始的RPC框架(扩展版)插图(9)

如果没有开启Mock服务,设置为mock = false,那么返回值为,

项目3:从0开始的RPC框架(扩展版)插图(10)

 即调用了真实的服务方法getUser和getNumber。

至此,扩展功能,Mock服务开发完成,后续可能会根据新增的功能通过Mock服务进行测试。

本站无任何商业行为
个人在线分享 » 项目3:从0开始的RPC框架(扩展版)
E-->