跳到主要内容

全局配置加载

为什么要全局配置加载?

在开发 RPC 框架时需要引入全局配置加载的功能主要有以下几个原因:

  1. 配置信息繁多:

    • RPC 框架需要涉及很多配置信息,如注册中心地址、序列化方式、网络服务端口号等。
    • 如果直接在代码中硬编码这些配置,不利于后期维护和扩展。
  2. 支持自定义配置:

    • RPC 框架需要被其他项目引入使用,作为服务提供者或消费者。
    • 引入框架的项目应该能够通过配置文件自定义 RPC 框架的配置,而不是强制使用框架中的硬编码配置。
  3. 统一配置管理:

    • 服务提供者和服务消费者需要使用相同的 RPC 框架配置,以保证网络通信的一致性。
    • 因此需要一个统一的全局配置对象,方便框架内部各组件快速获取一致的配置信息。
  4. 配置文件读取:

    • RPC 框架需要能够从配置文件中读取配置信息,并将其转换为Java对象。
    • 使用通用的配置读取工具(如Hutool)可以简化这个过程,提高代码复用性。

如何设计?

参考Dubbo的设计方案:https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/config/overview/

在本RPC框架中,我们主要做了以下一些配置:

RpcConfig 类是 RPC 框架的全局配置类,它包含了 RPC 框架中很多重要的配置项。

  1. 名称和版本:

    • nameversion 属性用于标识 RPC 框架的名称和版本号。这些信息可以在日志、监控等场景中使用。
  2. 服务器配置:

    • serverHostserverPort 定义了 RPC 服务器的主机和端口信息。这是 RPC 服务提供者端的必要配置。
  3. 模拟调用:

    • mock 属性用于控制是否开启模拟调用模式,可在测试或者特殊场景下使用。
  4. 序列化配置:

    • serializer 属性指定了默认的序列化实现,可以选择 SerializerKeys 中定义的不同序列化方式,如 JDK、Kryo 等。
  5. 注册中心配置:

    • registryConfig 属性包含了服务注册中心的配置信息,如地址、凭证等。这是 RPC 的关键功能之一。
  6. 负载均衡配置:

    • loadBalancer 属性指定了默认的负载均衡策略,可以选择 LoadBalancerKeys 中定义的不同策略,如轮询、随机等。
  7. 容错策略配置:

    • retryStrategy 属性用于配置服务调用的重试策略,如 RetryStrategyKeys 中定义的不重试、有限重试等。
    • tolerantStrategy 属性用于配置服务调用的容错策略,如 TolerantStrategyKeys 中定义的快速失败、熔断等。

这个 RpcConfig 类将 RPC 框架中的各种重要配置项集中在一起,使得整个框架的配置管理更加集中和便捷。开发者可以根据具体需求,灵活地配置不同的序列化、负载均衡、容错等策略,从而满足不同应用场景的需求。

为了更直观表示我们整个系统的配置,我画了一张框架图:

rpcconfig.svg

上面这些只是Rpc的配置,除此之外,我们还需要一个RpcApplication 类来管理 RPC 框架的入口和全局配置管理器,我们希望在里面可以集中管理这些配置选项,并且可以轻松获取这些选项,例如

  • 初始化Rpc配置信息
  • 通过单例模式来获取上述的Rpc配置信息

代码实现

RpcConfig类代码如下:

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

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

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

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

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

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

/**
* 序列化器
*/
private String serializer = SerializerKeys.JDK;

/**
* 注册中心配置
*/
private RegistryConfig registryConfig = new RegistryConfig();

/**
* 负载均衡器
*/
private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;

/**
* 重试策略
*/
private String retryStrategy = RetryStrategyKeys.NO;

/**
* 容错策略
*/
private String tolerantStrategy = TolerantStrategyKeys.FAIL_FAST;
}

RpcApplication类:


/**
* RPC应用
* 相当于holder ,存放了项目全局用到的变量,双检锁实现单例
*/
@Slf4j
public class RpcApplication {
private static volatile RpcConfig rpcConfig;

public static void init(RpcConfig newRpcConfig) {
rpcConfig = newRpcConfig;
log.info("rpc application init success,config:{}", rpcConfig);
// // 注册中心初始化
RegistryConfig registryConfig = rpcConfig.getRegistryConfig();
Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
registry.init(registryConfig);
log.info("registry init success,config:{}", registryConfig);

// 创建并 注册Shutdown Hook ,JVM 退出时执行擦欧总
Runtime.getRuntime().addShutdownHook(new Thread(registry::destroy));
}

/**
* 初始化
*/
public static void init() {
RpcConfig newRpcConfig;
try {
newRpcConfig = ConfigUtils.loadConfig(RpcConfig.class, RpcConstant.DEFAULT_CONFIG_PREFIX);
} catch (Exception e) {
// 读取配置文件失败,使用默认配置
log.error("load config error,use default config", e);
newRpcConfig = new RpcConfig();
}
init(newRpcConfig);
}

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

}

代码解释:

  1. 单例模式实现:
    • rpcConfig 是一个静态的 volatile 变量,用于存储全局的 RpcConfig 配置对象。
    • getRpcConfig() 方法使用了双重检查锁的单例模式实现,确保 rpcConfig 对象的唯一性。
  2. 初始化方法:
    • init(RpcConfig newRpcConfig) 方法用于初始化 RPC 应用,接受一个 RpcConfig 对象作为参数。
    • 然后根据 registryConfig 创建并初始化注册中心实例。
    • 最后注册一个 JVM 关闭钩子,在 JVM 退出时自动销毁注册中心实例。
  3. 自动初始化:
    • init() 方法用于自动初始化 RPC 应用。
    • 首先尝试使用 ConfigUtils.loadConfig() 方法从默认的配置文件路径加载 RpcConfig 对象。
    • 如果加载失败,则创建一个默认的 RpcConfig 对象。

这个 RpcApplication 类是整个 RPC 框架的核心入口点。它负责管理全局的 RpcConfig 配置对象,同时还负责初始化注册中心实例并确保其生命周期与 JVM 保持一致。这种设计模式能够确保整个 RPC 框架的配置管理和初始化过程是统一和可靠的。

我们再来看看如何从配置文件中读取信息:

 newRpcConfig = ConfigUtils.loadConfig(RpcConfig.class, RpcConstant.DEFAULT_CONFIG_PREFIX);

对于propertities文件的读取比较 简单,可以直接使用Hutool工具类实现:

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);
}

读取yml文件需要引入依赖:

        <dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>

读取配置文件的完整代码如下:

参考:github

package com.yunfei.rpc.utils;

import cn.hutool.core.io.resource.NoResourceException;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.setting.dialect.Props;
import cn.hutool.setting.yaml.YamlUtil;

import java.util.Map;

import lombok.extern.slf4j.Slf4j;

/**
* 配置工具类
* 加载配置文件规则:
* <p>conf/application.properties >
* application.properties >
* conf/application.yaml >
* application.yaml >
* conf/application.yml >
* application.yml</p>
*/
@Slf4j
public class ConfigUtils {

private static final String BASE_PATH_DIR = "conf/";
private static final String BASE_CONF_FILE_NAME = "application";
private static final String PROPERTIES_FILE_EXT = ".properties";
private static final String YAML_FILE_EXT = ".yaml";
private static final String YML_FILE_EXT = ".yml";
private static final String ENV_SPLIT = "-";

/**
* 加载配置
*
* @param clazz clazz
* @param prefix properties common prefix
* @param <T> T
* @return props
*/
public static <T> T loadConfig(Class<T> clazz, String prefix) {
return loadConfig(clazz, prefix, "");
}

/**
* 加载配置
* <p>
* 优先加载 properties, 找不到再加载 yaml / yml
*
* @param clazz clazz
* @param prefix properties common prefix
* @param env environment
* @param <T> T
* @return props
*/
public static <T> T loadConfig(Class<T> clazz, String prefix, String env) {
T props;
return (props = loadProperties(clazz, prefix, env)) != null ? props : loadYaml(clazz, prefix, env);
}

/**
* 加载 properties 配置 application-{env}.properties
* <p>
* 优先加载 conf/conf.properties, 找不到再加载 conf.properties
*
* @param clazz clazz
* @param prefix properties common prefix
* @param env environment
* @param <T> T
* @return props
*/
public static <T> T loadProperties(Class<T> clazz, String prefix, String env) {
try {
return doLoadProperties(clazz, BASE_PATH_DIR + BASE_CONF_FILE_NAME, prefix, env);
} catch (NoResourceException e) {
log.warn(
"Not exists properties conf file in [{}], will load properties file from classpath",
BASE_PATH_DIR);
}
try {
return doLoadProperties(clazz, BASE_CONF_FILE_NAME, prefix, env);
} catch (NoResourceException e) {
log.warn("Not exists properties conf file, will load yaml/yml file from classpath");
}
return null;
}

/**
* 加载 yaml 配置 application-{env}.yaml / application-{env}.yml
* <p>
* 优先加载 conf/conf.yaml, 找不到再加载 conf.yaml,其次加载 conf/conf.yml, 找不到再加载 conf.yml
*
* @param clazz clazz
* @param prefix properties common prefix
* @param env environment
* @param <T> T
* @return props
*/
public static <T> T loadYaml(Class<T> clazz, String prefix, String env) {
// 读取 yaml 文件,优先读取 conf/application-{env}.yaml
try {
return doLoadYaml(clazz, BASE_PATH_DIR + BASE_CONF_FILE_NAME, prefix, env,
YAML_FILE_EXT);
} catch (NoResourceException e) {
log.warn("Not exists yaml conf file in [{}], will load yaml file from classpath",
BASE_PATH_DIR);
}
// 加载 application-{env}.yaml 文件
try {
return doLoadYaml(clazz, BASE_CONF_FILE_NAME, prefix, env,
YAML_FILE_EXT);
} catch (NoResourceException e) {
log.warn("Not exists yaml conf file in [{}], will load yml file", BASE_PATH_DIR);
}
// 读取 yml 文件,优先读取 conf/application-{env}.yml
try {
return doLoadYaml(clazz, BASE_PATH_DIR + BASE_CONF_FILE_NAME, prefix, env,
YML_FILE_EXT);
} catch (NoResourceException e) {
log.warn("Not exists yml conf file in [{}], will load yml file from classpath",
BASE_PATH_DIR);
}
// 加载 application-{env}.yml 文件
try {
return doLoadYaml(clazz, BASE_CONF_FILE_NAME, prefix, env,
YML_FILE_EXT);
} catch (NoResourceException e) {
log.error("no conf file!");
throw e;
}
}

/**
* 加载 properties 配置 application-{env}.properties
*
* @param clazz clazz
* @param base base path
* @param prefix properties common prefix
* @param env environment
* @param <T> T
* @return props
*/
public static <T> T doLoadProperties(Class<T> clazz, String base, String prefix, String env)
throws NoResourceException {
String confFilePath = buildConfigFilePath(base, env, PROPERTIES_FILE_EXT);
Props props = new Props(confFilePath);
return props.toBean(clazz, prefix);
}

/**
* 加载 yaml 配置 application-{ev}.yaml / application-{env}.yml
*
* @param clazz clazz
* @param base base path
* @param prefix properties common prefix
* @param env environment
* @param ext file extension
* @param <T> T
* @return props
*/
public static <T> T doLoadYaml(Class<T> clazz, String base, String prefix, String env,
String ext) throws NoResourceException {
String confFilePath = buildConfigFilePath(base, env, ext);
Map<String, Object> props = YamlUtil.loadByPath(confFilePath);
JSONObject rpcConfigProps = JSONUtil.parseObj(props).getJSONObject(prefix);
return JSONUtil.toBean(rpcConfigProps, clazz);
}

/**
* 构建配置文件路径
*
* @param base base path
* @param env environment
* @param ext file extension
* @return config file path
*/
private static String buildConfigFilePath(String base, String env, String ext) {
StringBuilder configFileBuilder = new StringBuilder(base);
if (StrUtil.isNotBlank(env)) {
configFileBuilder.append(ENV_SPLIT).append(env);
}
configFileBuilder.append(ext);
return configFileBuilder.toString();
}
}

测试:

@Test
void loadYaml() {
RpcConfig rpcConfig = ConfigUtils.loadYaml(RpcConfig.class, "rpc", "");
System.out.println(rpcConfig);
}

运行结果:

image-20240502153844007