:::warning 持续更新中…… :::
引言
当我们现在尝试打开 dubbo-spring-boot-project 项目,会发现如下公告:从 3.0 开始代码已经被移动到 apache/dubbo 核心项目了。
这也从侧面证明了 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
内实现。
几者之间的抽象关系如下:
其中 classpath:/META-INF/spring.factories
用于告知 SpringBoot,当前 autoconfigure
模块的入口在哪里,是整个 starter
运行的起点,常见的配置如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
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
开始:
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
的注入流程可抽象概括为下图:
服务暴露桥接:@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 与 Spring 生命周期绑定关系
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:以上只是个人的分析,不代表官方观点。
参考资料
- Spring Boot Reference Documentation - Spring official |(https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)
- Spring @Import Annotation - baeldung|(https://www.baeldung.com/spring-import-annotation)
- Spring – Bean Life Cycle|(https://howtodoinjava.com/spring-core/spring-bean-life-cycle/#1)