基于redis實現(xiàn)rpc服務注冊

前言
昨天我們手寫了一個簡單到不能再簡單的rpc服務,對rpc服務有了一個基本的認知,但昨天的實現(xiàn)太過簡單,甚至都算不上rpc,因為rpc服務的核心是動態(tài)代理,但是今天我想先實現(xiàn)rpc的注冊,今天的服務注冊我沒有用zk,而是redis,用redis的目的就是讓各位小伙伴都能真正明白,任何組件的選用都不是必須的,而是一種更優(yōu)的選擇。
服務注冊
首先,我們要定義以下幾個注解,這些注解的作用就是輔助我們完成服務的注冊
定義注解
第一個注解和我們syske-boot中的注解作用一致,主要是為了掃描類
/**
* rpc掃描注解
*
* @author sysker
* @version 1.0
* @date 2021-06-16 23:15
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcComponentScan {
String[] value();
}
這個注解是標記我們的服務提供者,方便我們針對服務提供者進行注冊操作
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcProvider {
}
然后就是服務消費者,和服務提供者的注解類似,就是為了標記消費者
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcCustomer {
}
最后一個注解是加在屬性上的,主要是為了方便后期實現(xiàn)動態(tài)代理
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcClient {
}
包掃描
我們這里直接就把之前寫的包掃描器直接用起來了,這里會根據(jù)RpcComponentScan注解指定的包名進行掃描
public class ClassScanner {
private static final Logger logger = LoggerFactory.getLogger(ClassScanner.class);
private static Set<Class> classSet = Sets.newHashSet();
private ClassScanner() {
}
public static Set<Class> getClassSet() {
return classSet;
}
/**
* 類加載器初始化
*
* @throws IOException
* @throws ClassNotFoundException
*/
public static void init(Class aClass) {
try {
// 掃描包
componentScanInit(aClass);
} catch (Exception e) {
logger.error("ClassScanner init error: ", e);
}
}
/**
* 掃描指定的包路徑,如果無該路徑,則默認掃描服務器核心入口所在路徑
*
* @param aClass
* @throws IOException
* @throws ClassNotFoundException
*/
private static void componentScanInit(Class aClass) throws IOException, ClassNotFoundException {
logger.info("componentScanInit start init……");
logger.info("componentScanInit aClass: {}", aClass);
Annotation annotation = aClass.getAnnotation(RpcComponentScan.class);
if (Objects.isNull(annotation)) {
Package aPackage = aClass.getPackage();
scanPackage(aPackage.toString(), classSet);
} else {
String[] value = ((RpcComponentScan) annotation).value();
for (String s : value) {
scanPackage(s, classSet);
}
}
logger.info("componentScanInit end, classSet = {}", classSet);
}
/**
* 掃描指定包名下所有類,并生成classSet
*
* @param packageName
* @param classSet
* @throws IOException
* @throws ClassNotFoundException
*/
private static void scanPackage(String packageName, Set<Class> classSet)
throws IOException, ClassNotFoundException {
logger.info("start to scanPackage, packageName = {}", packageName);
Enumeration<URL> classes = ClassLoader.getSystemResources(packageName.replace('.', '/'));
while (classes.hasMoreElements()) {
URL url = classes.nextElement();
File packagePath = new File(url.getPath());
if (packagePath.isDirectory()) {
File[] files = packagePath.listFiles();
for (File file : files) {
String fileName = file.getName();
if (file.isDirectory()) {
String newPackageName = String.format("%s.%s", packageName, fileName);
scanPackage(newPackageName, classSet);
} else {
String className = fileName.substring(0, fileName.lastIndexOf('.'));
String fullClassName = String.format("%s.%s", packageName, className);
classSet.add(Class.forName(fullClassName));
}
}
} else {
String className = url.getPath().substring(0, url.getPath().lastIndexOf('.'));
String fullClassName = String.format("%s.%s", packageName, className);
classSet.add(Class.forName(fullClassName));
}
}
}
}
目的就是掃描我們的服務提供者,方便注冊服務的時候使用。
服務提供者注冊
首先我們要先通過RpcProvider注解拿到我們的服務提供者,然后組裝我們的的注冊信息。
private static void initServiceProvider() {
Set<Class> classSet = ClassScanner.getClassSet();
classSet.forEach(c -> {
Annotation annotation = c.getAnnotation(RpcProvider.class);
if (Objects.nonNull(annotation)) {
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
Class<?>[] parameterTypes = method.getParameterTypes();
String methodName = method.getName();
try {
Object newInstance = c.newInstance();
RpcRegisterEntity rpcRegisterEntity = new RpcRegisterEntity(c.getName(), methodName, parameterTypes, newInstance);
Class[] interfaces = c.getInterfaces();
String interfaceName = interfaces[0].getName();
RedisUtil.record2Cache(String.format(PROVIDER_KEY, interfaceName), JSON.toJSONString(rpcRegisterEntity));
System.out.println(JSON.toJSONString(rpcRegisterEntity));
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
});
}
注冊信息注冊完成后,我們將注冊信息寫入redis。
服務消費者注冊
消費者注冊也是類似的方法
private static void initServiceCustomer() {
final String CUSTOMER_KEY = "%s:customer";
final String PROVIDER_KEY = "%s:provider";
Set<Class> classSet = ClassScanner.getClassSet();
classSet.forEach(c -> {
Field[] declaredFields = c.getDeclaredFields();
for (Field field : declaredFields) {
try {
RpcClient annotation = field.getAnnotation(RpcClient.class);
if (Objects.nonNull(annotation)) {
Class<?> fieldType = field.getType();
String name = fieldType.getName();
RedisUtil.record2Cache(String.format(CUSTOMER_KEY, name), c.getName());
// String serviceObject = RedisUtil.getObject(String.format(PROVIDER_KEY, name));
// RpcRegisterEntity rpcRegisterEntity = JSON.parseObject(serviceObject, RpcRegisterEntity.class);
// field.set(c.newInstance(), rpcRegisterEntity.getNewInstance());
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
});
}
本來打算在消費者注冊完直接給接口賦值的(注釋部分),但是在實際操作的時候,發(fā)現(xiàn)這種方式行不通,因為服務提供者和消費者是不同的應用,序列化之后的服務提供者的實例(rpcRegisterEntity.getNewInstance())是沒法強轉(zhuǎn)的,控制臺一直會報類型不匹配的錯誤:

而且,后來我想了下,如果這種方式真的實現(xiàn)了,那就沒socket什么事了,還能叫rpc嗎?所以要想實現(xiàn),真正的動態(tài)調(diào)用,還是要通過動態(tài)代理。
這么說來,昨天實現(xiàn)的也不能叫rpc,因為沒有實現(xiàn)動態(tài)代理。
測試
分別運行服務提供者和消費者,然后我們?nèi)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 100, 65);">redis看下,服務是否已經(jīng)注冊上,如果看到如下所示,表面服務已經(jīng)成功注冊:

同時,我發(fā)現(xiàn)服務提供者的實例信息是空的,這又進一步表明,這種直接反射賦值方式行不通,只有動態(tài)代理才能拯救我們的rpc服務。

總結(jié)
不得不說,相比于zk,redis確實不適合做服務注冊,畢竟zk的樹形結(jié)構看起來就很友好,但是我暫時不考慮換成zk,等動態(tài)代理實現(xiàn)了再說。
另外,在實際測試中,我發(fā)現(xiàn)除了classFUllName之外,其他的參數(shù)都是冗余的,但是像服務的地址、端口等比較重要的信息又沒有,所以后面要把服務注冊的entity優(yōu)化下,暫時就先這樣。
明天,我明天打算分享動態(tài)代理的實現(xiàn)過程,這一塊實現(xiàn)了,rpc框架就成了,具體的明天再說,好了,今天就到這里吧!
完整項目開源地址如下,感興趣的小伙伴可以去看下,后續(xù)我們會繼續(xù)實現(xiàn)相關功能,比如整合注冊中心、實現(xiàn)動態(tài)代理等待:
https://github.com/Syske/syske-rpc-server
- END -