1. 从多态说起

面向对象有三大特征:继承、封装、多态,我们先来说说多态。

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

先来看传统的方法调用,在Java 中 All in object. 第一步是 new 一个对象,然后调用这个对象的成员方法。

实例1:

1
2
HelloService service = new HelloService();
service.sayHello();

Java 中,接口是最能体现多态的了,再来看一下多态的例子,我们把 HelloService 定义为接口,有两个实现类,我们在编码过程中向上转型用 HelloService 接口声明变量,而这两个具体实现类必定拥有 sayHello() 这一行为,因此可以直接用 HelloService 接口的实例进行方法调用,而不必关心他的具体实现:

实例2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface HelloService{
	void sayHello();
}
public class BobHelloServiceImpl implements HelloService {
	@Override
	public void sayHello(){
		System.out.println("Hello Bob!");
	}
}
public class JackHelloServiceImpl implements HelloService {
	@Override
	public void sayHello(){
		System.out.println("Hello Jack!");
	}
}

@Test
public void test(){
	HelloService service = null
	service  = new BobHelloServiceImpl();
	service.sayHello();//Hello Bob!

	service = new JackHelloServiceImpl();
	service.sayHello();//Hello Jack!
}

接口的实现类拥有该接口的所有行为(方法),利用接口可将声明与实现充分解耦,至于 Spring 就更进一步了,直接在 ApplicationContext.xml 中定义 Bean 指定 HelloService 的实现,做到了对代码的零侵入。这也是面向对象6个原则之一:面向接口编程而不是面向实现编程

2. 自己动手写RPC

有了面向接口编程的基本思路接下来对理解远程调用就简单多了,远程方法调用分这么几个角色:

  1. 接口API:我们要定义的通信接口。
  2. 服务提供者:提供接口API的具体实现。
  3. 服务消费者:接口调用方,也就是作为客户端的角色。

来改造一下 1. 中的例子,我们分三个子项目,分别是 rpc-provider、rpc-consumer、rpc-api 。rpc-provider 和 rpc-consumer 都依赖于 rpc-api 。三者的依赖关系如下图:

graph LR;
  rpc-provider-->rpc-api
  rpc-consumer-->rpc-api

实例3: rpc-api 的代码:

1
2
3
public interface HelloService{
	String sayHello(String name);
}

rpc-provider,服务提供者:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class HelloServiceImpl implements HelloService {
	@Override
	public String sayHello(String name){
		return "Hello " + name;
	}
}

public static void main(String[] args){
	String url = "http://localhost:8080/rpc/";
	RpcServer server = new RpcServer(url,HelloService.class,HelloServiceImpl.class);
	server.start();
}

实例2中 @Test 所在的方法就是 consumer ,远程方法调用跟本地方法调用的区别就是 consumer 获取 HelloService 接口的具体实现的方式变了,假设我们的远程调用框架封装为一个 InvokeProxy 类,使用代理模式来实现 RPC,这里只提供伪代码说明原理,我隐藏了具体实现,实际上很多第三方框架已经为我们做好了这一点。

rpc-consumer,服务消费端:

1
2
3
4
5
6
@Test
public void test(){
	InvokeProxy invokeProxy = new InvokeProxy("http://localhost:8080/rpc/");
	String result = (String)InvokeProxy.invoke(HelloService.class,"sayHello",new Object[]{"xiaoming"});
	System.out.println(result);//Hello xiaoming
}

InvokeProxy 类的 invoke 方法接收三个参数,一为接口所在的 class ,二为要调用的方法名字符串,三为方法所需接收的参数。

来探索一下整个流程发生了什么:

  1. 启动 rpc-provider 项目。RpcServer 作为远程调用的服务发布框架以 HTTP 协议将 HelloServiceImpl 的所有方法发布到 http://localhost:8080/rpc/ 这个跟路径上,如 sayHello 方法的访问路径就为 http://localhost:8080/rpc/sayHello
  2. rpc-consumer 启动后根据指定的接口和方法名参数等将序列化好的请求报文发送给 rpc-provider 。
  3. rpc-provider 收到请求后将信息反序列化为 Java 对象在 rpc-provider 端进行本地方法调用产生返回值,将返回值响应给 rpc-consumer 的请求。

这个过程是一个简单的基于 HTTP 的 RPC 调用服务,事实上,这也是时下流行的 RPC 框架 Hessian 的原理,Hessian 是一个基于 HTTP 的高性能 RPC 框架。当然,你也可以实现一个基于 TCP 的 RPC 框架,原理都是类似的。

如果你关注微服务或者远程调用,那么对 Dubbo、RMI、REST 这几个名词应该不会陌生,接下来的文章我们将继续探索。