原文参考至:
https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/
仅学习使用,如有侵权请联系我,马上删除。
gitee地址
https://gitee.com/zmsnhh/rpc_demo
大纲
- 1、初识RPC
- 2、实现简单RPC
- 2.1、场景模拟
- 2.2、思路分析
- 2.3、代码实现
- 3、细节优化
- 3.1、将框架与spring整合,实现服务调用透明
- 3.2、使用Netty中的提供NIO网络模型
- 3.3、使用Protostuff实现序列化
- 3.4、利用zookeeper实现服务自动注册和发现
学习目标
通过学习,达到以下目的:
- 熟悉java的BIO、NIO的网络编程。
- 会使用线程池
- 熟练使用动态代理
- 会编写自定义注解,并结合spring使用
- 使用netty编写简单的网络通信
- 掌握zookeeper的节点树的基本操作
- 使用zookeeper实现服务自动注册和发现
课程内容较多,可能需要分多个课时讲完。
1、初识RPC
1.1、什么是RPC?
RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。说得通俗一点就是:A计算机提供一个服务,B计算机可以像调用本地服务那样调用A计算机的服务。
通过上面的概念,我们可以知道,实现RPC主要是做到两点:
- 实现远程调用其他计算机的服务
- 要实现远程调用,肯定是通过网络传输数据。A程序提供服务,B程序通过网络将请求参数传递给A,A本地执行后得到结果,再将结果返回给B程序。这里需要关注的有两点:
- 1)采用何种网络通讯协议?
- 现在比较流行的RPC框架,都会采用TCP作为底层传输协议
- 2)数据传输的格式怎样?
- 两个程序进行通讯,必须约定好数据传输格式。就好比两个人聊天,要用同一种语言,否则无法沟通。所以,我们必须定义好请求和响应的格式。另外,数据在网路中传输需要进行序列化,所以还需要约定统一的序列化的方式。
- 1)采用何种网络通讯协议?
- 要实现远程调用,肯定是通过网络传输数据。A程序提供服务,B程序通过网络将请求参数传递给A,A本地执行后得到结果,再将结果返回给B程序。这里需要关注的有两点:
- 像调用本地服务一样调用远程服务
- 如果仅仅是远程调用,还不算是RPC,因为RPC强调的是过程调用,调用的过程对用户而言是应该是透明的,用户不应该关心调用的细节,可以像调用本地服务一样调用远程服务。所以RPC一定要对调用的过程进行封装
1.2、问题:Http和RPC有什么关系
Http协议:超文本传输协议,是一种应用层协议。规定了网络传输的请求格式、响应格式、资源定位和操作的方式等。但是底层采用什么网络传输协议,并没有规定,不过现在都是采用TCP协议作为底层传输协议。说到这里,大家可能觉得,Http与RPC的远程调用非常像,都是按照某种规定好的数据格式进行网络通信,有请求,有响应。没错,在这点来看,两者非常相似,但是还是有一些细微差别。
- RPC并没有规定数据传输格式,这个格式可以任意指定。
- Http中还定义了资源定位的路径,RPC中并不需要
- 最重要的一点:RPC需要满足像调用本地服务一样调用远程服务,也就是对调用过程在API层面进行封装。Http协议没有这样的要求,因此请求、响应等细节需要我们自己去实现。
- 优点:RPC方式更加透明,对用户更方便。Http方式更灵活,没有规定API和语言,跨语言、跨平台
- 缺点:RPC方式需要在API层面进行封装,限制了开发的语言环境。
1.3、流行的RPC框架
事实上,限制的RPC框架,不仅仅是实现透明化的远程调用,更多的侧重点放在了服务治理上。实现诸如:服务自动发现、自动注册、负载均衡、服务治理等功能。例如阿里巴巴的Dubbo就是流行RPC框架的佼佼者。
2、实现简单的RPC
通过前面介绍,我们已经知道,现在主流的RPC框架其实是比较复杂的,除了“远程过程调用"以外,更多的是服务的治理。我们在入门案例中,先侧重于对“远程过程调用"的实现。接下来就来完成一个简单的RPC入门框架
2.1、场景模拟
我们先模拟一个远程调用的场景:计算机A提供服务,计算机B远程调用服务。
2.1.1、服务提供方
现在我们创建一个工程rpc-service,模拟计算机A,提供一个简单的服务:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6rgvetj-1595402326994)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/rpc-service.png)]
服务接口:
public interface HelloService {
String sayHello(String name);
}
服务的具体实现:
public class HelloServiceImpl implements HelloService{
public String sayHello(String name) {
return "hello," + name;
}
}
调用一个本地服务,需要知道是调用哪个类的哪个方法,然后创建对象,调用方法,传递具体参数即可。例如,在计算机A中,我们直接new对象,调用方法即可:
public static void main(String[] args) {
HelloService service = new HelloServiceImpl();
String msg = service.sayHello("Jack");
System.out.println(msg);// hello, Jack
}
2.1.2、服务调用方
现在,我们再创建一个工程rpc-client,模拟计算机B,这里只有HelloService接口,没有具体实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmFOmbtX-1595402327011)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/rpc-client.png)]
服务接口:
public interface HelloService {
String sayHello(String name);
}
我们要在计算机B中远程调用计算机A的HelloService服务,该怎么做?
2.2、思路分析
在计算机B中,我们确切的知道要调用哪个类的哪个方法,而且知道具体的参数。但问题是:在计算机B中,只有接口,并没有方法的实现,无法直接调用。在计算机A中才有方法的具体实现。因此,现在就需要计算机B来调用计算机A中的方法,并且传参。也就是说:计算机B需要遥控指挥A做事情,把要执行的方法及参数告诉A即可。
如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3kWyTz3G-1595402327016)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/remote-call.png)]
整个调用过程分为以下几个步骤:
- 1)计算机B封装请求的参数信息
- 2)计算机B将请求参数信息序列化(网络传输可以接收的形式)
- 3)计算机A接收请求并进行反序列化,得到请求参数
- 4)计算机A解析请求参数,获取服务信息及参数信息
- 5)计算机A调用本地服务,获取结果
- 6)计算机A将执行的结果序列化
- 7)计算机B接收数据,进行反序列化
这里有两个需要大家注意的地方:
- 序列化和反序列化的方式
- 就序列化而言,Java 提供了默认的序列化方式,但在高并发的情况下,这种方式将会带来一些性能上的瓶颈,于是市面上出现了一系列优秀的序列化框架,比如:Protobuf、Kryo、Hessian、Jackson 等,它们可以取代 Java 默认的序列化,从而提供更高效的性能。不过在入门案例中,我们先采用Java的默认序列化方式。
- 网络传输的方式
- 现在主流的RPC框架主要有两种网络传输方式:一种是Http协议,一种是TCP协议。事实上TCP才是底层的传输协议,Http是在TCP基础上进行了封装的应用层协议。大部分情况下,TCP协议的效率会更改。所以我们采用TCP方式传输数据。
- 而TCP方式又有传统的BIO(阻塞IO),性能更好的NIO(非阻塞IO)。当然我们可以选择非常热门的框架Netty来编写代码。不过因为有一定的学习成本,在入门案例中,我们将采用JDK原生的网络编程来实现。
- 请求参数和响应结果封装
- 请求参数中需要包含的数据:
- 服务的接口名
- 接口中的方法名
- 方法的参数类型(以防方法重载)
- 方法的参数值
- 响应结果要包含的数据:
- 响应的状态(请求不一定会成功)
- 异常信息
- 结果数据
- 请求参数中需要包含的数据:
2.3、代码实现
我们通过代码来实现刚才的思路:
传送门:
- 2.3.1、准备工作
- 2.3.2、请求参数RpcRequest
- 2.3.3、响应参数RpcResponse
- 2.3.4、服务提供方RpcServer
- 2.3.5、服务消费方
- 2.3.6、项目结构
- 2.3.7、测试
2.3.1、准备工作
为了便于以后的复用,我们创建一个新的maven工程,编写所有RPC的工具,以后使用时,就可以直接引入坐标即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oce6qXeE-1595402327025)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/rpc-tools.png)]
列出我们需要编写的部分:
-
对外提供的服务(已完成,就是rpc-service中定义的
HelloService
) -
服务提供方(rpc-service,需要通过
ServerSocket
对外提供服务) -
服务消费方(rpc-client,需要通过
Socket
连接rpc-service,实现远程访问)
- 需要注意的是,客户端只有服务接口,并没有实现类,所以我们需要利用动态代理的方式为这个接口生成一个实现类,然后在代理方法中,通过远程连接发起请求到达服务端,获取响应结果并返回。
-
序列化和反序列化:采用JDK默认的序列化
-
请求参数封装
-
响应参数封装
2.3.2、请求参数RpcRequest
注意,因为要使用JDK的序列化,因此该类需要实现Serializable
接口:
/**
* @author: HuYi.Zhang
*/
public class RpcRequest implements Serializable{
private static final long serialVersionUID = 1L;
private String className;
private String methodName;
private Class<?>[] parameterTypes;
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 Class<?>[] getParameterTypes() {
return parameterTypes;
}
public void setParameterTypes(Class<?>[] parameterTypes) {
this.parameterTypes = parameterTypes;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}
2.3.3、响应参数RpcResponse
与RpcRequest一样,这里也需要实现Serializable接口:
另外为了使用方便,我们定义了几个静态方法,用来生成该类实例:
ok(Object data)
表示响应成功,接收要返回的数据error(String error)
表示响应失败,接收错误信息build(int stastus, String error, Object data)
用来自定义返回状态和消息
/**
* @author: HuYi.Zhang
**/
public class RpcResponse implements Serializable{
private static final long serialVersionUID = 2L;
private int status;// 响应状态 0失败,1成功
private String error;// 错误信息
private Object data;// 返回结果数据
public static RpcResponse ok(Object data) {
return build(1, null, data);
}
public static RpcResponse error(String error) {
return build(0, error, null);
}
public static RpcResponse build(int status, String error, Object data) {
RpcResponse r = new RpcResponse();
r.setStatus(status);
r.setError(error);
r.setData(data);
return r;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
2.3.4、服务提供方RpcServer
服务端的实现方式多种多样,可以用BIO、NIO、AIO来实现,为了便于后期扩展,这里先定义一个接口:
/**
* RPC的服务端接口
* @author HuYi.Zhang
*/
public interface RpcServer {
/**
* 启动服务
*/
void start();
/**
* 停止服务
*/
void stop();
}
2.3.4.1、服务端要做的事情
我们思考一下服务端要做的事情:
- 1)启动服务:根据指定端口,启动一个
ServerSocket
,等待客户端连接 - 2)请求处理:接收客户端连接,接收请求(
RpcRequest
)并解析请求,得到要调用的接口信息 - 3)服务发现:根据接口查找接口的具体实现
- 4)本地执行:执行接口中的方法,获取响应结果(
RpcResponse
) - 5)返回结果
需要注意的是:
- 服务发现阶段需要从众多的类中,找到已知接口的实现,比较麻烦。为了方便查找,我们可以在一开始就将所有接口及其实现类关系缓存,实现简单的服务注册。
- 解析
RpcRequest
,返回RpcRespon
的流程,在以后其它的RpcServer
实现类中也会用到,为了复用性,我们可以进行抽取。
2.3.4.2、服务注册器ServiceRegistry
当请求到达,我们就需要根据请求的接口信息,找到对应的实现类。因此,我们最好提前把所有要对外提供的服务,提前记录在一个地方,方便以后寻找。
我们定义一个类ServiceRegistry,在服务启动前,先向其中注册服务。这里我们先手动注册服务,以后再考虑实现自动扫描并注册服务:
/**
* @author: HuYi.Zhang
**/
public class ServiceRegistry{
private static final Logger logger = LoggerFactory.getLogger(ServiceRegistry.class);
private static final Map<String, Object> registeredServices = new HashMap<>();
public static <T> T getService(String className) {
return (T) registeredServices.get(className);
}
public static void registerService(Class<?> interfaceClass, Class<?> implClass) {
try {
registeredServices.put(interfaceClass.getName(), implClass.newInstance());
logger.info("服务注册成功,接口:{},实现{}", interfaceClass.getName(), implClass.getName());
} catch (Exception e) {
e.printStackTrace();
logger.error("服务" + implClass + "注册失败", e);
}
}
}
2.3.4.3、请求处理器RequestHandler
处理请求,获取响应的过程在RpcServer
的各个实现类中都可能会用到,所以我们进行抽取:
/**
* @author: HuYi.Zhang
**/
public class RequestHandler {
private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
public static RpcResponse handleRequest(RpcRequest request) {
try {
// 获取服务
Object service = ServiceRegistry.getService(request.getClassName());
if (service != null) {
Class<?> clazz = service.getClass();
// 获取方法
Method method = clazz.getMethod(request.getMethodName(),
request.getParameterTypes());
// 执行方法
Object result = method.invoke(service, request.getParameters());
// 写回结果
return RpcResponse.ok(result);
} else {
logger.error("请求的服务未找到:{}.{}({})",
request.getClassName(),
request.getMethodName(),
StringUtils.join(request.getParameterTypes(), ", "));
return RpcResponse.error("未知服务!");
}
} catch (Exception e) {
e.printStackTrace();
logger.error("处理请求失败", e);
return RpcResponse.error(e.getMessage());
}
}
}
2.3.4.4、服务端BioRpcServer
入门案例中,我们先通过BIO方式实现RpcServer
类:
/**
* BIO的RPC服务端
*
* @author HuYi.Zhang
*/
public class BioRpcServer implements RpcServer{
private static final Logger logger = LoggerFactory.getLogger(BioRpcServer.class);
// 用来处理请求的连接池
private static final ExecutorService es = Executors.newCachedThreadPool();
private int port = 9000;// 默认端口
private volatile boolean shutdown = false;// 是否停止
/**
* 使用默认端口9000,构建一个BIO的RPC服务端
*/
public BioRpcServer() {
}
/**
* 使用指定端口构建一个BIO的RPC服务端
*
* @param port 服务端端口
*/
public BioRpcServer(int port) {
this.port = port;
}
@Override
public void start() {
try {
// 启动服务
ServerSocket server = new ServerSocket(this.port);
logger.info("服务启动成功,端口:{}", this.port);
while (!this.shutdown) {
// 接收客户端请求
Socket client = server.accept();
es.execute(() -> {
try (
// 使用JDK的序列化流
ObjectInputStream in = new ObjectInputStream(client.getInputStream());
ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream())
) {
// 读取请求参数
RpcRequest request = (RpcRequest) in.readObject();
logger.info("接收请求,{}.{}({})",
request.getClassName(),
request.getMethodName(),
StringUtils.join(request.getParameterTypes(), ", "));
logger.info("请求参数:{}",
StringUtils.join(request.getParameters(), ", "));
// 处理请求
out.writeObject(RequestHandler.handleRequest(request));
} catch (Exception e) {
logger.error("客户端连接异常,客户端{}:{}", client.getInetAddress().toString());
throw new RuntimeException(e);
}
});
}
} catch (IOException e) {
e.printStackTrace();
logger.error("服务启动失败", e);
}
}
@Override
public void stop() {
this.shutdown = true;
logger.info("服务即将停止");
}
}
2.3.5、服务消费方
2.3.5.1、客户端接口RpcClient
同RpcServer
一样,我们先定义一个接口:
客户端做的事情比较单一,发起RpcRequest
请求,获取响应并解析即可。
/**
* RPC客户端
* @author HuYi.Zhang
*/
public interface RpcClient {
/**
* 发起请求,获取响应
* @param request
* @return
*/
RpcResponse sendRequest(RpcRequest request) throws Exception;
}
2.3.5.2、客户端实现BioRpcClient
然后实现一个BIO的客户端:
/**
* RPC客户端的BIO实现
*
* @author HuYi.Zhang
*/
public class BioRpcClient implements RpcClient {
private static final Logger logger = LoggerFactory.getLogger(BioRpcClient.class);
private String host;
private int port;
public BioRpcClient(String host, int port) throws IOException {
this.host = host;
this.port = port;
}
@Override
public RpcResponse sendRequest(RpcRequest request) throws Exception{
try (
Socket socket = new Socket(host, port);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream())
) {
logger.info("建立连接成功:{}:{}", host, port);
// 发起请求
out.writeObject(request);
logger.info("发起请求,目标主机{}:{},服务:{}.{}({})", host, port,
request.getClassName(), request.getMethodName(),
StringUtils.join(request.getParameterTypes(), ","));
// 获取结果
return (RpcResponse) in.readObject();
}
}
}
2.3.5.3、动态代理工厂RpcProxyFactory
刚刚实现的RpcClient
中,我们实现了发起请求,获取响应的功能。那么问题来了:
谁来发起请求?
谁来解析响应?
根据我们前面的分析,客户端(计算机B)只有HelloService
接口,并没有具体的实现。我们需要通过动态代理为HelloService生成实现类。当有人调用HelloService
的sayHello()
方法时,底层可以调用RpcClient
的sendRequset()
功能向服务端(计算机A)发起请求,获取执行结果。
不管接口是什么,生成动态代理的代码和逻辑几乎是一样的。因此我们可以抽取出一个生成代理的工厂:
/**
* 一个动态代理工厂,为接口生成实现了Rpc远程调用的实现类。
* @author: HuYi.Zhang
**/
public class RpcProxyFactory<T> implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(RpcProxyFactory.class);
private Class<T> clazz;
public RpcProxyFactory(Class<T> clazz) {
this.clazz = clazz;
}
public T getProxyObject() {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 处理Object中的方法
if (Object.class == method.getDeclaringClass()) {
String name = method.getName();
if ("equals".equals(name)) {
return proxy == args[0];
} else if ("hashCode".equals(name)) {
return System.identityHashCode(proxy);
} else if ("toString".equals(name)) {
return proxy.getClass().getName() + "@" +
Integer.toHexString(System.identityHashCode(proxy)) +
", with InvocationHandler " + this;
} else {
throw new IllegalStateException(String.valueOf(method));
}
}
// 封装请求参数
RpcRequest request = new RpcRequest();
request.setClassName(clazz.getName());
request.setMethodName(method.getName());
request.setParameters(args);
request.setParameterTypes(method.getParameterTypes());
try {
// 发起网络请求,并接收响应
RpcClient client = new BioRpcClient("127.0.0.1", 9000);
RpcResponse response = client.sendRequest(request);
// 解析并返回
if (response.getStatus() == 1) {
logger.info("调用远程服务成功!");
return response.getData();
}
logger.debug("远程服务调用失败,{}。", response.getError());
return null;
} catch (Exception e) {
logger.error("远程调用异常", e);
throw new RuntimeException(e);
}
}
}
2.3.6、项目结构
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-st7QVlmr-1595402327035)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/rpc-tools-01.png)]
2.3.7、测试
在rpc-service中编写测试类,启动一个服务端:
public class RpcServerTest {
@Test
public void test01() throws InterruptedException {
// 注册服务
ServiceRegistry.registerService(HelloService.class, HelloServiceImpl.class);
// 启动服务
new BioRpcServer(9000).start();
}
}
运行日志:
2018-03-16 16:14:31 INFO BioRpcServer:69 - 注册服务cn.itcast.rpc.service.HelloService,实现类cn.itcast.rpc.service.impl.HelloServiceImpl
2018-03-16 16:14:31 INFO BioRpcServer:78 - 服务启动成功,端口:9000
在rpc-client中启动一个客户端:
public class RpcClientTest {
@Test
public void test01(){
// 通过代理工厂,获取服务
HelloService helloService = new RpcProxyFactory<>(HelloService.class).getProxyObject();
// 调用服务
String result = helloService.sayHello("Jack");
System.out.println(result);
Assert.assertEquals("调用失败", "hello, Jack", result);
}
}
运行后,服务端日志:
2018-03-16 16:14:37 INFO BioRpcServer:113 - 接收请求,cn.itcast.rpc.service.HelloService.sayHello(class java.lang.String)
2018-03-16 16:14:37 INFO BioRpcServer:117 - 请求参数:Jack
客户端日志:
2018-03-18 14:23:39 INFO BioRpcClient:44 - 建立连接成功:127.0.0.1:9000
2018-03-18 14:23:39 INFO BioRpcClient:47 - 发起请求,目标主机127.0.0.1:9000,服务:cn.itcast.rpc.service.HelloService.sayHello(class java.lang.String)
2018-03-18 14:23:39 INFO RpcProxyFactory:61 - 调用远程服务成功!
hello, Jack
到这里为止,一个简单的RPC框架就实现了!
3、优化RPC框架
在刚才实现的RPC框架中,其实隐藏者许多问题需要去解决:
- 服务的注册和发现需要手动完成
- 序列化采用的是JDK的默认方式,效率较低
- 网络模型采用的是BIO,而且是短连接,效率很差
接下来,我们就一一解决这些问题,对框架进行升级
3.1、与spring整合
Spring几乎是现在开发JavaEE的必备框架,特别是在SpringBoot出现以后,其快速搭建项目的功能一直被人们津津乐道。当然Spring的核心功能AOP和依赖注入功能,也非常强大。我们接下来就利用Spring的依赖注入功能,将服务通过注解直接扫描并注册到Spring容器,并且可以通过依赖注入功能实现自动注入。
快速通道:
- 3.1.1、服务的自动注册
- 3.1.2、服务端RpcServer与Spring整合
- 3.1.3、服务端测试
- 3.1.4、客户端实现服务的依赖注入
- 3.1.5、客户端测试
3.1.1、服务的自动注册
在刚才的案例中,我们要在服务端注册一个服务,需要通过下面的方式手动注册:
// 注册服务
ServiceRegistry.registerService(HelloService.class, HelloServiceImpl.class);
这种方式兼职弱爆了。我们回忆一下Spring的功能,在spring中提供了以下一些注解:
@Component
@Service
@Controller
@Reponsitory
如果想要一个Bean加入Spring容器,只需要使用上面任意一个注解即可。我们能不能通过类似的方式来实现将服务自动注册到Spring,并且注册到ServiceRegistry
的功能呢?
先来看看我们要达到的目标:
- 1)将接口的实现类注册到Spring容器
- 2)将接口和实现类信息保存到ServiceRegistry中,方便以后查找
好了,接下来我们分别实现这两个目标
3.1.1.1、将接口的实现类注册到Spring容器
相信大家很快就能想到:我们直接在实现类上加上前面提到过的Spring提供的任意一个注解就可以实现了。
没错,这样确实能达到目的。但是大家思考一下,如果我们使用Spring提供的注解,那么我们将来如何能知道Spring容器中的哪些类是需要注册到ServiceRegistry
的呢?
所以,我们不能使用Spring的注解,这样就产生了新的问题:
- 如果我们不使用Spring的注解,Spring就不会主动把类注册到Spring容器了。
这个问题其实很好解决,大家看一下spring的@Service
或者@Controller
源码就明白了:
/**
* Indicates that an annotated class is a "Controller" (e.g. a web controller).
*
* <p>This annotation serves as a specialization of {@link Component @Component},
* allowing for implementation classes to be autodetected through classpath scanning.
* It is typically used in combination with annotated handler methods based on the
* {@link org.springframework.web.bind.annotation.RequestMapping} annotation.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 2.5
* @see Component
* @see org.springframework.web.bind.annotation.RequestMapping
* @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
String value() default "";
}
我们可以发现,在@Controller
注解上,其实有一个@Component
注解,然后类上的一段注释:
This annotation serves as a specialization of @Component,allowing for implementation classes to be autodetected through classpath scanning.
此类作为@Componet注解的一个特例,运行通过类路径自动扫描获取实现类。
也就是说,一个自定义注解,只要加上了@Component
注解,就会起到跟@Component
一样的作用,被标记的类会被Spring自动扫描,并加入spring容器中。
所以,我们定义一个自定义注解,用来识别需要对外提供的服务:
/**
* 用来标记RPC服务,并且声明其接口
* @author: HuYi.Zhang
**/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Service {
/**
* 接口名称
* @return
*/
Class<?> value();
}
这个注解需要接收value属性,用来指定被标记的类所实现的接口。
接下来,我们就可以在HelloServiceImpl
上使用这个自定义的注解了:
@Service(HelloService.class)
public class HelloServiceImpl implements HelloService{
public String sayHello(String name) {
return "hello, " + name;
}
}
3.1.1.2、将接口和实现类注册到ServiceRegistry
我们已经将HelloServiceImpl
注册到Spring容器了。下一步动作,就是将该实现类及其接口HelloService信息注册到ServiceRegistry
中。
这一步动作,我们也希望由Spring来帮我们完成,怎么做呢?
实现的方式有很多种,我们这里介绍其中一种,使用ApplicationContextAware
接口。
public interface ApplicationContextAware extends Aware {
/**
* Set the ApplicationContext that this object runs in.
* Normally this call will be used to initialize the object.
* <p>Invoked after population of normal bean properties but before an init callback such
* as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
* or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader},
* {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and
* {@link MessageSourceAware}, if applicable.
* @param applicationContext the ApplicationContext object to be used by this object
* @throws ApplicationContextException in case of context initialization errors
* @throws BeansException if thrown by application context methods
* @see org.springframework.beans.factory.BeanInitializationException
*/
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
这个东西是干什么的呢?
我们先看一下Spring初始化Bean的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wgoI2t3U-1595402327041)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/spring-flow.png)]
当Spring对所有Bean进行实例化后,会完成对属性的设置。然后会检查有没有类实现了与Aware相关的接口,并且处理,我们的ApplicationContextAware
就是其中之一。
在这个接口上有这么一段注释:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oxoy53Gx-1595402327043)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/applicationConextAware.png)]
翻译一下:
当一个对象需要访问容器一些bean时,可以实现这个接口,否则就没有必要。 请注意,通过Bean引用进行配置优于为了bean查找目的而实现此接口。
当一个类实现ApplicationContextAware
接口时,spring扫描到以后,就会调用setApplicationContext
方法,将spring容器传递给这个方法中。拿到了容器,我们就可以从中寻找带有我们自定义注解@Service的类了。
我们改造ServiceRegistry,让它实现ApplicationContextAware
接口:
/**
* @author: HuYi.Zhang
**/
public class ServiceRegistry implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(ServiceRegistry.class);
private static final Map<String, Object> registeredServices = new HashMap<>();
public static <T> T getService(String className) {
return (T) registeredServices.get(className);
}
public static void registerService(Class<?> interfaceClass, Class<?> implClass) {
try {
registeredServices.put(interfaceClass.getName(), implClass.newInstance());
logger.info("服务注册成功,接口:{},实现{}", interfaceClass.getName(), implClass.getName());
} catch (Exception e) {
e.printStackTrace();
logger.error("服务" + implClass + "注册失败", e);
}
}
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
Map<String, Object> services = ctx.getBeansWithAnnotation(Service.class);
if (services != null && services.size() > 0) {
for (Object service : services.values()) {
String interfaceName = service.getClass().getAnnotation(Service.class).value().getName();
registeredServices.put(interfaceName, service);
logger.info("加载服务:{}", interfaceName);
}
}
}
}
这样以来,我们就不需要手动注册服务了!
3.1.2、服务端RpcServer与Spring整合
要把RpcServer与Spring整合,就需要Bean能够在初始化完成后启动,我们可以给BioRpcServer添加一个初始化方法init()
。还需要添加一个注解@PostConstructor
,这样Spring在初始化完成后,会自动调用该方法。另外,可以给stop方法添加@PreDestroy
注解,这样Spring会在Bean销毁前调用该方法,将服务停止。
@Override
@PreDestroy
public void stop() {
this.shutdown = true;
logger.info("服务即将停止");
}
@PostConstruct
public void init(){
es.submit(this::start);
}
注意:因为start()
方法是阻塞的,所以不能在init()
方法中直接调用start()
,会导致Spring线程阻塞,所以我们在init中开启线程来异步执行start()
方法;
3.1.3、服务端测试
首先编写配置类,将注册器ServiceRegistry及服务端BioRpcServer注册到Spring:
这里我们采用Java配置方式,千万不要忘了指定扫描包:
/**
* @author: HuYi.Zhang
**/
@Configuration
@ComponentScan(basePackages = "cn.itcast.rpc.service")
public class RpcServerConfig {
/**
* BIO的RPC服务端
* @return
*/
@Bean
public RpcServer rpcServer() {
return new BioRpcServer();
}
/**
* 服务的自动注册器
* @return
*/
@Bean
public ServiceRegistry serviceRegistry(){
return new ServiceRegistry();
}
}
编写测试类,我们并不需要手动注册任何服务:
/**
* @author: HuYi.Zhang
**/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RpcServerConfig.class)
public class RpcServerTestWithSpring {
@Test
public void test01() throws InterruptedException {
// spring会自动注册服务,只要保证容器存活即可
Thread.sleep(Integer.MAX_VALUE);
}
}
启动并查看日志:
2018-03-18 17:31:45 INFO DefaultTestContextBootstrapper:248 - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]
2018-03-18 17:31:45 INFO DefaultTestContextBootstrapper:174 - Using TestExecutionListeners: [org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@39fb3ab6, org.springframework.test.context.support.DependencyInjectionTestExecutionListener@6276ae34, org.springframework.test.context.support.DirtiesContextTestExecutionListener@7946e1f4]2018-03-18 17:31:45 INFO GenericApplicationContext:583 - Refreshing org.springframework.context.support.GenericApplicationContext@1e88b3c: startup date [Sun Mar 18 17:31:45 CST 2018]; root of context hierarchy
2018-03-18 17:31:46 INFO ServiceRegistry:44 - 加载服务:cn.itcast.rpc.service.HelloService
2018-03-18 17:31:46 INFO BioRpcServer:60 - 服务启动成功,端口:9000
3.1.4、客户端实现服务的依赖注入
3.1.4.1、需求
在前面的案例中,客户端要想获取服务,需要手动创建RpcProxyFactory
实例,并从中获取服务的代理:
// 通过代理工厂,获取服务
HelloService helloService = new RpcProxyFactory<>(HelloService.class).getProxyObject();
这样太麻烦了,能不能像Spring中那样,通过@Autowired
实现自动的依赖注入呢?
要想通过@Autowired
自动注入,就必须在Spring容器中有对应的实例对象。然而在客户端只有接口,并没有实现类,更不会有对象,所以无法通过@Autowired
自动注入。
我们能不能模拟@Autowired
功能,自己来实现依赖注入呢?
3.1.4.2、@Autowired的原理
先来看一下Spring的Bean初始化流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdkxzEr2-1595402327049)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/spring-flow.png)]
执行流程:
- 1)所有的Bean实例化
- 2)对Bean的属性进行初始化
- 3)检查Aware相关接口
- 4)执行BeanPostProcessor的前置方法
- 5)处理实现了InitializingBean的类
- 6)处理添加了自定义init-method的类
- 7)BeanPostProcessor的后置方法
- …
我们需要注意其中的BeanPostProcessor这个东西:
BeanPostProcessor
接口中定义了两个方法:
/**
* Factory hook that allows for custom modification of new bean instances,
* e.g. checking for marker interfaces or wrapping them with proxies.
* ...
* 运行客户对 bean 进行自定义修改的 Bean工厂钩子(hook),Spring通过该接口作为标记进行检查
*
* <p>ApplicationContexts can autodetect BeanPostProcessor beans in their
* bean definitions and apply them to any beans subsequently created.
* ...
* ApplicationContexts 可以自动检测到实现该接口的Bean,并且在任何其他Bean创建之后执行它。
* 略...
*/
public interface BeanPostProcessor {
/**
* Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method).
* 前置方法:在给定的这个bean的初始化方法(afterPropertiesSet或init-method)执行之前执行。
* 略...
*/
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
/**
* Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method).
* 后置方法:在给定的这个bean的初始化方法(afterPropertiesSet或init-method)执行之后执行。
* 略...
*/
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
再来了解一下@Autowired
的实现原理,Spring正是通过一个BeanPostProcessor来实现@Autowired
自动注入的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M0lBfdZY-1595402327052)(https://huge0612.gitbooks.io/tour-of-rpc/content/rpc/images/autowired-processor.png)]
我们看类上的一段说明:
org.springframework.beans.factory.config.BeanPostProcessor implementation that autowires annotated fields, setter methods and arbitrary config methods.Such members to be injected are detected through a Java 5 annotation: by default,Spring's @Autowired and @Value annotations.
org.springframework.beans.factory.config.BeanPostProcessor 的一个实现,用来自动注入带有Autowired注解的字段、setter方法和任意配置方法。默认情况下,这些成员可以通过Java 5注解检测到,包括Spring的@Autowired和@Value注释。
总结:
Spring在实例化完成所有Bean以后,会检查Bean上是否实现了BeanPostProcessor
接口,如果有就会在任何普通Bean初始化时,先调用该接口的前置方法:postProcessBeforeInitialization()
,然后进行普通Bean的初始化。然后再调用后置方法:postProcessAfterInitialization()
。
@Autowired
注解正是利用了这个接口的特性,编写了一个AutowiredAnnotationBeanPostProcessor
,然后在其中对每一个Bean的成员进行检测,如果发现实现了@Autowired
注解或者@Value
注解(也支持JSR-330的注解),就会对其进行注入。
3.1.4.3、实现自动注入
思路:
我们也可以通过自定义注解的方式,来标记这些需要注入的属性。然后实现一个BeanPostProcessor
,在普通Bean初始化的时候进行拦截。如果发现我们的自定义标记,则通过RpcProxFactory
来生成代理并且注入。
首先,编写一个自定义注解,用来标记需要注入的成员:
/**
* 标记一个需要通过RPC注入的资源
* @author: HuYi.Zhang
* @create: 2018-03-16 22:04
**/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Reference {
}
然后,再编写一个BeanPostProcessor
:
/**
* Bean的后处理器,用来注入Rpc的动态代理对象
* @author: HuYi.Zhang
**/
public class RpcProxyBeanPostProcessor implements BeanPostProcessor {
private static final Logger logger = LoggerFactory.getLogger(RpcProxyBeanPostProcessor.class);
private final Map<Class<?>, Object> cache = new HashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 遍历所有字段
for (Field f : bean.getClass().getDeclaredFields()) {
// 判断是否有@Reference注解
if (f.isAnnotationPresent(Reference.class)) {
f.setAccessible(true);
Class<?> clazz = f.getType();
Object proxy = null;
// 判断该字段类型在缓存中是否存在
if (cache.containsKey(clazz)) {
proxy = cache.get(clazz);
} else {
// 动态代理生成对象
proxy = new RpcProxyFactory<>(clazz).getProxyObject();
cache.put(bean.getClass(), proxy);
}
try {
f.set(bean, proxy);
logger.info("为{}注入{}。", f, proxy);
} catch (Exception e) {
e.printStackTrace();
logger.error("属性" + f + "注入失败", e);
}
}
}
return bean;
}
}
3.1.5、客户端测试
首先编写Spring的配置类,只需要把自定义的BeanPostProcessor
注册就可以了:
@Configuration
public class RpcClientConfig {
/**
* 处理@Refrence注解标记的属性自动注入
* @return
*/
@Bean
public RpcProxyBeanPostProcessor serviceReferenceHandler() {
return new RpcProxyBeanPostProcessor();
}
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RpcClientConfig.class)
public class RpcClientTestWithSpring {
@Reference
private HelloService helloService;// 自动注入,无需手动获取
@Test
public void test01() {
String result = this.helloService.sayHello("Jack");
System.out.println(result);
Assert.assertEquals("调用失败", "hello, Jack", result);
}
}
启动并查看日志:
2018-03-18 17:38:34 INFO DefaultTestContextBootstrapper:248 - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]
2018-03-18 17:38:34 INFO DefaultTestContextBootstrapper:174 - Using TestExecutionListeners: [org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@5cc7c2a6, org.springframework.test.context.support.DependencyInjectionTestExecutionListener@b97c004, org.springframework.test.context.support.DirtiesContextTestExecutionListener@4590c9c3]2018-03-18 17:38:34 INFO GenericApplicationContext:583 - Refreshing org.springframework.context.support.GenericApplicationContext@5e3a8624: startup date [Sun Mar 18 17:38:34 CST 2018]; root of context hierarchy
2018-03-18 17:38:34 INFO PostProcessorRegistrationDelegate$BeanPostProcessorChecker:327 - Bean 'rpcClientConfig' of type [cn.itcast.rpc.config.RpcClientConfig$$EnhancerBySpringCGLIB$$c5d66a2a] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2018-03-18 17:38:34 INFO RpcProxyBeanPostProcessor:47 - 为private cn.itcast.rpc.service.HelloService cn.itcast.rpc.RpcClientTestWithSpring.helloService注入com.sun.proxy.$Proxy17@e056f20, with InvocationHandler cn.itcast.rpc.client.RpcProxyFactory@4b0b0854。
2018-03-18 17:38:34 INFO BioRpcClient:44 - 建立连接成功:127.0.0.1:9000
2018-03-18 17:38:34 INFO BioRpcClient:47 - 发起请求,目标主机127.0.0.1:9000,服务:cn.itcast.rpc.service.HelloService.sayHello(class java.lang.String)
2018-03-18 17:38:34 INFO RpcProxyFactory:61 - 调用远程服务成功!
hello, Jack
2018-03-18 17:38:34 INFO GenericApplicationContext:984 - Closing org.springframework.context.support.GenericApplicationContext@5e3a8624: startup date [Sun Mar 18 17:38:34 CST 2018]; root of context hierarchy