全局配置加载
在简易版本实现中采用硬编码方式将端口信息写死,但在真实案例中需要维护注册中心的信息,序列化方式等,项目中应当允许编写配置信息来自定义配置
/**
*RPC 框架的配置加载
*/
@Data
public class RpcConfig {
/**
* 名称
*/
private String name = "sfy";
/**
* 版本号
*/
private String version = "1.0";
/**
* 服务器主机名
*/
private String serverHost = "localhost";
/**
* 服务器端口号
*/
private Integer serverPort = 9999;
}
新建工具类:读取相关的配置文件,简化调用
/**
* 配置工具类
*/
public class ConfigUtils {
/**
* 加载配置对象
* @param tClass
* @param prefix
* @return
* @param <T>
*/
public static <T> T loadConfig(Class<T> tClass, String prefix) {
return loadConfig(tClass,prefix,"");
}
/**
* 加载配置对象,支持多环境切换
* @param tClass
* @param prefix
* @param environment
* @return
* @param <T>
*/
public static <T> T loadConfig(Class<T> tClass, String prefix,String environment) {
StringBuilder configFileBuilder = new StringBuilder("application");
if (StrUtil.isBlank(configFileBuilder)){
configFileBuilder.append("-").append(environment);
}
configFileBuilder.append(".properties");
Props props = new Props(configFileBuilder.toString());
return props.toBean(tClass,prefix);
}
}
维护全局配置对象
在框架启动时,从配置文件中读取并创建对象,不用每次加载都重新读取配置,使用单例模式来完成创建,在RpcApplication中实现
/**
* 启动类,存放了全局用到的变量,
*/
@Slf4j
public class RpcApplication {
/**
* 加载配置类
*/
private static volatile RpcConfig rpcConfig;
/**
* 框架初始化,传入自定义配置
* @param newRpcConfig
*/
public static void init(RpcConfig newRpcConfig) {
rpcConfig = newRpcConfig;
log.info("读取到配置信息为:rpc init,config:{}", rpcConfig.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;
}
}
接口Mck测试
访问真实远程服务可能会出现不可控影响,例如网络延迟,服务不稳定的情况,这时需要mock来模拟远程服务的行为
序列化器和SPI机制
自己实现一个序列化器,在resources目录下面创建一个META-INF/services的目录,并在其中写入完整类路径,通过序列化名称->序列化实际对象的映射,根据不同配置实现不同的序列化
jdk=com.sfy.core.example.serializer.JdkSerializer
hessian=com.sfy.core.example.serializer.HessianSerializer
kryo=com.sfy.core.example.serializer.KryoSerializer
动态实现所有的序列化器
public class HessianSerializer implements Serializer {
@Override
public <T> byte[] serialize(T object) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.writeObject(object);
return bos.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> tClass) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
HessianInput hi = new HessianInput(bis);
return (T) hi.readObject(tClass);
}
}
public class KryoSerializer implements Serializer {
/**
* kryo 线程不安全,使用 ThreadLocal 保证每个线程只有一个 Kryo
*/
private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
// 设置动态动态序列化和反序列化类,不提前注册所有类(可能有安全问题)
kryo.setRegistrationRequired(false);
return kryo;
});
@Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
KRYO_THREAD_LOCAL.get().writeObject(output, obj);
output.close();
return byteArrayOutputStream.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> classType) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream);
T result = KRYO_THREAD_LOCAL.get().readObject(input, classType);
input.close();
return result;
}
}
定义一个序列化名称的常量
public interface SerializerKeys {
String JDK = "jdk";
String KRYO = "kryo";
String HESSIAN = "hessian";
}
利用工厂+单例模式简化创建的操作,并且通过SPILoader来加载
/**
* 存储已经加载过的类,接口名--(key--实现类)
*/
private static Map<String,Map<String,Class<?>>> loaderMap = new ConcurrentHashMap<>();
重构序列化工厂
public class RegistryFactory {
static {
SpiLoader.load(Registry.class);
}
/**
* 获取实例
* @return
*/
public static Registry getInstance(String key){
return SpiLoader.getInstance(Registry.class,key);
}
}
注册中心
采用ETCD来实现:
Lease(租约):对键值进行TTL超时设置,当租约过期后,相应的key会被删除
Watch(监听):监听特定key的变化
- 数据分布式存储:注册信息的存储,读取和共享
- 服务注册
- 服务发现
- 心跳检测
- 服务注销
- 优化点:注册中心本身的容错,服务消费者缓存等
设计思路
- key的设计
- value的设计
- key的过期策略
注册中心开发
@Data
public class ServiceMetaInfo {
/**
* 服务名称
*/
private String serviceName;
/**
* 服务版本号
*/
private String serviceVersion = "1.0";
/**
* 服务域名
*/
private String serviceHost;
/**
* 服务端口号
*/
private Integer servicePort;
}
并且为其设计方法
public String getServiceKey() {
return String.format("%s:%s", serviceName, serviceVersion);
}
/**
* 获取服务注册节点键名
*
* @return
*/
public String getServiceNodeKey() {
return String.format("%s/%s:%s", getServiceKey(), serviceHost, servicePort);
}
注册中心配置
public class RegistryConfig {
/**
* 注册中心类别
*/
private String registry = RegistryKeys.ETCD;
/**
* 注册中心地址
*/
private String address = "http://localhost:2380";
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 超时时间(单位毫秒)
*/
private Long timeout = 100000L;
}
注册中心的接口
遵循可扩展原则,与序列化器一样使用spi机制进行扩展,主要实现创建,销毁,服务发现的功能
@Slf4j
public class EtcdRegistry implements Registry {
private Client client;
private KV kvClient;
/**
* 根节点
*/
private static final String ETCD_ROOT_PATH = "/rpc/";
@Override
public void init(RegistryConfig registryConfig) {
client = Client.builder().endpoints(registryConfig.getAddress())
.connectTimeout(Duration.ofMillis(registryConfig.getTimeout())).build();
kvClient = client.getKVClient();
heartBeat();
}
@Override
public void register(ServiceMetaInfo serviceMetaInfo) throws Exception {
// 创建 Lease 和 KV 客户端
Lease leaseClient = client.getLeaseClient();
// 创建一个 30 秒的租约
long leaseId = leaseClient.grant(30).get().getID();
// 设置要存储的键值对
String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey();
ByteSequence key = ByteSequence.from(registerKey, StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from(JSONUtil.toJsonStr(serviceMetaInfo), StandardCharsets.UTF_8);
// 将键值对与租约关联起来,并设置过期时间
PutOption putOption = PutOption.builder().withLeaseId(leaseId).build();
kvClient.put(key, value, putOption).get();
/**
* 添加节点信息到本地缓存
*/
loaclRegistryNodeKeySet.add(registerKey);
}
@Override
public void unRegister(ServiceMetaInfo serviceMetaInfo) {
String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey();
kvClient.delete(ByteSequence.from(registerKey, StandardCharsets.UTF_8));
// 也要从本地缓存移除
loaclRegistryNodeKeySet.remove(registerKey);
}
@Override
public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) {
// 优先从缓存中寻找
List<ServiceMetaInfo> cachedServiceMetaInfoList = registryServiceCache.readCache();
if (!CollUtil.isEmpty(cachedServiceMetaInfoList)) {
return cachedServiceMetaInfoList;
}
// 前缀搜索,结尾一定要加 '/'
String searchPrefix = ETCD_ROOT_PATH + serviceKey + "/";
try {
// 前缀查询
GetOption getOption = GetOption.builder().isPrefix(true).build();
List<KeyValue> keyValues = kvClient.get(
ByteSequence.from(searchPrefix, StandardCharsets.UTF_8),
getOption)
.get()
.getKvs();
// 解析服务信息
List<ServiceMetaInfo> serviceMetaInfoList = keyValues.stream()
.map(keyValue -> {
String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
return JSONUtil.toBean(value, ServiceMetaInfo.class);
})
.collect(Collectors.toList());
// 写入缓存操作
registryServiceCache.writeCache(serviceMetaInfoList);
return serviceMetaInfoList;
} catch (Exception e) {
throw new RuntimeException("获取服务列表失败", e);
}
}
@Override
public void destroy() {
System.out.println("当前节点下线");
// 释放资源
if (kvClient != null) {
kvClient.close();
}
if (client != null) {
client.close();
}
}
}