前言
总结 内容主要包括dubbo 框架的spi 实现,服务暴露实现,服务调用实现,以及个人感觉里面比较好的设计分享。涉及的代码部分较多。主要是自己的主观理解,如有纰漏 可以随时指正。
RPC
rpc 框架在使用时可以让使用者调用远程的接口的时候犹如调本地接口一样,然而一个远程过程调用一定是会使用网络和序列化的,因此简单一点看,dubbo或者说所有的rpc框架提供的核心的能力就是通过动态代理的方式把接口的网络操作和序列化操作代理掉,对于使用者是透明的。dubbo在这个核心能力之上,外加了一些过滤,负载均衡,服务注册发现等功能,同时通过spi机制为这些功能加入了很多动态的扩展,为了和Spring一起使用,也有很多Spring的扩展,使得dubbo越来越庞大。因此理解dubbo,最好是从他核心去看,抽象出他的本质是要干什么,在去看他的扩展的东西。这样会清楚很多,否则面对庞大的框架代码就是盲人摸象。因此在此之前先总结下一个rpc的本质在java中的实现,然后对照的去它的代码中寻找实现点。
基于java实现简单的rpc
代码可以下载:GitHub - hbzhangwenjie/miniRPC: 使用java反射+动态代理模拟 RPC
dubbo或者说一个rpc抽象出核心的能力就是通过动态代理的方式把接口的网络操作和序列化操作代理掉。之所以他很复杂,是因为在这个核心的步骤之上加入了扩展功能。那么只管本质的话 通过一个动态代理和网络操作就可以实现简单的rpc。
框架把这部分的东西做了,对使用者透明, 在这里 简单 展现出来,方便后面查看它的代码时会比较好理
提供查询当前时间服务的接口
package api; public interface TimeService { public long getCurrentTimeMillis() ; public Long getCurrentTimeSec(); } |
服务端timeService的具体实现
package provide; import api.TimeService; public class TimeServiceImpl implements TimeService { @Override public long getCurrentTimeMillis() { return System.currentTimeMillis(); } @Override public Long getCurrentTimeSec() { return System.currentTimeMillis()/1000; } } |
服务端通过暴露服务
package provide; import static provide.Provider.beanFactory; import api.TimeService; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; /** * 主要是类比rpc服务端的网络和序列化部分 * */ public class Server extends Thread { @Override public void run() { System.out.println("server start..."); try { // 打开服务端Socket ServerSocket serverSocket = new ServerSocket(9000); // 接受客户端的请求 Socket socket = serverSocket.accept(); while (null != socket) { // ObjectInputStream是java 提供的序列化/反序列化的一种工具 ObjectInputStream inStream = null; try { inStream = new ObjectInputStream(socket.getInputStream()); } catch (Exception e) { } if (inStream == null) { continue; } String invoker = null; try { invoker = (String) inStream.readObject(); } catch (Exception e) { } if (invoker == null) { continue; } //这里 没有实现 exchange 层 System.out.println("server receive...:" + invoker); //invoker 在框架中是一个类,这里用string简单代替 String service = invoker.split(",")[0]; String method = invoker.split(",")[1]; //根据客户端传入的请求 找到具体的方法,使用实现类去执行,这里还还少一个类型, // api.TimeService,在框架中通过类型和名字找到唯一的bean //还有入参 这里简单实现没有写 TimeService timeService = (TimeService) beanFactory.get(service); //通过反射执行服务端实现类的方法 Class clazz = timeService.getClass(); Method[] methods = clazz.getMethods(); for (Method targetMethod : methods) { if (method.equals(targetMethod.getName())) { long currentTimeMillis = (long) targetMethod.invoke(timeService); //序列化 ObjectOutputStream outStream = new ObjectOutputStream(socket.getOutputStream()); // 返回结果 outStream.writeLong(currentTimeMillis); outStream.flush(); } } } } catch (Throwable t) { t.printStackTrace(); } } } |
客户端发起请求的代理
package consumer.proxy; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.Socket; /** * 这个类 在rpc框架中 自动生成 * */ public class ConsumerProxy implements InvocationHandler { private Socket socket; private Invoker invoker; public ConsumerProxy(Invoker invoker) { this.invoker = invoker; try { socket = new Socket("127.0.0.1", 9000); } catch (IOException e) { e.printStackTrace(); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ObjectOutputStream outStream = new ObjectOutputStream(socket.getOutputStream()); //通过反射来构造 invoker,invoker还包括了,args 这个里 简单实现没有用到 String rpcInvocation = invoker.getInterfaces().getTypeName() + "," + method.getName(); outStream.writeObject(rpcInvocation); outStream.flush(); ObjectInputStream inStream = new ObjectInputStream(socket.getInputStream()); return inStream.readLong(); } public Object getProxy() { return Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[]{invoker.getInterfaces()}, this ); } } |
这样一个最简单的rpc调用就写好了,客户端是没有具体的实现的,具体的实现在服务端。客户端通过和服务端一起定义好的协议(invoker)通过一样的序列化方式。完成远程调用
启动服务端
package provide; import java.util.HashMap; /** */ public class Provider { public static final HashMap<String, Object> beanFactory = new HashMap<>(); public static void main(String[] args) { //类比框架加载服务端的实现类 beanFactory.put("api.TimeService", new TimeServiceImpl()); //类比rpc框架 启动 监听一个端口 Server server = new Server(); server.start(); } } |
启动客户端
package consumer; import api.TimeService; import consumer.proxy.ConsumerProxy; import consumer.proxy.Invoker; public class Consumer { public static void main(String[] args) { Invoker invoker = new Invoker(TimeService.class); ConsumerProxy consumerProxy = new ConsumerProxy(invoker); //这个客户端是类比框架根据接口代理生成的一个实现类,它把网络代理进去 ,使用方无感知 //dubbo 中是框架启动时帮我们生成的这个代理类,通过扫描 @dubboReference 这2个注解来实现 TimeService timeService = (TimeService) consumerProxy.getProxy(); for (int i = 0; i < 10; i++) { System.out.println("consumer response:" + timeService.getCurrentTimeMillis()); } } } |
上面是一个简单的例子,要能用还需要在关键步揍加上 服务发现,协议完善,等等,这些加上就是一个rpc框架,但是它的核心思路和整体的流程还是不变的。
dubbo
基于上面对rpc 本质的 总结,在看dubbo框架,它是一个扩展力非常强的框架,使用者可以不用改源码的情况下通过插件化替换一个rpc过程中的关键实现。其中包括,容错,负载均衡,调用前后过滤,消费方线程池(在dubbo里把主调方称为消费者),生产方线程池(在dubbo里把被调方称为生产者),协议(默认是dubbo协议,可以暴露成其他协议 http,grpc等),序列化,服务发现等都可以 基于使用方自己的需求进行替换。之所以这么灵活是因为dubbo实现了一套spi机制。
图例说明:
- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
exchange 层其实是指明这个rpc协议的交互方式,一般是 request/repsonse 或者subscription/publish 应该说所有的rpc都是前者 包括http, 消息中间件和一些物联网协议(mqtt)和im 是后者。
从图看,最重要的是 服务方怎么把impl 映射到invoker,调用方怎么把service 转成invoker,这都是用到了动态代理,另一个重点是 如何做到插件话,spi跟rpc没有关系,他是实现插件话的一种机制,在关键步骤通过spi把自己的实现替换框架原有的实现。对spi理解, 非常关键。
SPI
SPI ,全称为 Service Provider Interface,它通过在ClassPath路径下的约定的某个文件夹查找java文件,然后自动加载文件里所定义的类。在面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。
java 的spi
在jdk6里面引进的一个新的特性ServiceLoader,从官方的文档来说,它主要是用来装载一系列的service provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。最经常被使用的就是我们的程序加载数据库驱动jdk不关心连接什么数据库,只提供标准的数据库操作,这些操作都是接口,没有具体的实现,而具体的实现由数据库厂商去实现,并提供jar 包给我们的程序使用,如下 加载mysql的驱动时:
springboot 的spi
spring boot 的 spi 主要是用在自动装配上,springboot 使用自动装配的方式 消灭了spring xml 的装配过程。例如现在我们使用一些客户端 只需要引入对应的starter 然后按照它的约定配置 ,在配置文件写上对应的自己项目需要的值,就可以使用这个客户端了,这也是 springboot 约定大于配置 这一思想的实现方式。
springboot 使用@Configuration 和@bean 注解 替代了xml配置bean 的方式。那么如果我们不使用starter也是可以自己用这个2个注解写一个配置类来装配一个bean 注册到spring容器。 但是这个装配类怎么提供给其他项目使用,就可以把它通过spi 变成一个starter来实现。
在springboot的自动装配过程中,会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。
@Configuration @EnableConfigurationProperties(CosClientProperties.class) public class CosClientAutoConfiguration { private CosClientProperties cosClientProperties; @Autowired public CosClientAutoConfiguration(CosClientProperties cosClientProperties) { this.cosClientProperties = cosClientProperties; } @Bean public COSClient getCOSClient() { //1 初始化用户身份信息(secretId, secretKey)。 COSCredentials cred = new BasicCOSCredentials(cosClientProperties.getSecretId(), cosClientProperties.getSecretKey()); // 2 设置 bucket 的区域, COS 地域的简称请参照 Region region = new Region(cosClientProperties.getRegionStr()); ClientConfig clientConfig = new ClientConfig(region); // 3 生成 cos 客户端。 return new COSClient(cred, clientConfig); } @Bean public CosService getCosService(COSClient cosClient) { return new CosServiceImpl(cosClient); } } |
import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "cos.client") public class CosClientProperties { private String secretId; private String secretKey; private String regionStr; public String getSecretId() { return secretId; } public void setSecretId(String secretId) { this.secretId = secretId; } public String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public String getRegionStr() { return regionStr; } public void setRegionStr(String regionStr) { this.regionStr = regionStr; } } |
如上一个cos 客户端的 就生成了,其他程序可以直接引入然后通过application.yml配置自己的cos信息 来注册一个cos客户端到Spring容器
如果不在spring.factories文件中配置,那么 其他项目引入这个jar包的话,还能通过自动装配这个bean吗?
答案是不一定,因为springboot 是通过扫描@Configuration注解,来装配一个bean,也就是只要能扫到这个配置类就可以,而springboot项目启动扫描@Configuration,@Component, @service时如果不配置@ComponentScan 的包路径那么,默认是扫描启动类的当前包路径,因此如果是第三方或者第二方包,他们的包路径不具备通用性的话,是加载不到这个配置类的因此需要通过在spring.factories文件告诉springboot去加载这个类。 |
springboot 的spi除了在自动装配中使用,还在一些暴露出给用户的扩展点里使用,基本上可以概括为3类postprocessor,
1.BeanPostprocessor : 初始化一个bean时可以扩展这个扩展点来 加一些自己的逻辑
2.BeanFactoryPostProcessor(BeanDefinitionRegistryPostProcessor) 解析一个bean 时可以扩展这个扩展点来 加一些自己的逻辑
3.EnvironmentPostProcessor 在项目启动加载application 和系统的配置时可以扩展,来加自己的逻辑,如把配置中心的配置从远程读取写到Spring的环境,项目像使用本地appllication.yml配置的一样使用这些配置。
像3这个扩展点就必须在spring.factories文件中配置才行,因为他发生在扫描装配bean之前。不依赖包路径。 其中1,2 基本是一些集成Spring的框架会用到,比如 @DubboService @DubboReference 这些注解 的实现就依赖 1,2的扩展点。
Dubbo的spi
dubbo的spi 实现的更加灵活 它可以做到在方法级别的spi。Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader<T> 类中,通过 ExtensionLoader<T> ,我们可以加载指定的实现类,先讲总结她的功能 后面总结它的实现,dubbo中的扩展点根据功能可以分为如下几类
普通的扩展点: 使用@spi 标示的接口,根据spi(“value”) 选择具体的实现类 和上诉的java /springboot 的spi 一样 通过 ExtensionLoader.getExtensionLoader(type).getExtension(name)来使用
自适应扩展点:根据方法的不同入参使用不同的实现类,依赖@adaptive注解实现 , 通过 ExtensionLoader.getExtensionLoader(RouterFactory.class).getAdaptiveExtension() 来使用
自激活扩展点:根据不同的条件 选择使用不同的实现类 多作用在fillter上,依赖@Activate 注解实现,通过 ExtensionLoader.getExtensionLoader(RegistryServiceListener.class).getActivateExtension(url, key,group)) 使用 ,通过key 从url中取出value,group 区分 消费方和提供方
扩展点自动包装: 实现类的构造函数包含这个扩展点,那么就是一个自动包装的扩展点,功能和aop类似。 (cluster)
扩展点自动注入:加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader
会自动注入依赖的扩展点。ExtensionLoader
通过扫描扩展点实现类的所有 setter 方法来判定其成员。功能和ioc类似。(spring 自动注入)
@SPI
@SPI主要标示在接口上,标示这个接口是个扩展点,具体使用哪个实现是通过配置去找到具体实现类。这个注解可以接受一个value,这个值和文件(spi获取具体现实类全路径名的文件)里面的key 对应。dubbo的这个文件格式是key:实现类。例如
filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper mock=org.apache.dubbo.rpc.support.MockProtocol |
key也不是一定需要,不写key的话使用类名作为key
dubbo 会从3个路径是读取这个文件,它的代码如下
private static final String SERVICES_DIRECTORY = "META-INF/services/"; private static final String DUBBO_DIRECTORY = "META-INF/dubbo/"; private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/"; |
private Map<String, Class<?>> loadExtensionClasses() { cacheDefaultExtensionName(); Map<String, Class<?>> extensionClasses = new HashMap<>(); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); return extensionClasses; } |
总结就是/META-INF/services /META-INF/dubbo /META-INF/dubbo/internal META-INF/dubbo/external/
4个路径 现在的dubbo已经不是直接写上 路径而是通过路径策略获取,下文会出现
@Adaptive
当这个注解标注在方法上的时候,这个方法的入参一定是带有URL这个在dubbo定义的一个对象的。在扫描到这个扩展点的这个方法的注解的时候,会生成一个代理类,在这个方法里,会获取URL,他是和这个接口名字一样的一个属性,(例如protocol接口的export方法有@adaptive注解它生成的代理类就会从url里面获取一个protocol属性的值)这个属性的值是来获取对应的扩展实现类的。如果异常就获取默认的扩展实现类,它生成这个类的方式就是通过string字符串去拼接这个代理类,然后在用编译器编译为class。它生成的字符串如下如下:例子是 Protocol的扩展Adaptive
package org.apache.dubbo.rpc; import org.apache.dubbo.common.extension.ExtensionLoader; public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { public void destroy() { throw new UnsupportedOperationException( "The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } public int getDefaultPort() { throw new UnsupportedOperationException( "The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { if (arg0 == null) { throw new IllegalArgumentException("org.apache.dubbo.rpc.Invokerargument == null"); } if (arg0.getUrl() == null) { throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); } org.apache.dubbo.common.URL url = arg0.getUrl(); String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) { throw new IllegalStateException( "Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); } org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.export(arg0); } public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException { if (arg1 == null) { throw new IllegalArgumentException("url == null"); } org.apache.dubbo.common.URL url = arg1; String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) { throw new IllegalStateException( "Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); } org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.refer(arg0, arg1); } } |
Protocol的源码是 上下对照就能清楚知道生成的代理类是怎么来的了spi中的值就是默认值
@SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); } |
@Adaptive标示在类上的时候比较特殊,且只能有一个实现类被它标示在类上。标示在类上,此种情况,表示拓展的代理类由人工编码完成,而不会自动生成,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory,,它一般会遍历其他所有的实现类来完成功能,比如AdaptiveExtensionFactory 这样就是和标示在方法上相对应,标示在方法上是根据条件选择一个扩展点。这里是遍历所由扩展点看哪个可以执行。
@Adaptive public class AdaptiveExtensionFactory implements ExtensionFactory { private final List<ExtensionFactory> factories; public AdaptiveExtensionFactory() { ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class); List<ExtensionFactory> list = new ArrayList<ExtensionFactory>(); for (String name : loader.getSupportedExtensions()) { list.add(loader.getExtension(name)); } factories = Collections.unmodifiableList(list); } @Override public <T> T getExtension(Class<T> type, String name) { for (ExtensionFactory factory : factories) { T extension = factory.getExtension(type, name); if (extension != null) { return extension; } } return null; } } |
这个类的作用是为扩展点实例化的时候注入对象属性的,它遍历的主要是SpiExtensionFactory和SpringExtensionFactory 来获取它保存的一个实例。SpringExtensionFactory就是拿到Spring管理的那些bean。
总结dubbo中的自适应扩展点就是依赖@Adaptive注解实现,它的含义就是在运行时根据不同的配置选择接口的不同实现类的不同方法。
@Activate
主要是用在有多个扩展点的实现,需要跟据不同条件被激活的场景,主要用在filter上例如暴露服务ProtocolFilterWrapper的代码
private static <T> Invoker<T> buildInvokerChain(final In |