写点什么

源码分析 Dubbo 服务消费端启动流程

  • 2021 年 11 月 12 日
  • 本文字数:5790 字

    阅读完需:约 19 分钟

if (initialized) {


return;


}


initialized = true;


if (interfaceName == null || interfaceName.length() == 0) {


throw new IllegalStateException("<dubbo:reference interface="" /> interface not allow null!");


}


Step1:如果已经初始化,直接返回,如果 interfaceName 为空,则抛出异常。


ReferenceConfig#init 调用 ReferenceConfig#checkDefault


private void checkDefault() {


if (consumer == null) {


consumer = new ConsumerConfig();


}


appendProperties(consumer);


}


Step2:如果 dubbo:reference 标签也就是 ReferenceBean 的 consumer 属性为空,调用 appendProperties 方法,填充默认属性,其具体加载顺序:


  1. 从系统属性加载对应参数值,参数键:dubbo.consumer.属性名,从系统属性中获取属性值的方法为:System.getProperty(key)。

  2. 加载属性配置文件的值。属性配置文件,可通过系统属性:dubbo.properties.file,如果该值未配置,则默认取 dubbo.properties 属性配置文件。


ReferenceConfig#init


appendProperties(this);


Step3:调用 appendProperties 方法,填充 ReferenceBean 的属性,属性值来源与 step2 一样,当然只填充 ReferenceBean 中属性为空的属性。


ReferenceConfig#init


if (getGeneric() == null && getConsumer() != null) {


setGeneric(getConsumer().getGeneric());


}


if (ProtocolUtils.isGeneric(getGeneric())) {


interfaceClass = GenericService.class;


} else {


try {


interfaceClass = Class.forName(interfaceName, true, Thread.currentThread().getContextClassLoader());


} catch (ClassNotFoundException e) {


throw new IllegalStateException(e.getMessage(), e);


}


checkInterfaceAndMethods(interfaceClass, methods);


}


Step4:如果使用返回引用,将 interface 值替换为 GenericService 全路径名,如果不是,则加载 interfacename,并检验 dubbo:reference 子标签 dubbo:method 引用的方法是否在 interface 指定的接口中存在。


ReferenceConfig#init


String resolve = System.getProperty(interfaceName); // @1


String resolveFile = null;


if (resolve == null || resolve.length() == 0) { // @2


resolveFile = System.getProperty("dubbo.resolve.file"); // @3 start


if (resolveFile == null || resolveFile.length() == 0) {


File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties");


if (userResolveFile.exists()) {


resolveFile = userResolveFile.getAbsolutePath();


}


} // @3 end


if (resolveFile != null && resolveFile.length() > 0) { // @4


Properties properties = new Properties();


FileInputStream fis = null;


try {


fis = new FileInputStream(new File(resolveFile));


properties.load(fis);


} catch (IOException e) {


throw new IllegalStateException("Unload " + resolveFile + ", cause: " + e.getMessage(), e);


} finally {


try {


if (null != fis) fis.close();


} catch (IOException e) {


logger.warn(e.getMessage(), e);


}


}


resolve = properties.getProperty(interfaceName);


}


}


if (resolve != null && resolve.length() > 0) { // @5


url = resolve;


if (logger.isWarnEnabled()) {


if (resolveFile != null && resolveFile.length() > 0) {


logger.warn("Using default dubbo resolve file " + resolveFile + " replace " + interfaceName + "" + resolve + " to p2p invoke remote service.");


} else {


logger.warn("Using -D" + in


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


terfaceName + "=" + resolve + " to p2p invoke remote service.");


}


}


}


Step5:处理 dubbo 服务消费端 resolve 机制,也就是说消息消费者只连服务提供者,绕过注册中心。


代码 @1:从系统属性中获取该接口的直连服务提供者,如果存在 -Dinterface=dubbo://127.0.0.1:20880,其中 interface 为 dubbo:reference interface 属性的值。


代码 @2:如果未指定-D 属性,尝试从 resolve 配置文件中查找,从这里看出-D 的优先级更高。


代码 @3:首先尝试获取 resolve 配置文件的路径,其来源可以通过-Ddubbo.resolve.file=文件路径名来指定,如果未配置该系统参数,则默认从 ${user.home}/dubbo-resolve.properties,如果过文件存在,则设置 resolveFile 的值,否则 resolveFile 为 null。


代码 @4:如果 resolveFile 不为空,则加载 resolveFile 文件中内容,然后通过 interface 获取其配置的直连服务提供者 URL。


代码 @5:如果 resolve 不为空,则填充 ReferenceBean 的 url 属性为 resolve(点对点服务提供者 URL),打印日志,点对点 URL 的来源(系统属性、resolve 配置文件)。


ReferenceConfig#init


checkApplication();


checkStubAndMock(interfaceClass);


Step6:校验 ReferenceBean 的 application 是否为空,如果为空,new 一个 application,并尝试从系统属性(优先)、资源文件中填充其属性;同时校验 stub、mock 实现类与 interface 的兼容性。系统属性、资源文件属性的配置如下:


application dubbo.application.属性名,例如 dubbo.application.name


ReferenceConfig#init


Map<String, String> map = new HashMap<String, String>();


Map<Object, Object> attributes = new HashMap<Object, Object>();


map.put(Constants.SIDE_KEY, Constants.CONSUMER_SIDE);


map.put(Constants.DUBBO_VERSION_KEY, Version.getVersion());


map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));


if (ConfigUtils.getPid() > 0) {


map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));


}


Step7:构建 Map,封装服务消费者引用服务提供者 URL 的属性,这里主要填充 side:consume(消费端)、dubbo:2.0.0(版本)、timestamp、pid:进程 ID。


ReferenceConfig#init


if (!isGeneric()) {


String revision = Version.getVersion(interfaceClass, version);


if (revision != null && revision.length() > 0) {


map.put("revision", revision);


}


String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();


if (methods.length == 0) {


logger.warn("NO method found in service interface " + interfaceClass.getName());


map.put("methods", Constants.ANY_VALUE);


} else {


map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));


}


}


Step8:如果不是泛化引用,增加 methods:interface 的所有方法名,多个用逗号隔开。


ReferenceConfig#init


map.put(Constants.INTERFACE_KEY, interfaceName);


appendParameters(map, application);


appendParameters(map, module);


appendParameters(map, consumer, Constants.DEFAULT_KEY);


appendParameters(map, this);


Step9:用 Map 存储 application 配置、module 配置、默认消费者参数(ConsumerConfig)、服务消费者 dubbo:reference 的属性。


ReferenceConfig#init


String prefix = StringUtils.getServiceKey(map);


if (methods != null && !methods.isEmpty()) {


for (MethodConfig method : methods) {


appendParameters(map, method, method.getName());


String retryKey = method.getName() + ".retry";


if (map.containsKey(retryKey)) {


String retryValue = map.remove(retryKey);


if ("false".equals(retryValue)) {


map.put(method.getName() + ".retries", "0");


}


}


appendAttributes(attributes, method, prefix + "." + method.getName());


checkAndConvertImplicitConfig(method, map, attributes);


}


}


Step10:获取服务键值 /{group}/interface:版本,如果 group 为空,则为 interface:版本,其值存为 prifex,然后将 dubbo:method 的属性名称也填入 map 中,键前缀为 dubbo.method.methodname.属性名。dubbo:method 的子标签 dubbo:argument 标签的属性也追加到 attributes map 中,键为 prifex + methodname.属性名。


ReferenceConfig#init


String hostToRegistry = ConfigUtils.getSystemProperty(Constants.DUBBO_IP_TO_REGISTRY);


if (hostToRegistry == null || hostToRegistry.length() == 0) {


hostToRegistry = NetUtils.getLocalHost();


} else if (isInvalidLocalHost(hostToRegistry)) {


throw new IllegalArgumentException("Specified invalid registry ip from property:" + Constants.DUBBO_IP_TO_REGISTRY + ", value:" +


hostToRegistry);


}


map.put(Constants.REGISTER_IP_KEY, hostToRegistry);


Step11:填充 register.ip 属性,该属性是消息消费者连接注册中心的 IP,并不是注册中心自身的 IP。


ReferenceConfig#init


ref = createProxy(map);


Step12:调用 createProxy 方法创建消息消费者代理,下面详细分析其实现细节。


ReferenceConfig#init


ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());


ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);


Step13:将消息消费者缓存在 ApplicationModel 中。


1.2.1 源码分析 ReferenceConfig#createProxy 方法


ReferenceConfig#createProxy


URL tmpUrl = new URL("temp", "localhost", 0, map);


final boolean isJvmRefer;


if (isInjvm() == null) {


if (url != null && url.length() > 0) { // if a url is specified, don't do local reference


isJvmRefer = false;


} else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {


// by default, reference local service if there is


isJvmRefer = true;


} else {


isJvmRefer = false;


}


} else {


isJvmRefer = isInjvm().booleanValue();


}


Step1:判断该消费者是否是引用本(JVM)内提供的服务。


如果 dubbo:reference 标签的 injvm(已过期,被 local 属性替换)如果不为空,则直接取该值,如果该值未配置,则判断 ReferenceConfig 的 url 属性是否为空,如果不为空,则 isJvmRefer =false,表明该服务消费者将直连该 URL 的服务提供者;如果 url 属性为空,则判断该协议是否是 isInjvm,其实现逻辑:获取 dubbo:reference 的 scop 属性,根据其值判断:


  • 如果为空,isJvmRefer 为 false。

  • 如果协议为 injvm,就是表示为本地协议,既然提供了本地协议的实现,则无需配置 isJvmRefer 该标签为 true,故,isJvmRerfer=false。

  • 如果 scope=local 或 injvm=true,isJvmRefer=true。

  • 如果 scope=remote,isJvmRefer 设置为 false。

  • 如果是泛化引用,isJvmRefer 设置为 false。

  • 其他默认情况,isJvmRefer 设置为 true。


ReferenceConfig#createProxy


if (isJvmRefer) {


URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);


invoker = refprotocol.refer(interfaceClass, url);


if (logger.isInfoEnabled()) {


logger.info("Using injvm service " + interfaceClass.getName());


}


}


Step2:如果消费者引用本地 JVM 中的服务,则利用 InjvmProtocol 创建 Invoker,dubbo 中的 invoker 主要负责服务调用的功能,是其核心实现,后续会在专门的章节中详细分析,在这里我们需要知道,会创建于协议相关的 Invoker 即可。


ReferenceConfig#createProxy


if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.


String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url); // @1


if (us != null && us.length > 0) {


for (String u : us) {


URL url = URL.valueOf(u);


if (url.getPath() == null || url.getPath().length() == 0) {


url = url.setPath(interfaceName);


}


if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) { // @2


urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));


} else {


urls.add(ClusterUtils.mergeUrl(url, map)); // @3


}


}


}


}


Step3:处理直连情况,与 step2 互斥。


代码 @1:对直连 URL 进行分割,多个直连 URL 用分号隔开,如果 URL 中不包含 path 属性,则为 URL 设置 path 属性为 interfaceName。


代码 @2:如果直连提供者的协议为 registry,则对 url 增加 refer 属性,其值为消息消费者所有的属性。(表示从注册中心发现服务提供者)


代码 @3:如果是其他协议提供者,则合并服务提供者与消息消费者的属性,并移除服务提供者默认属性。以 default 开头的属性。


ReferenceConfig#createProxy


List<URL> us = loadRegistries(false); // @1


if (us != null && !us.isEmpty()) {


for (URL u : us) {


URL monitorUrl = loadMonitor(u); // @2


if (monitorUrl != null) {


map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString())); // @3


}


urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map))); // @4


}


}


if (urls == null || urls.isEmpty()) {


throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry


address="..." /> to your spring config.");


}


Step4:普通消息消费者,从注册中心订阅服务。


代码 @1:获取所有注册中心 URL,其中参数 false 表示消费端,需要排除 dubbo:registry subscribe=false 的注册中心,其值为 false 表示不接受订阅。


代码 @2:根据注册中心 URL,构建监控中心 URL。


代码 @3:如果监控中心不为空,在注册中心 URL 后增加属性 monitor。


代码 @4:在注册中心 URL 中,追加属性 refer,其值为消费端的所有配置组成的 URL。


ReferenceConfig#createProxy


if (urls.size() == 1) {


invoker = refprotocol.refer(interfaceClass, urls.get(0)); // @1


} else {


List<Invoker<?>> invokers = new ArrayList<Invoker<?>>(); // @2,多个服务提供者 URL,集群模式


URL registryURL = null;


for (URL url : urls) {


invokers.add(refprotocol.refer(interfaceClass, url)); // @2


if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {


registryURL = url; // use last registry url


}


}


if (registryURL != null) { // registry url is available


// use AvailableCluster only when register's cluster is available

评论

发布
暂无评论
源码分析Dubbo服务消费端启动流程