:::warning 持续更新中…… :::

引言

当我们现在尝试打开 dubbo-spring-boot-project 项目,会发现如下公告:从 3.0 开始代码已经被移动到 apache/dubbo 核心项目了。
image.png
这也从侧面证明了 Spring 与 Dubbo 的深度绑定关系,以及绝大多数用户的使用场景是在 Spring 内引入 Dubbo 依赖。今天,我们就来捋下之间的关系。

什么是 spring-boot-starter?

starter 用于三方类库与 Spring 的整合,避免用户进行繁杂的配置,快速进入开发阶段。
可以通过命名来判定该 starter 是官方维护的,还是三方维护的,约定如下:

  • 官方包命名允许 spring-boot-starter-* 模式,* 号处可以填写具体的应用命,例如 spring-boot-starter-web
  • 用户自定义 Starter 的时候,遵循 *-spring-boot-starter 模式,例如上文提到的 dubbo-spring-boot-starter

结构上,一个自定义 starter 通常包含如下部分,

  • 一个 autoconfigure 模块,用于解决自动配置问题,也是核心所在
  • 一个 starter 包含 autoconfigure 等三方库初始化所必要的依赖,通常只包含一个 POM 文件

此外 autoconfigure 可按照需求,灵活决定是否需要单独做成模块,或直接在 starter 内实现。

几者之间的抽象关系如下:
image.png
其中 classpath:/META-INF/spring.factories 用于告知 SpringBoot,当前 autoconfigure 模块的入口在哪里,是整个 starter 运行的起点,常见的配置如下:

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2. com.baeldung.greeter.autoconfigure.GreeterAutoConfiguration

如果你觉得上述概念抽象的话,可以看下 spring-boot-custom-starter - baeldung 示例,同步配有博文。

如何在 Spring Boot 中引入 Dubbo?

当我们去翻阅 apache/dubbo-samples 实例项目的时候,最常见的引入方式如下:

<dependencyManagement>
  <dependencies>
    <!-- Spring 依赖管理 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <!-- Dubbo 依赖管理,由于 dubbo-bom 也包含 Spring 版本定义,所以可能会与 spring-boot-starter 的定义发生冲突 -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-bom</artifactId>
      <version>${dubbo.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <!-- 引入 Spring starter -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <!-- 引入 Dubbo starter -->
  <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
  </dependency>
</dependencies>

但也会看到在 <dependencies> 部分没有引入 dubbo starter(包含 dubbo),而只引入了 dubbo 的,此时需要配合 @EnableDubbo 注解使用。

<dependencies>
  <!-- 引入 Spring starter -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <!-- 引入 Dubbo starter -->
  <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
  </dependency>
</dependencies>

@EnableDubbo 注解是什么?

这也是第二种引用方式一开始最使我困惑的地方,因为当我尝试去寻找 spring.factories 时候,没有找到相关配置,那么 Spring 是如何知道 Dubbo 存在的呢?我们尝试跟踪 @EnableDubbo 源码,其中 @Import 注解成功引起了我们的注意,答案可能就在这里。

@EnableDubboConfig
@DubboComponentScan
public @interface EnableDubbo {
    // ......
}

@Import(DubboConfigConfigurationRegistrar.class) // <======= HERE
public @interface EnableDubboConfig {
    // ......
}

@Import(DubboComponentScanRegistrar.class)  // <======= HERE
public @interface DubboComponentScan {
    // ......
}

查阅文档得,@Import@ComponentScan 作用类似,都是为了告知 Spring 你应该去加载哪些 Bean,但两者也有区别:

  • @Import 支持按需加载特定的配置类,支持分组,更像是一些精细的个性化“配置”。
  • @ComponentScan 是对特性路径下的配置进行扫描,是批量化操作,是通用化的 “约定”

所以,@EnableDubbo 启用 Dubbo 的原理是告知 Spring Boot:“Hi,请加载我的这些配置类,我就能把我的服务信息注册到你的上下文了!”

Dubbo 向 Spring 注册了什么?

从功能上来看,DubboConfigConfigurationRegistrar 其实是多余的,见 apache/dubbo#8633 ,所以我们直接从 DubboComponentScanRegistrar 开始: 「Dubbo」3.0 Dubbo in Spring Boot - 图3

DubboBeanUtils#registerCommonBeans

在方法内部调用 registerInfrastructureBean 方法,向 BeanDefinitionRegistry(实际运行时是 DefaultListableBeanFactory 实例)注册类型为 ROLE_INFRASTRUCTURE 的 Bean Definition,表示该 Bean 属于基础设施,前台用户无感知。

Role hint indicating that a BeanDefinition is providing an entirely background role and has no relevance to the end-user

涉及到如下关键类:

类名 Side 作用
ServicePackagesHolder Provider 用于临时存放扫描到的 Service 服务
ReferenceBeanManager Consumer 用于服务引用管理,是否通用待考究
ReferenceAnnotationBeanPostProcessor Consumer 用户 @DubboReference 注解的处理,是否通用待考究
DubboBootstrapApplicationListener All 监听 Spring 生命周期事件,触发相关联操作
DubboConfigDefaultPropertyValueBeanPostProcessor Bean 默认属性配置
DubboConfigBeanInitializer
DubboInfraBeanRegisterPostProcessor

registerServiceAnnotationPostProcessor

先看下上下文,非常简洁:

// => org.apache.dubbo.config.spring.context.annotation.DubboComponentScanRegistrar
public class DubboComponentScanRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        // @since 2.7.6 Register the common beans
        registerCommonBeans(registry);

        // 获取需要扫描的 package 路径,可以通过 @EnableDubbo 的 scanBasePackages 和 scanBasePackageClasses 属性进行配置
        // 但在服务引用的时候,和此处配置似乎没有关系
        Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);

        // HERE
        registerServiceAnnotationPostProcessor(packagesToScan, registry);
    }
}

ServiceAnnotationPostProcessor 与 ReferenceAnnotationBeanPostProcessor 相对应,用于处理 @DubboService 注解,两者都是 BeanFactoryPostProcessor 处理器。

临门一脚: @DubboReference 字段注入

Spring 会在给 Bean 实例的属性字段正式设值之前,触发
ReferenceAnnotationBeanPostProcessor#postProcessPropertyValues 回调,在这个方法里面,Dubbo 有机会对 Bean 实例中被 @DubboReference 标记的字段进行提前注入。

// => org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#postProcessPropertyValues
public PropertyValues postProcessPropertyValues(
    PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

    try {
        // 1. 寻找 @DubboReference 标记的字段,并放置到 metadata 中
        AnnotatedInjectionMetadata metadata = findInjectionMetadata(beanName, bean.getClass(), pvs);
        // 2. 将 injectedObject 设置为注入字段属性名(字符串类型)
        prepareInjection(metadata);

        // 其实 1、2 步要做的事情,在之前已经准备好了(见附录:Dubbo 与 Spring 生命周期绑定关系),这里只是查漏补缺

        // 重写了 Spring 的 InjectedElement.inject 方法
        metadata.inject(bean, beanName, pvs);
    } catch (BeansException ex) {
        throw ex;
    } catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of @" + getAnnotationType().getSimpleName()
                                        + " dependencies is failed", ex);
    }
    return pvs;
}

最后的 metadata.inject 方法牵扯出一个新的疑问,请看下面代码,此处的 beanName 为 demoService,但 HelloService 只是一个接口,是无法进行 Bean 实例化的。我们进行代码跟踪的时候,发现此处 demoService 指向的是类型为 ReferenceBean 的实例 Bean,那么 ReferenceBean 又是怎么来的呢?

@Service
public class ServiceA {
    @DubboReference(version = "1.0.0")
    private HelloService demoService;
}

进入 metadata.inject 方法:

// => org.apache.dubbo.config.spring.beans.factory.annotation.AbstractAnnotationBeanPostProcessor.AnnotatedInjectElement#inject
protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
    // 此处 attributes 是 @DubboReference 注解上的参数,例如 version、interface、group 等参数信息
    // 该方法会进行 ReferenceBean 的实例化,最终会调用到 Spring 的 beanFactory.getBean("demoService") 上来。
    Object injectedObject = getInjectedObject(attributes, bean, beanName, getInjectedType(), this);

    // 支持字段注入
    if (member instanceof Field) {
        Field field = (Field) member;
        ReflectionUtils.makeAccessible(field);
        // 设置值
        field.set(bean, injectedObject);
    } else if (member instanceof Method) {
        // 支持方法注入
        Method method = (Method) member;
        ReflectionUtils.makeAccessible(method);
        method.invoke(bean, injectedObject);
    }
}

现在问题来到,在 Spring 的 beanFactory 内 demoService 是如何映射到 ReferenceBean 上来的?在这之前,肯定是提前修改了相应的 BeanDefination,答案在之前已经出现过的 prepareInjection 方法里,存在如下调用链:prepareInjection -> registerReferenceBean

// => org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#registerReferenceBean
public String registerReferenceBean(String propertyName, Class<?> injectedType, Map<String, Object> attributes, Member member) throws BeansException {
    // ......
    // 三元组限制,Dubbo 本身也做了重复检测
    // TODO Only register one reference bean for same (group, interface, version)

    // Register the reference bean definition to the beanFactory
    RootBeanDefinition beanDefinition = new RootBeanDefinition();
    beanDefinition.setBeanClassName(ReferenceBean.class.getName());
    beanDefinition.getPropertyValues().add(ReferenceAttributes.ID, referenceBeanName);

    // set attribute instead of property values
    beanDefinition.setAttribute(Constants.REFERENCE_PROPS, attributes);
    beanDefinition.setAttribute(ReferenceAttributes.INTERFACE_CLASS, interfaceClass);
    beanDefinition.setAttribute(ReferenceAttributes.INTERFACE_NAME, interfaceName);
    // ......

    // 向 Spring BeanFactory 新增 demoService 的 BeanDefinition 定义
    beanDefinitionRegistry.registerBeanDefinition(referenceBeanName, beanDefinition);

    // ......
}

常见面试题:BeanFactory 和 FactoryBean 的区别,在这里都用到了

继续,ReferenceBean 是一个 FactoryBean,它接管了 Bean 的创建过程,Spring 会尝试通过调用 FactoryBean#getObject 来获取 Bean 实例对象,那么在 ReferenceBean#getObject 里面能做的事情就很多了。

// => org.apache.dubbo.config.spring.ReferenceBean#getObject
public T getObject() {
    if (lazyProxy == null) {
        // 通过 Proxy 技术来生成动态 Invoker,更多的 Dubbo 自身的服务引用逻辑了,这里不作展开
        createLazyProxy();
    }
    return (T) lazyProxy;
}

@DubboReference 的注入流程可抽象概括为下图: 「Dubbo」3.0 Dubbo in Spring Boot - 图4

服务暴露桥接:@DubboService 注解

相比于服务引用流程,服务暴露的连接过程,就简单很多了,先看个简单的示例:

@SpringBootApplication
// 告诉 Dubbo 该去哪里寻找 DubboReference 定义
// 假如该目录已经在 Spring 扫描目录里了,那无需重复配置
// 默认扫描启动类所在目录以及子目录
@EnableDubbo(scanBasePackages = {"org.apache.dubbo.spring.boot.provider.impl"})
public class ProviderApplication {

    public static void main(String[] args) throws Exception {

        SpringApplication.run(ProviderApplication.class, args);

        System.out.println("dubbo service started");
        new CountDownLatch(1).await();
    }

}

再看下服务实现,只需要标记 @DubboService,不再需要重复标记 @Service

@DubboService(version = "1.0.0")
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        throw new RuntimeException("Exception to show hystrix enabled.");
    }
}

服务暴露桥接的启动流程在 ServiceAnnotationPostProcessor#postProcessBeanDefinitionRegistry 内,该函数会进行目录扫描,并向 Spring 注册相关 BeanDefinition:

// => org.apache.dubbo.config.spring.beans.factory.annotation.ServiceAnnotationPostProcessor#scanServiceBeans
// 从指定目录加载 BeanDefinition 的逻辑主要在这里
private void scanServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

    // DubboClassPathBeanDefinitionScanner 继承自 Spring 的 ClassPathBeanDefinitionScanner
    DubboClassPathBeanDefinitionScanner scanner =
        new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

    // ......

    for (Class<? extends Annotation> annotationType : serviceAnnotationTypes) {
        // 添加注解过滤器,这里的要求是类必须被 @DubboService 标注
        // AnnotationTypeFilter 也是 Spring 提供的
        scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
    }

    // ......

    // 扫描并注册符合条件的 BeanDefinition,之后就可以通过 Spring 的 @Autowired 注解进行字段注入了。
    // Registers @Service Bean first
    scanner.scan(packageToScan);

    // ......

    for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
        // 从 Service BeanDefinition 生成 ServiceBean
        processScannedBeanDefinition(beanDefinitionHolder, registry, scanner);
    }

    // ......
}

与 ReferenceBean 相呼应,但 ServiceBean 不是个 FactoryBean,因为在 Provider 侧,如果 Servcie 之间有引用需求的话,可以直接走函数引用,不需要走 RPC 代理,但我们依旧需要触发服务暴露的逻辑:

// => org.apache.dubbo.config.spring.beans.factory.annotation.ServiceAnnotationPostProcessor#processScannedBeanDefinition
private void processScannedBeanDefinition(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry,
                                          DubboClassPathBeanDefinitionScanner scanner) {
    // 获取原始实现类,此处是 HelloServiceImpl
    Class<?> beanClass = resolveClass(beanDefinitionHolder);

    // 获取 DubboService 属性配置 
    Annotation service = findServiceAnnotation(beanClass);

    // The attributes of @Service annotation
    Map<String, Object> serviceAnnotationAttributes = AnnotationUtils.getAttributes(service, true);

    // 获取实现的基类名
    String serviceInterface = resolveInterfaceName(serviceAnnotationAttributes, beanClass);

    String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

    // ServiceBean Bean name
    String beanName = generateServiceBeanName(serviceAnnotationAttributes, serviceInterface);

    AbstractBeanDefinition serviceBeanDefinition =
        buildServiceBeanDefinition(serviceAnnotationAttributes, serviceInterface, annotatedServiceBeanName);

    // 没有覆盖覆盖原始的 BeanDifinition 定义,annotatedServiceBeanName != beanName
    // beanName 示例如下:ServiceBean:org.apache.dubbo.spring.boot.api.HelloService:1.0.0
    // 新增 ServcieBean 定义,
    registerServiceBeanDefinition(beanName, serviceBeanDefinition, serviceInterface);
}

最后看下 ServiceBean 是如何进行服务暴露的:

// => org.apache.dubbo.config.spring.ServiceBean
// 继承自Dubbo ServiceConfig,ServcieConfig#export 方法可用于服务暴露
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, BeanNameAware, ApplicationEventPublisherAware {
    @Override
    public void afterPropertiesSet() throws Exception {
        if (StringUtils.isEmpty(getPath())) {
            if (StringUtils.isNotEmpty(getInterface())) {
                setPath(getInterface());
            }
        }
        // 熟悉的服务注册,接下来就由 Dubbo 接管整个服务暴露流程了
        //register service bean and set bootstrap
        DubboBootstrap.getInstance().service(this);

        // 后续多实例特性会对 DubboBootstrap.getInstance() 出现较大改动
    }
}

同样,我们总结抽象服务暴露桥接流程如下图: 「Dubbo」3.0 Dubbo in Spring Boot - 图5

附录:Dubbo 与 Spring 生命周期绑定关系

image.png

ReferenceBeanManager    
    ApplicationContextAware

ReferenceAnnotationBeanPostProcessor
    ApplicationContextAware
    BeanFactoryPostProcessor

DubboBootstrapApplicationListener
    ApplicationListener
    ApplicationContextAware
    Ordered - 指定的是最低优先级

DubboConfigDefaultPropertyValueBeanPostProcessor - 所以该类其实也废弃了
    MergedBeanDefinitionPostProcessor - 实际上没有使用

DubboConfigBeanInitializer
    BeanFactoryAware 
    InitializingBean

DubboInfraBeanRegisterPostProcessor
    BeanDefinitionRegistryPostProcessor
    ApplicationContextAware
Dubbo 侧 事件类(按触发顺序排列) 备注
ApplicationListener#onApplicationEvent 作用:
典型观察者模式,用于事件监听

Provider 侧:
ServiceAnnotationPostProcessor#postProcessBeanDefinitionRegistry

扫描路径下被 @DubboRefefence 注解标记的类,进行 BeanDefinition 注册
BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 阶段:> before the next post-processing phase kicks in

在 BeanFactoryPostProcessor 之前?

作用:
可以修改 BeanDefinition 定义 | | | ApplicationContextAware#setApplicationContext | 阶段:


作用:
获取 ApplicationContext 上下文 | | | BeanFactoryAware#setBeanFactory | 作用:
获取当前 Bean 归属的 BeanFactory | | Consumer 侧:

对应:
ReferenceAnnotationBeanPostProcessor#postProcessBeanFactory
提前进行 metadata 的批量生成


DubboInfraBeanRegisterPostProcessor#postProcessBeanFactory
| BeanFactoryPostProcessor#postProcessBeanFactory | 阶段:
在所有 BeanDefinition 已经加载完毕,但还未实例化的阶段

作用:
可以修改 BeanDefinition 定义 | | | InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation | 阶段:> before-instantiation

在进行实例化之前

作用:
| | | BeanPostProcessor#postProcessBeforeInitialization
� | 阶段:
在 Bean 初始化之前,此时 Bean 已经实例化

作用:> populate beans via marker interfaces

依据标记接口指定的逻辑,返回新的实例,相当于覆盖操作 | | | InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation | 阶段:> after instantiation but before explicit properties are set or autowiring occurs

实例化完成后,属性值设置之前

作用:
可以更改替换属性值 | | Consumer 侧:

对应:
�ReferenceAnnotationBeanPostProcessor#postProcessPropertyValues

获取 metadata,对 Spring Bean 内被
@DubboReference 标记的字段进行注入 | InstantiationAwareBeanPostProcessor#postProcessPropertyValues | 阶段:> Post-process the given property values before the factory applies them to the given bean



作用:
| | | InitializingBean#afterPropertiesSet | 阶段:> Invoked by the containing BeanFactory after it has set all bean properties

此时 Bean 已经实例化

作用:
可用于校验配置属性合法性等
| | | BeanPostProcessor#postProcessBeforeInitialization
� | 阶段:
在 Bean 初始化之后

作用:> wrap beans with proxies

原实例被代理实例包装了起来,相当于增强而不是覆盖 |

题外:为什么 Dubbo 需要多实例?

来看一个服务引用配置,在三元组(group、interface、version)相同的情况下,demoService 和 demoService1 其实指向的是同一个引用对象,我们不同的 timeout 配置其实是无效的。在测试的 3.0.2 版本中,会抛出 Found multiple ReferenceConfigs with unique service name 异常。

@DubboReference(version = "1.0.0", group = "default", timeout = 1000)
private HelloService demoService;

@DubboReference(version = "1.0.0", group = "default", timeout = 500)
private HelloService demoService1;

对于这个问题的解决,通常我们会想到,通过把三元组因子范围扩大来实现不同的 timeout 配置,但 DubboReference 有几十个配置,难道我们需要把所有配置项都作为确定引用唯一性的因子来计算吗?再则,随着因子的扩大,引用缓存的命中率的将会出现急剧的下降,进而拖垮 RPC 服务框架的性能。

另外一种思路是,三元组依旧不变,将其余配置作为参数在调用过程中传递。这种思路对于 timeout 这种,只影响最终调用的参数,是一种可行的思路。但碰到像 proxy(使用 jdk 还是 javassist 生成动态代理)这种影响底层公共依赖的参数,就显得力不从心了。更不用说,需要对远端服务引用进行域隔离这种需求了。

综上,Dubbo 团队最后考虑了多实例的支撑方案,在架构底层进行最彻底的隔离。当然多实例特性不单单是为了解决这个问题,例如还考虑了优化由于 static 特性使用过多导致的架构耦合和测试苦难等问题。

PS:以上只是个人的分析,不代表官方观点。

参考资料