🍎 Dubbo SPI 之 Adaptive 自适应类

翻看 Dubbo 的源码,不难发现,框架到处都在用 SPI 机制进行扩展。这是由于 Dubbo 框架对各种层做了很多的实现方式,然后由用户自己去选择具体的实现方式。比如 ProtocolDubbo 提供的实现就有 dubbohessianthriftredisinJvm 等等。这么多的实现方式,Dubbo 是如何做到动态的切换的呢?

接口自适应与 @Adaptive 注解

Dubbo 没有采用 JDK 提供的 SPI 机制,而是自己实现了一套,增强了很多功能。具体细节可以浏览网上其他博客。

Dubbo 充分利用面向对象思想,每一个组件内引入其他组件都是以接口的形式进行依赖,动态的 inject 实现类。所以这种思想上也用到了 AOPDI 的思想。
我们定义一个接口 Registry,假定它的功能是将本地服务暴露到注册中心,以及从注册中心获取可用服务,属于 RPC 框架中的服务注册与发现组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
@SPI("zookeeper")
public interface Registry {
/**
* 注册服务
*/
@Adaptive()
String register(URL url, String content);
/**
* 发现服务
*/
@Adaptive()
String discovery(URL url, String content);
}
  • @SPI 注解标注这是一个可扩展的组件,注解内的内容后文详细介绍。
  • 该接口定义了两个方法 registerdiscovery,分别代表注册服务和发现服务。两个方法中都有 URL 参数,该类是 Dubbo 内置的类,代表了 Dubbo 整个执行过程中的上下文信息,包括各类配置信息,参数等。content 代表备注信息。
  • @Adaptive()注解表明这两个方法都是自适应方法,具体作用后文分析。

下面分别是两个实现类 ZookeeperRegistryEtcdRegistry

ZookeeperRegistry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ZookeeperRegistry implements Registry {
private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);

@Override
public String register(URL url, String content) {
logger.info("服务: {} 已注册到zookeeper上,备注: {}", url.getParameter("service"), content);

return "Zookeeper register already! ";
}

@Override
public String discovery(URL url, String content) {
logger.info("zookeeper上发现服务: {} , 备注: {}", url.getParameter("service"), content);

return "Zookeeper discovery already! ";
}
}

EtcdRegistry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EtcdRegistry implements Registry {

private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);

@Override
public String register(URL url, String content) {
logger.info("服务: {} 已注册到 Etcd 上,备注: {}", url.getParameter("service"), content);

return "Etcd register already! ";
}

@Override
public String discovery(URL url, String content) {
logger.info("Etcd 上发现服务: {} , 备注: {}", url.getParameter("service"), content);

return "Etcd discovery already! ";
}
}

我们可以将服务注册信息注册到老牌注册中心 zookeeper 上,或者使用新兴流行轻量级注册中心 etcd 上。

配置扩展点实现信息到 Resource 目录下

Resource 下的 META-INF.dubbo 下新建 以Registry 全限定名为名的文件,配置实现类信息,以 key-value 的形式。(这里要注意与 JDK 默认的 SPI 机制的区别)
spi.png

文件内内容如下:

1
2
etcd=com.maple.spi.impl.EtcdRegistry
zookeeper=com.maple.spi.impl.ZookeeperRegistry

测试 Dubbo SPI 自适应类

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/test")
.addParameter("service", "helloService");

Registry registry = ExtensionLoader.getExtensionLoader(Registry.class)
.getAdaptiveExtension();
String register = registry.register(url, "maple");

System.out.println(register);
}
}

该程序首先通过 Registry 接口得到它专属的 ExtensionLoader 实例,然后调用 getAdaptiveExtension 拿到该接口的自适应类。Dubbo 会判断是否有实现类(即实现了 Registry 接口) 上有注解 @Adaptive,如果没有就会动态生成。本例子将会动态生成。

直接运行程序结果如下,程序最终选择的实现类是 ZookeeperRegistry,控制台结果如下:

1
2
09-20 23:43:13 323 main INFO - 服务: helloService 已注册到zookeeper上,备注: maple
Zookeeper register already!

上面代码我们没有看到任何实现类的信息,Dubbo SPI 机制会为动态的去调用实现类。

我们重点分析 getAdaptiveExtension方法找到的是 Registry 的自适应类,可以理解为是 Registry 的一个 适配器和代理类。如果该适配器类不存在,Dubbo 会通过动态代理方式在运行时自动生成一个自适应类。

打开 DEBUG 日志,在控制台我们看到了 Dubbo 生成的类的源码如下:

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
package com.maple.spi;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Registry$Adaptive implements com.maple.spi.Registry {
public java.lang.String register(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("registry", "zookeeper");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");
com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);
return extension.register(arg0, arg1);
}

public java.lang.String discovery(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("registry", "zookeeper");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");
com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);
return extension.discovery(arg0, arg1);
}
}

看代码,该适配器类的作用是类似于 AOP 的功能,再调用具体的实现类之前,先通ExtensionLoader.getExtensionLoader(Registry.class).getExtension(extName);
根据 extNameloader 具体的实现类,然后再去调用实现类的相应的方法。

分析上面代码中的一句:

1
String extName = url.getParameter("registry", "zookeeper");

extName 可以通过 url 进行传递,默认值为 zookeeper, 该默认值即为我们定义的接口上的注解 @SPI 里的内容,上文我们定义的 @SPI("zookeeper"),所以这里的默认值为 zookeeper, 当 url 中没有对应的参数时,我们会去拿默认值。

我们可以修改 Main 测试程序,增加 keyregistryparameter

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/test").addParameter("service", "helloService")
.addParameter("registry","etcd");

Registry registry = ExtensionLoader.getExtensionLoader(Registry.class).getAdaptiveExtension();

String register = registry.register(url, "maple");

System.out.println(register);

}

URL 中增加 Key,并设置值为 etcd,运行程序,结果如下:

1
2
09-20 23:44:00 009 main INFO - 服务: helloService 已注册到 Etcd 上,备注: maple
Etcd register already!

实现类已经切换为 EtcdRegistry 了。

@Adaptive 注意细节

@Adaptive 源码

1
2
3
public @interface Adaptive {
String[] value() default {};
}

细心的读者已经发现了 @Adaptivevalue这个属性。上文在接口方法上定义的 @Adaptive 是没有设置值的。如果没有定义值,Dubbo 默认会使用一种策略生成。这种策略是将类名定义的驼峰法则转换为小写,并以 .号区分。 例如上文的接口名为 Registry,那么这个Key 值就是 registry。如果接口名为 HelloWorldKey 值就为 hello.world

当然如果 @Adaptive 是有值的话,优先按里面的这个值来作为 Key,例如 Dubbo 框架中的接口 RegistryFactory ,该接口的自适应类将会从 URLprotocolkey 来找实现类的 extName

1
2
3
4
5
@SPI("dubbo")
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}

总结

Dubbo Adaptive 模式在整个框架中运用十分广泛,如果用户没有在 URL 中进行自定义,Dubbo 默认会去加载扩展点接口上 @SPI 标注的内容,如果此注解没有值,那么我们就必须要在 URL 中进行值的传递了。如果我们想覆盖 Dubbo 默认的实现策略。可以通过在 URL 中增加 key-value 的形式来改变。

这样一个组件充分体现了设计模式,对修改关闭,对扩展开放的原则。当我们想自己实现 Dubbo 中的某个组件时,我们完全可以通过 Dubbo Adaptive 来动态的切换程序使用我们提供的组件。

彻底理解 Dubbo SPI 模式,以及 Adaptive 自适应类, Activate 激活类等,是看 Dubbo 源码的基础。如果我们想十分流畅的去分析 Dubbo 内部其他组件的实现机制,第一道要跨过的坎便是 Dubbo SPI

🍎 Dubbo实战 整合 SpringBoot 实现自动装配

如今 Spring Boot 在微服务领域已十分风靡,开箱即用的特性,简化了很多开发工作。而 Dubbo2017 年重新得到维护以后,社区逐渐活跃,Dubbo RPC 十分优秀,本文我们将通过一个例子来构建一个基于 Spring BootDubbo 微服务工程。

Dubbo Spring Boot Starter 项目

我们在采用 Dubbo 作为 微服务框架时,可以通过依赖这个项目来轻松整合 SpringBoot

dubbo-spring-boot.png

上面是 Dubbo Spring Boot 工程地址 。Dubbo Spring Boot 工程致力于简化 Dubbo RPC 框架在 Spring Boot 应用场景的开发。同时也整合了 Spring Boot 特性:

SpringBoot Dubbo 项目实战

我们将实现一个简单的基于SpringBoot 和 Dubbo 的微服务例子来进行讲解。
项目采用多模块的形式打包。分为如下三个工程:

  • dubbo-boot-api
    统一使用的API工程,提供给生产者和消费者,包括服务接口等,实体模型等。
  • dubbo-boot-consumer
    服务消费端,该工程将会提供web服务,并调用 Dubbo 提供的微服务。
  • dubbo-boot-provider
    服务提供方,提供服务供消费者进行调用

POM 依赖

项目根 POM 文件 需要依赖 SpringBoot 的 Parent 工程 作为 父工程以方便管理Spring的版本,我们采用的版本号为 Spring Boot 2.0.0.RELEASE

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>

服务提供者 (dubbo-boot-provider) 、服务消费者 (dubbo-boot-consumer) 都需要依赖 dubbo 整合 Springdubbo-spring-boot-starter工程,以及 spring-boot-starter-web,通过 web 作为服务启动运行载体。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Dubbo 整合 Boot Dubbo 依赖 -->
<dependency>
<groupId>com.alibaba.spring.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>

<!-- Spring Boot Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

API 工程 dubbo-boot-api

api工程主要提供实体类和公用服务接口, providerconsumer 都会依赖它

实体类 City

1
2
3
public class City implements Serializable {
private String name;
private String from;

服务接口 CityService

1
2
3
4
5
6
7
8
public interface CityService {
/**
* 根据城市名称,查询城市信息
*
* @param cityName
*/
City findCityByName(String cityName);
}

Provider 服务提供工程 dubbo-boot-provider

服务提供实现 CityServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service(
version = "${demo.service.version}",
application = "${dubbo.application.id}",
protocol = "${dubbo.protocol.id}",
registry = "${dubbo.registry.id}"
)
public class CityServiceImpl implements CityService {

public City findCityByName(String cityName) {
System.out.println("request cityName: " + cityName);
return new City("武汉", "湖北");
}
}

启动类 AppProvider

使用非 web 的形式启动 SpringBoot 容器,提供 Dubbo Rpc 服务

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableDubboConfiguration
public class App {
public static void main(String[] args) {
//使用非 Web 环境启动 Spring容器,提供dubbo rpc 服务
new SpringApplicationBuilder().sources(AppProvider.class)
.web(WebApplicationType.NONE)
.run(args);
}
}

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Spring boot application
spring.application.name = dubbo-provider-demo

## Provider类配置
demo.service.version=1.0.0
dubbo.application.id = dubbo-boot-provider
dubbo.application.name = dubbo-boot-provider

## 使用通讯协议、暴露端口
dubbo.protocol.id = dubbo
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20880

## 注册中心 (不使用,直接本地互连)
dubbo.registry.id = my-registry
dubbo.registry.address = N/A

Consumer 消费者工程 dubbo-boot-provider

Spring Boot Controller 类,提供 Rest 接口。引用 Dubbo 服务接口 CityService

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class HelloController {

@Reference(version = "${demo.service.version}",
application = "${dubbo.application.id}",
url = "${dubbo.service.url}")
private CityService cityService;

@RequestMapping("/hello")
public Object hello() {
return cityService.findCityByName("武汉");
}
}

启动类

提供 web 服务供前端调用

1
2
3
4
5
6
7
@SpringBootApplication
public class AppConsumerServer {

public static void main(String[] args) {
SpringApplication.run(AppConsumerServer.class, args);
}
}

application.properties

本例子没有采用注册中心,而是通过本地 url 互相直连服务端进行调用。

1
2
3
4
5
6
7
8
9
spring.application.name=dubbo-boot-consumer

demo.service.version=1.0.0

dubbo.application.id=dubbo-boot-consumer
dubbo.application.name=dubbo-boot-consumer

# 通过直连方式
dubbo.service.url=dubbo://127.0.0.1:20880

测试

分别运行启动服务端(AppServer) 和消费端(AppConsumerServer) 类。然后打开浏览器访问 localhost:8080/hello
调用结果.png

结果显示已经调用成功。整个执行链为,前端访问 url 调用消费端 Spring Rest接口,Controller 里通过 Dubbo Rpc 调用 服务端提供的服务,然后返回。

总结

采用 Spring Boot 工程,引入 Dubbo 整合 Spring Boot 的 依赖,将会使我们非常简单轻松的使用 Dubbo 来提供微服务。我们可以充分利用 Spring Boot 的 注解驱动、自动装配、外部化配置、Actuator 监控等特性,来轻松管理基于Dubbo 的微服务。

社区提供的 dubbo-spring-boot-starter 工程 整合了与 Spring Boot 这些自动配置、依赖等。所以下一步我们将会研究 它是如何实现与 Spring Boot 无缝整合的。

推荐

最后推荐一下本人微信公众号,欢迎大家关注。

image.png

Spring实战:彻底理解FactoryBean

在强调 面向接口编程的同时,
有一点需要注意:虽然对象可以通过声明接口来避免对特定接口实现类的过度耦合,
但总归需要一种方式将声明依赖接口的对象与接口实现类关联起来。否则,只依赖一个不做任何事情的接口是没有任何用处的。

Spring:基于反射启动Spring容器

什么场景下需要反射来启动Spring容器?在 dapeng-soa 中就有这样的需求。
一般是类似于 tomcat 容器这种模式,容器本身并没有包含 spring 的代码,而是去加载含有 spring 的业务fatJar 时,才进行启动,
此时就需要使用反射启动 spring 容器。

RocketMQ(一):环境搭建

RocketMQ 是一款分布式、队列模型的消息中间件,具有以下特点:
能够保证严格的消息顺序,提供丰富的消息拉取模式,高效的订阅者水平扩展能力,实时的消息订阅机制,亿级消息堆积能力,