基於redis實現rpc服務註冊
前言
昨天我們手寫了一個簡單到不能再簡單的rpc
服務,對rpc
服務有了一個基本的認知,但昨天的實現太過簡單,甚至都算不上rpc
,因為rpc
服務的核心是動態代理,但是今天我想先實現rpc
的註冊,今天的服務註冊我沒有用zk
,而是redis
,用redis
的目的就是讓各位小夥伴都能真正明白,任何元件的選用都不是必須的,而是一種更優的選擇。
服務註冊
首先,我們要定義以下幾個註解,這些註解的作用就是輔助我們完成服務的註冊
定義註解
第一個註解和我們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 {
}
最後一個註解是加在屬性上的,主要是為了方便後期實現動態代理
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface RpcClient { }
包掃描
我們這裡直接就把之前寫的包掃描器直接用起來了,這裡會根據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();
}
}
});
}
本來打算在消費者註冊完直接給介面賦值的(註釋部分),但是在實際操作的時候,發現這種方式行不通,因為服務提供者和消費者是不同的應用,序列化之後的服務提供者的例項(rpcRegisterEntity.getNewInstance()
)是沒法強轉的,控制檯一直會報型別不匹配的錯誤:
而且,後來我想了下,如果這種方式真的實現了,那就沒socket
什麼事了,還能叫rpc
嗎?所以要想實現,真正的動態呼叫,還是要通過動態代理。
這麼說來,昨天實現的也不能叫rpc
,因為沒有實現動態代理。
測試
分別執行服務提供者和消費者,然後我們去redis
看下,服務是否已經註冊上,如果看到如下所示,表面服務已經成功註冊:
同時,我發現服務提供者的例項資訊是空的,著有進一步表明,這種直接反射賦值方式行不通,只有動態代理才能拯救我們的rpc
服務。
總結
不得不說,相比於zk
,redis
確實不適合做服務註冊,畢竟zk
的樹形結構看起來就很友好,但是我暫時不考慮換成zk
,等動態代理實現了再說。
另外,在實際測試中,我發現除了classFUllName
之外,其他的引數都是冗餘的,但是像服務的地址、埠等比較重要的資訊又沒有,所以後面要把服務註冊的entity
優化下,暫時就先這樣。
明天,我明天打算分享動態代理的實現過程,這一塊實現了,rpc
框架就成了,具體的明天再說,好了,今天就到這裡吧!
完整專案開源地址如下,感興趣的小夥伴可以去看下,後續我們會繼續實現相關功能,比如整合註冊中心、實現動態代理等待:
https://github.com/Syske/syske-rpc-server