RPC开发2_全局配置加载

一、需求分析

在 RPC 框架运行的过程中,会涉及到很多的配置信息,比如注册中心的地址、序列化方式、网络服务器端口号等等。

之前的简易版 RPC 项目中,我们是在程序里硬编码了这些配置,不利于维护。

而且 RPC 框架是需要被其他项目作为服务提供者或者服务消费者引入的,我们应当允许引入框架的项目通过编写配置文件来 自定义配置。并且一般情况下,服务提供者和服务消费者需要编写相同的 RPC 配置。

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

二、设计方案

配置项

首先我们梳理需要的配置项,刚开始就一切从简,只提供以下几个配置项即可:

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

后续随着框架功能的扩展,我们会不断地新增配置项,还可以适当地对配置项进行分组。

比如以下是一些常见的 RPC 框架配置项,仅做了解:

  1. 注册中心地址:服务提供者和服务消费者都需要指定注册中心的地址,以便进行服务的注册和发现。
  2. 服务接口:服务提供者需要指定提供的服务接口,而服务消费者需要指定要调用的服务接口。
  3. 序列化方式:服务提供者和服务消费者都需要指定序列化方式,以便在网络中传输数据时进行序列化和反序列化。
  4. 网络通信协议:服务提供者和服务消费者都需要选择合适的网络通信协议,比如 TCP、HTTP 等。
  5. 超时设置:服务提供者和服务消费者都需要设置超时时间,以便在调用服务时进行超时处理。
  6. 负载均衡策略:服务消费者需要指定负载均衡策略,以决定调用哪个服务提供者实例。
  7. 服务端线程模型:服务提供者需要指定服务端线程模型,以决定如何处理客户端请求。

作为参考可以了解 Dubbo RPC 框架的配置项,包括应用配置、注册中心配置、服务配置等。

参考 Dubbo:https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/config/api/

读取配置文件

如何读取配置文件呢?可以使用 Java 的 Properties 类自行编写,但更推荐使用一些第三方工具库,比如 Hutool 的 Setting 模块,可以直接读取指定名称的配置文件中的部分配置信息,并且转换成 Java 对象,非常方便。

参考官方文档:https://doc.hutool.cn/pages/Props/。

一般情况下,我们读取的配置文件名称为 application.properties,还可以通过指定文件名称后缀的方式来区分多环境,比如 application-prod.properties 表示生产环境、 application-test.properties 表示测试环境。

三、开发实现

1、项目初始化

2、配置加载

1)在 config 包下新建配置类 RpcConfig,用于保存配置信息。

可以给属性指定一些默认值,完整代码如下:

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
/**
* RPC 框架配置
*/
@Data
public class RpcConfig {

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

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

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

/**
* 服务器端口号
*/
private Integer serverPort = 8080;

}

2)在 utils 包下新建工具类 ConfigUtils,作用是读取配置文件并返回配置对象,可以简化调用。

工具类应当尽量通用,和业务不强绑定,提高使用的灵活性。比如支持外层传入要读取的配置内容前缀、支持传入环境等。

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
/**
* 配置工具类
*/
public class ConfigUtils {

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

/**
* 加载配置对象,支持区分环境
*
* @param tClass
* @param prefix
* @param environment
* @param <T>
* @return
*/
public static <T> T loadConfig(Class<T> tClass, String prefix, 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, prefix);
}
}

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

3)在 constant 包中新建 RpcConstant 接口,用于存储 RPC 框架相关的常量。

比如默认配置文件的加载前缀为 rpc

1
2
3
4
5
6
7
8
9
10
11
12
package com.yupi.yurpc.constant;

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

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

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

1
2
3
rpc.name=yurpc
rpc.version=2.0
rpc.serverPort=8081

3、维护全局配置对象

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

使用设计模式中的 单例模式,就能够很轻松地实现这个需求了。

一般情况下,我们会使用 holder 来维护全局配置对象实例。在我们的项目中,可以换一个更优雅的命名,使用 RpcApplication 类作为 RPC 项目的启动入口、并且维护项目全局用到的变量。

完整代码如下:

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
/**
* 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 来加载配置。

以后 RPC 框架内只需要写一行代码,就能正确加载到配置:

1
RpcConfig rpc = RpcApplication.getRpcConfig();