在上一篇我们通过 BIO 实现了一个简易的 RPC 框架,但是,它还有很大的优化空间,本篇我们就对它进行改造 --把所有 bean 都交给 Spring 去管理。
我们先来看一下 version2 的框架结构:
注:如果相对于 version1 没有改变的类,我会在标题后标注上“未变”,看过 version1 的同学可以直接看变化的地方。
1.RpcRequest(未变)
RpcRequest 封装了消费者要调用方法的具体信息,是我们的自定义协议,或者说是消息格式。
涉及两个过程:
- Consumer 编码:RpcRequest -> 数据流(二进制) => 告诉 Provider 要执行哪个方法
- Provider 解码:数据流(二进制)-> RpcRequest => 获取 Consumer 要调用哪个方法
// 注:只有实现了序列化接口,才能实现远程传输
public class RpcRequest implements Serializable {
private String className; // 类(接口/服务)
private String methodName; // 方法
private Object[] parameters; // 参数
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}
2.RpcService(新增)*
对于服务 Bean,我们不能像普通 bean 一样直接 @Compent,因为它的注解里面还应该包括更多信息,比如负载均衡策略,版本信息等,所以这里为所有的服务 bean 新定义一个注解。
@Target(ElementType.TYPE) // 用在类/接口
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Component // 有该注解的类,会被Spring实例化,然后放入IOC容器
public @interface RpcService {
// 记录要发布服务的接口
Class<?> value();
}
注:接口信息其实可以在类上直接获取,这里只是为了模拟通过注解传递信息
通过注解标识要发布的服务,相比上一个版本,有了可扩展性,后期还可以增加更多属性。
3.RpcServer(修改)*
负责将请求派发给不同线程。
PS:引入 Spring 的意义就在于对 Bean 的管理(生老病死)更灵活,这里就是对服务 Bean 的管理更灵活
在 IOC 容器初始化时
- 通过实现 ApplicationContextAware 扩展点,在对象初始后拿到注解的配置服务接口信息
- 通过实现 InitializingBean 扩展点,在对象初始化后阻塞在这里接收请求
public class RpcServer implements ApplicationContextAware, InitializingBean {
ExecutorService executorServic = Executors.newCachedThreadPool();
// 存放接口名(服务名)与服务Bean 的对应关系
private Map<String, Object> handlerMap = new HashMap<>();
private int port;
public RpcServer(int port) {
this.port = port;
}
@Override
/**
* ApplicationContextAware 接口是 Spring 的一个扩展点
* setApplicationContext() 在 Bean 初始化之后执行,可以获取到所有 Bean
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 通过注解拿到服务Bean
Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(RpcService.class);
if (!serviceBeanMap.isEmpty()) {
for (Object serviceBean : serviceBeanMap.values()) {
RpcService rpcService = serviceBean.getClass().getAnnotation(RpcService.class);
// 拿到服务名(接口名)
String serviceName = rpcService.value().getName();
// 放入容器
handlerMap.put(serviceName, serviceBean);
}
}
}
@Override
/**
* InitializingBean 接口也是 Spring 的一个扩展点
* afterPropertiesSet() 在 Bean 初始化后执行,在 setApplicationContext() 之后
*/
public void afterPropertiesSet() throws Exception {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
// 注:因为当 bean 走到 afterPropertiesSet() 已经完成了初始化,
// 所以,可以 while(true) 让 Provider 一直停在这个阶段
while (true) {
Socket socket = serverSocket.accept();
// 这里是将handlerMap传入给具体的处理器,让处理器在其中拿到具体的服务Bean
// 注:这里就不能像v1直接传入一个固定的service,而是要动态获取
executorServic.execute(new ProcessorHandler(socket, handlerMap));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注:这里并没有处理一个服务有多个实现的情况,对于一个服务多个实现可以参考 dubbo 的 SPI 自适应扩展点和激活扩展点的处理逻辑。
4.ProcessorHandler(修改)
Provider 线程的具体调用逻辑:
- 接收(解码):接收 Client 请求,将二进制流转换成 RpcRequest
- 执行:获取要执行的方法和参数,调用服务实现对象去执行方法。注意,与 version1 的区别就在这里,执行方法时多了一步在 handlerMap 中取出服务 bean。
- 发送(编码):将方法执行结果返回,将基本类型/Java对象转换成 二进制流
PS:由于这里采用的是BIO,并且传输时涉及到对象的编解码,所以
- 可以统一调用 ObjectOutputStream#writeObject() 编码
- 可以统一调用 ObjectInputStream#readObject() 解码,然后再将 Object 转换成包装类/普通Java对象
public class ProcessorHandler implements Runnable {
private Socket socket;
private Map<String, Object> handlerMap;
// 相较于v1这里传入的不是一个具体的实例,而是所有的服务bean
public ProcessorHandler(Socket socket,Map<String, Object> handlerMap) {
this.socket = socket;
this.handlerMap = handlerMap;
}
@Override
public void run() {
try {
// Object~Stream也是包装类,其作用在于将字节流解析成java对象
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
// 解析出具体的请求信息(RPCRequest)
RpcRequest rpcRequest = (RpcRequest)objectInputStream.readObject();
// 反射调用本地服务
Object res = invoke(rpcRequest);
// 将执行结果返回
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(res);
objectOutputStream.flush(); // 切记要flush手动刷新
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 通过反射具体执行provider提供的方法(即消费者要调用的方法)
public Object invoke(RpcRequest request) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
// 首先通过handlerMap拿到具体的服务Bean
Object service = handlerMap.get(request.getClassName());
// 若无相应服务则报错
if (service == null) {
throw new RuntimeException("server not found:" + service);
}
// 根据实参获取形参列表
// 注:获取形参列表后才能确定一个方法
Object[] args = request.getParameters();
Class<?>[] types = new Class[args.length];
for (int i = 0; i < args.length; i++) {
types[i] = args[i].getClass();
}
// 通过全类名拿到具体具体Class对象
Class<?> clazz = Class.forName(request.getClassName());
// 获取 Method
Method method = clazz.getMethod(request.getMethodName(), types);
// 执行方法
// 注:service 这里是单例模式,但是也可以new一个对象后再执行具体方法
Object res = method.invoke(service, args);
return res;
}
}
5.RpcProxyClient(未变)
代理对象,通过 JDK 动态代理生成一个代理对象
PS:这里创建一个代理对象是因为,服务的实现实例在 Provider,但 Consumer 调用服务的具体方法时也需要一个实例,而 Consumer 并没有这个实例。
public class RpcProxyClient {
// 创建代理对象,代理的就是指定服务
// 注:因为 Provider 发布时就是一个端口一个服务,所以这里代理的唯一标识就是 host:port
public <T>T clientProxy(final Class<T> interfaceCls, final String host, final int port) {
return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[]{interfaceCls},
new RemoteInvocationHandler(host, port));
}
}
6.RemoteInvocationHandler(未变)
代理对象的具体逻辑,核心是 invoke 方法,当 Consumer 调用了服务的方法时,就会走到 invoke():
- 构建请求信息 RpcRequest
- 发送(编码):将 RpcRequest 转换成二进制流,发送
- 线程阻塞等待 Provider 处理结果
- 接收(解码):接收 Provider 的处理结果,将二进制流转换成基本类型/Java对象,并返回给上层函数
注:234步的逻辑都是网络 IO 相关,所以后面单独封装了一个 RpcNetTransport 类去实现
public class RemoteInvocationHandler implements InvocationHandler {
private String host;
private int port;
public RemoteInvocationHandler(String host, int port) {
this.host = host;
this.port = port;
}
@Override
// 所有请求都会进入这里
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 构建调用Provider的请求参数
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName(method.getDeclaringClass().getName());
rpcRequest.setMethodName(method.getName());
rpcRequest.setParameters(args);
// 进行远程调用,并返回执行结果
RpcNetTransport netTransport = new RpcNetTransport(host, port);
Object res = netTransport.send(rpcRequest);
return res;
}
}
7.RpcNetTransport(未变)
/**
* 实现网络调用
*/
public class RpcNetTransport {
private String host;
private int port;
public RpcNetTransport(String host, int port) {
this.host = host;
this.port = port;
}
public Object send(RpcRequest request) {
Socket socket = null;
Object result = null;
ObjectInputStream inputStream = null;
ObjectOutputStream outputStream = null;
try {
// 将调用的服务的具体信息通过网络写出
socket = new Socket(host, port);
outputStream = new ObjectOutputStream(socket.getOutputStream());
// writeObject实际上是一种序列化
outputStream.writeObject(request);
outputStream.flush();
// 阻塞等待Provider的返回结果
inputStream = new ObjectInputStream(socket.getInputStream());
result = inputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
}
8.ProviderConfig(新增)*
JavaConfig
- 负责注册 RpcServer到 IOC 容器
- 负责扫描指定包下的所有 @Compent 注解(@RpcService)
@Configuration
@ComponentScan(basePackages = "com.xupt.yzh")
public class ProviderConfig {
@Bean(name = "myRpcServer")
public RpcServer rpcServer() {
return new RpcServer(8080);
}
}
注:这里是直接写死了要扫描的包,也可以再优化为通过配置。
9.ConsumerConfig(新增)
JavaConfig,负责注册 RpcProxyClient 到 IOC 容器。那么用户在获取代理时就直接从 IOC 容器获取了,不用再 new 了。
@Configuration
public class ConsumerConfig {
@Bean(name = "rpcProxyClient")
public RpcProxyClient proxyClient() {
return new RpcProxyClient();
}
}
结果测试
api
public interface TestService {
String test(String name);
}
Provider
服务实现类(发布服务)
@RpcService(TestService.class)
public class TestServiceImpl implements TestService {
@Override
public String test(String name) {
System.out.println("new requst coming..." + name);
Random random = new Random();
String json = "{\"name\":" + "\"" + name + "\"" + ", \"age\":" + random.nextInt(40) + "}";
return json;
}
}
启动 Provider:
public class Provider {
public static void main(String[] args) {
// 启动 IOC 容器
new AnnotationConfigApplicationContext(ProviderConfig.class).start();
}
}
Consumer
public class Consumer {
public static void main(String[] args) {
// 与v1相比,区别是通过IOC容器获取代理Bean
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfig.class);
RpcProxyClient rpcProxyClient = context.getBean(RpcProxyClient.class);
TestService service = rpcProxyClient.clientProxy(TestService.class, "localhost", 8080);
String json = service.test("张三");
System.out.println(json);
}
}
结果如下:
到此,将 RPC 框架中所有的 bean 交给 Spring 管理的升级优化就完成了!
完整代码我放到 GitHub 上了,有需要参考的同学点击这里跳转…
既然这么重要个改造都能完成,咱么再趁着首热再扩展一个功能 – 添加版本控制。
扩展:如何添加版本控制?
具体步骤我就不一一列代码出来了,同样是修改代码的几个地方,我都通过 todo 按顺序标示出来了:
这部分代码我也放到 GitHub 上,感性趣或者看不太明白的同学可以再看看代码,调试调试,点击这里跳转…