本文对应源码地址:https://github.com/nieandsun/dubbo-study
想要在一个JVM里调用另一个JVM里的的方法,或许你会想到如下的姿势:
但是在真正进行方法调用时,你不仅要考虑传什么参数、调用哪个方法,还得考虑对方服务的暴露地址等。
而用过dubbo的肯定都知道,它可以做到:在一个JVM(消费端)里调用另一个JVM(服务端)的方法,就如同你在另一个JVM(服务端)直接调自己的方法一样 —> 这究竟是如何做到的呢,本篇文章将主要来探索一下这个问题。
无论是使用注解,还是xml配置的方式,大家必须要有的前置知识是,dubbo中一个要向外暴露的服务(service),会先被spring解析并包装为一个ServiceBean,然后再拿着该ServiceBean进行真正的服务暴露。在dubbo中真正进行服务暴露的源码如下:
其实在该段代码中还涉及到了几个重要的概念:
【具体要暴露的服务(即ref)】
、【服务的接口】
和【要暴露服务的url】
等【服务的接口】
和【要暴露服务的url】
同时还需要注意 ProxyFactory 和 Protocol 都是dubbo的SPI扩展点
, 在dubbo源码中获取PROTOCOL和PROXY_FACTORY的姿势如下:
看过我《【dubbo源码解析】 — dubbo spi 机制(@SPI、@Adaptive)详解》这篇文章的你,肯定会想翻一下 ProxyFactory 和 Protocol 的源码,这里我们简单看一下:
@SPI("dubbo") //dubbo默认使用的协议为dubbo协议
public interface Protocol {
int getDefaultPort();
@Adaptive //在方法上标注该注解,说明会静态代理Protocol 实现类的该方法 ---> 服务暴露的方法
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive//在方法上标注该注解,说明会静态代理Protocol 实现类的该方法 ---> 根据class类型和URL获取Invoker【消费端的逻辑】
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
default List<ProtocolServer> getServers() {
return Collections.emptyList();
}
}
@SPI("javassist")//dubbo默认使用的代理工厂为静态代理工厂
public interface ProxyFactory {
//PROXY_KEY其实指代的就是字符串“proxy”
@Adaptive({PROXY_KEY}) //在方法上标注该注解,说明会静态代理ProxyFactory 实现类的该方法 ---> 获取要暴露的服务的代理类
<T> T getProxy(Invoker<T> invoker) throws RpcException;
@Adaptive({PROXY_KEY})//在方法上标注该注解,说明会静态代理ProxyFactory 实现类的该方法 ---> 获取服务的代理类
<T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException;
@Adaptive({PROXY_KEY})//在方法上标注该注解,说明会静态代理ProxyFactory 实现类的该方法 ---> 其实这里是生成Invoker
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;
通过上面的源码分析,你会想到什么呢??? ---》 这里请大家自己先开脑洞。。。
有了上面的知识后,其实我们就可以仿照dubbo的源码,自己实现一个服务端了。当然本文仅仅是探索RPC远程调用的原理,所以这里的Serive,我们大可以直接new一个出来。
仿照dubbo源码自己写的dubbo服务端:
ExtensionLoader<ProxyFactory> proxyLoader = ExtensionLoader.getExtensionLoader(ProxyFactory.class);
//支持的协议:dubbo、http、hessian、rmi等
ExtensionLoader<Protocol> protocolLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
//URL protocol_url = URL.valueOf("dubbo://127.0.0.1:9300/" + DemoService.class.getName());//静态代理
URL protocol_url = URL.valueOf("dubbo://127.0.0.1:9300/" + DemoService.class.getName() + "?proxy=jdk"); //动态代理
@Test
public void serverRpc() throws IOException {
DemoService service = new DemoServiceImpl();
//生成代理工厂
// --- 由URL确定到底是动态代理工厂(JdkProxyFactory)还是静态代理工厂(JavassistProxyFactory)
// --- 默认情况下为静态代理工厂
ProxyFactory proxy = proxyLoader.getAdaptiveExtension();
//由代理工厂生成Invoker对象
// --- Invoker对象里包含了具体的服务(service,在dubbo源码里服务端称为ref)、服务的接口和要暴露服务的url
// --- 但值得注意的是这里返回的Invoker对象,是用Invoker接口进行接收的,也就是说通过下面的serviceInvoker,只能获取到service的接口
Invoker<DemoService> serviceInvoker = proxy.getInvoker(service, DemoService.class, protocol_url);
//获取具体的协议
// ---由URL确定到底使用哪个协议,默认情况下使用dubbo协议
Protocol protocol = protocolLoader.getAdaptiveExtension();
//利用protocol暴露服务
// --- 暴露服务的具体流程为
// --- (1) 拿到服务的【动态代理类或者静态代理类】、【接口】、以及【url】
// --- (2) 拿着(1)中获取到的三个内容进行真正的服务暴露
Exporter<DemoService> exporter = protocol.export(serviceInvoker);
System.out.println("server 启动协议:" + protocol_url.getProtocol());
// 保证服务一直开着
System.in.read();
exporter.unexport();
}
这里简单提一下dubbo的jar包的引入问题。
如果你想了解dubbo具体的协议,可以单独引入各个协议的jar包,比如说上面的代码如果想支持dubbo、http、hessian、rmi四种协议,需要引入的jar包如下:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubbo-rpc-dubboartifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubbo-remoting-netty4artifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubbo-serialization-hessian2artifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubbo-rpc-httpartifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubbo-rpc-rmiartifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubbo-rpc-hessianartifactId>
<version>${dubbo.version}version>
dependency>
以注解配置为例,大家必须要有的前置知识是:当dubbo项目启动时,遇到@Reference(新版本的dubbo为@DubboReference)注解,会创建一个 ReferenceBean实例。该实例实现了InitializingBean 接口, 在其调用的afterPropertiesSet 方法中, 会为服务调用方 — 也就是@Reference标注的那个bean,创建一个远程代理对象。
创建过程中比较重要的两步源代码如下:
【1】获取可以调用服务端的服务,并将其和url、interface一起封装成一个Invoker
dubbo源码(1):消费端直连服务端的情况(在@Reference注解里可以配置直连的url)
或
dubbo源码(2):从注册中心获取服务端URL的情况
这里需要注意的是
:其实这一步已经获取到可以调用服务端的服务了,也就是说这个Invoker里封装了【可以调用服务端的服务】、【服务端的url】和【interface】。
获取【可以调用服务端的服务】的大致流程如下:
跟踪源码可以发现,无论是走http协议、rmi协议还是hessian协议【其他的协议我没具体研究】,它最终都会调用一个doRefer方法。我们这里简单看一下RMI协议的doRefer方法的源码:
对比一下我的上篇文章《【dubbo源码解析~番外篇】— JDK和Spring的RMI使用姿势简介》,你就会豁然开朗了,原来这就是通过【服务端url】和【interface】查找到服务端暴露的服务啊。
当然这里我还是保留上篇文章第4部分的观点:
消费端咋就找到了服务端的服务,并且为啥在消费端一调用,就会直接调用到服务端的方法这一过程,无需过度探索。
【2】代理工厂拿着协议对象创建的Invoker,创建实际的代理类,该代理类最终就会成为@Reference注解标注的那个实例bean
有了上面的知识后,其实我们也可以仿照dubbo的源码,自己实现一个消费端了。当然还是那句话本文仅仅是探索RPC远程调用的原理,所以一切从简。
仿照dubbo源码自己写的dubbo消费端:
@Test
public void clientRpc() {
//获取具体的协议
// ---由URL确定到底使用哪个协议,默认情况下使用dubbo协议
Protocol protocol = protocolLoader.getAdaptiveExtension();
//由代理工厂生成Invoker对象
// --- Invoker对象里包含了服务的接口和要暴露服务的url
// --- 但值得注意的是这里返回的Invoker对象,是用Invoker接口进行接收的,也就是说通过下面的serviceInvoker,只能获取到service的接口
Invoker<DemoService> referInvoker = protocol.refer(DemoService.class, protocol_url);
//生成代理工厂
// --- 由URL确定到底是动态代理工厂(JdkProxyFactory)还是静态代理工厂(JavassistProxyFactory)
// --- 默认情况下为静态代理工厂
ProxyFactory proxy = proxyLoader.getAdaptiveExtension();
//生成DemoService的代理类
DemoService service = proxy.getProxy(referInvoker);
Map<String, String> info = new HashMap();
info.put("target", "orderService");
info.put("methodName", "getOrderInfo");
info.put("arg", "1");
Map<String, String> result = service.getHelloInfo(info);
System.out.println(result);
}
启用服务端【我提供的代码可以测试的协议有http、rmi、dubbo、hessian】
启用消费端调用服务端的服务
这里需要再提一下的是:
看完本文2.1.2的源码分析后,你会不会想到,其实dubbo使用注册中心的玩法,就是扩展了一个Protocol—>RegistryProtocol,然后只要你修改一下URL的参数【表现在dubbo实际使用中,就是修改一下配置文件+引入一下注册中心的jar】,就可以指向那个Protocol了 —》由此dubbo的核心源码还是本文提到的这些,但却可以将注册中心引入进来了—> 代码的扩展性多么好!!!
2020-07-15 夜