1. 程式人生 > 其它 >基於redis實現rpc服務註冊

基於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服務。

總結

不得不說,相比於zkredis確實不適合做服務註冊,畢竟zk的樹形結構看起來就很友好,但是我暫時不考慮換成zk,等動態代理實現了再說。

另外,在實際測試中,我發現除了classFUllName之外,其他的引數都是冗餘的,但是像服務的地址、埠等比較重要的資訊又沒有,所以後面要把服務註冊的entity優化下,暫時就先這樣。

明天,我明天打算分享動態代理的實現過程,這一塊實現了,rpc框架就成了,具體的明天再說,好了,今天就到這裡吧!

完整專案開源地址如下,感興趣的小夥伴可以去看下,後續我們會繼續實現相關功能,比如整合註冊中心、實現動態代理等待:

https://github.com/Syske/syske-rpc-server