手写RPC框架(四)使用Redis进行服务注册
在使用Redis之前,我们发现项目代码结构设计不合理,对于消费者,服务者应该作为不同的模块,在测试和运行时可以分别运行对应的模块,在此我们将项目拆分为四个模块
- Consumer 消费者,即客户端
- Provider 服务提供者
- Service 项目中使用的服务,提供服务接口,服务实现类
- Util 项目中使用到的工具类
项目结构参考该项目:syske-rpc-server结构图如下:
- windows安装redis
redis安装redis可视化工具 - Redis管理工具类
public class RedisUtil {
private static Jedis jedis = new Jedis("127.0.0.1", 6379);
public static void record2Cache(String key, String value) {
jedis.set(key, value);
}
public static String getObject(String key) {
return jedis.get(key);
}
}
key值为服务名,value为序列化对象
- 注解类实现
通过注解可以实现服务在项目启动时自动注册
- 扫描类注解
@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 {
}
包扫描器:
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));
}
}
}
}
在启动项目时候,通过调用
Set<Class> classSet = ClassScanner.getClassSet();
可以得到对应的类集合
- Redis使用
Redis用作服务注册,key值存储服务名,value存储序列化信息(实现类信息)
- 服务注册
static final String PROVIDER_KEY = "%s:provider";
RedisUtil.record2Cache(String.format(PROVIDER_KEY, interfaceName), JSON.toJSONString(rpcRegisterEntity));
- 服务获取
String serviceObject = RedisUtil.getObject(String.format(PROVIDER_KEY, interfaceName));
一个简单的RPC框架结构如上图,主要的执行步骤如下:
- 启动服务端,服务发现,项目会扫描具有@RpcProvider注解的类,并将<服务名,服务实现类信息存储值Redis中。
- 启动消费端,项目通过反射获取代理类,将调用类,方法、参数等序列化,通过socket发送到服务端
- 服务端获取序列化对象,并将其反序列化,得到interfaceName(接口名,即服务名),参数列表,参数类型,并根据服务名获取实现类,通过反射执行调用的方法。
- 服务端将指定结果通过socket返回给消费端