一. 全局配置加载
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:
用于保存配置信息。
可以给属性指定一些默认值,
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:
用于读取配置文件并返回配置对象,可以简化调用。配置类应当尽量通用,不和业务强绑定。
之后调用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 <T>
* @return
*/
public static <T> T loadConfig(Class<T> tClass,String perfix){
return loadConfig(tClass,perfix,"");
}
/**
* 加载配置对象,支持区分环境
*
* @param tClass
* @param perfix
* @param environment
* @param <T>
* @return
*/
public static <T> T loadConfig(Class<T> 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。
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项目的启动入口,并维护项目全局用到的变量。
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,
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);
}
}
读取结果为,
已成功读到配置文件中的内容。
(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()));
}
}
启动结果为,
已成功在8080端口启动。
至此,扩展功能,全局配置加载完成,后续可能会根据新增的功能逐步修改全局配置信息。