Poison

关于 Dubbo 2.7.5 中通过 Dubbo @Service 注解配置的服务分组只能注册首个服务实现的问题

业务应用报错找不到 Dubbo 服务提供者,简单排查了下,确认服务提供方没有注册上该服务,查了下代码,发现该接口有多个实现,每个实现都是 Dubbo 的服务提供者,通过 Dubbo 的 服务分组 进行区分。

从服务提供方的启动日志中可以看到有如下输出:

1
2021-12-06 22:32:48,432 WARN    org.apache.dubbo.config.context.ConfigManager:492 -  [DUBBO] Duplicate ServiceBean found, there already has one default ServiceBean or more than two ServiceBeans have the same id, you can try to give each ServiceBean a different id : <dubbo:service beanName="ServiceBean:me.tianshuang.service.TestService:1.0:group1" unexported="false" exported="false" ref="me.tianshuang.service.impl.TestServiceImpl@6ff7b3d5" interface="me.tianshuang.service.TestService" uniqueServiceName="group1/me.tianshuang.service.TestService:1.0" prefix="dubbo.service.me.tianshuang.service.TestService" deprecated="false" group="group1" dynamic="true" version="1.0" id="me.tianshuang.service.TestService" valid="true" />, dubbo version: 2.7.5, current host: 192.168.1.9

相同的问题在 GitHub 上可以看到已经有其他用户反馈:Dubbo 2.7.5: Duplicate ServiceBean found · Issue #5923,表现与我们本地测试的一致。上面日志输出的源码位于 ConfigManager.java at dubbo-2.7.5,对应的源码为:

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
26
27
static <C extends AbstractConfig> void addIfAbsent(C config, Map<String, C> configsMap, boolean unique)
throws IllegalStateException {

if (config == null || configsMap == null) {
return;
}

if (unique) { // check duplicate
configsMap.values().forEach(c -> {
checkDuplicate(c, config);
});
}

String key = getId(config);

C existedConfig = configsMap.get(key);

if (existedConfig != null && !config.equals(existedConfig)) {
if (logger.isWarnEnabled()) {
String type = config.getClass().getSimpleName();
logger.warn(String.format("Duplicate %s found, there already has one default %s or more than two %ss have the same id, " +
"you can try to give each %s a different id : %s", type, type, type, type, config));
}
} else {
configsMap.put(key, config);
}
}

查询调用栈帧可知由 InitDestroyAnnotationBeanPostProcessor 对该 Dubbo 服务对应的 ServiceBean@PostConstruct 注解标记的 addIntoConfigManager() 方法处理时触发,源码位于 AbstractConfig.java at dubbo-2.7.5

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Add {@link AbstractConfig instance} into {@link ConfigManager}
* <p>
* Current method will invoked by Spring or Java EE container automatically, or should be triggered manually.
*
* @see ConfigManager#addConfig(AbstractConfig)
* @since 2.7.5
*/
@PostConstruct
public void addIntoConfigManager() {
ApplicationModel.getConfigManager().addConfig(this);
}

根据上面的日志及源码我们知道,当服务有多个实现时,即在 Dubbo 2.7.5 中使用注解配置的服务分组时,首个服务实现之后的服务实现因为 getId(config) 方法返回的 key 与之前的服务实现返回的 key 相同,导致不会被加入 configsMap,从而导致后续的服务导出不会将该服务实现进行导出。其中服务导出的源码位于 DubboBootstrap.java at dubbo-2.7.5,因为服务实现没有被加入至 configsMap,从而使服务导出调用的 configManager.getServices() 返回的集合中没有包含我们需要导出的服务实现,最终造成消费者找不到服务提供者。

该问题在 GitHub 上有多个 issue 反馈,其中 2.7.5和2.7.6的group和version同一个接口只能注册一个服务 · Issue #6056 这个 issue 中提到:

我也发现了 使用注解是不行的 xml配置可以的

于是我在本地进行了测试,发现果然 XML 配置是可以正常进行服务导出的,而通过 Dubbo @Service 注解配置无法导出,在本地进行源码调试后,发现差异在于对 ServiceBeanid 属性的设置上。其中 XML 配置时,如果用户在 <dubbo:service /> 中配置了 id,那么会使用用户显式指定的 id 作为 ServiceBeanid 属性值,如果用户没有显式指定 id, 那么会依次尝试使用配置的 nameinterface 去作为 id,且在此过程中,如果 id 值与之前的 id 值有重复,那么会将当前 id 后面追加数字以避免出现重复的 id,该段处理逻辑位于 DubboBeanDefinitionParser.java at dubbo-2.7.5

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
26
27
28
29
30
31
32
33
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClass(beanClass);
beanDefinition.setLazyInit(false);
String id = element.getAttribute("id");
if (StringUtils.isEmpty(id) && required) {
String generatedBeanName = element.getAttribute("name");
if (StringUtils.isEmpty(generatedBeanName)) {
if (ProtocolConfig.class.equals(beanClass)) {
generatedBeanName = "dubbo";
} else {
generatedBeanName = element.getAttribute("interface");
}
}
if (StringUtils.isEmpty(generatedBeanName)) {
generatedBeanName = beanClass.getName();
}
id = generatedBeanName;
int counter = 2;
while (parserContext.getRegistry().containsBeanDefinition(id)) {
id = generatedBeanName + (counter++);
}
}
if (StringUtils.isNotEmpty(id)) {
if (parserContext.getRegistry().containsBeanDefinition(id)) {
throw new IllegalStateException("Duplicate spring bean id " + id);
}
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
beanDefinition.getPropertyValues().addPropertyValue("id", id);
}

// omitted
}

即读取并设置不重复的 id 并设置至 beanDefinition 中,在后续实例化 ServiceBean 时会将该此处生成的 id 值设置至 ServiceBean 实例中,保证了服务的正常发布,这就是 issue 中有用户反馈 XML 配置可以正常使用服务分组的实现机制。

而当使用 Dubbo 的 @Service 注解配置服务实现时,在 @Service 的属性中,是没有提供 id 属性进行配置的,所以通过 @Service 配置 Dubbo 服务时,该 ServiceBeanid 属性值无法由用户显式指定,而是在实例化 ServiceBean 时在 setInterface(String interfaceName) 方法中被设置,其源码位于 ServiceConfigBase.java at dubbo-2.7.5

1
2
3
4
5
6
public void setInterface(String interfaceName) {
this.interfaceName = interfaceName;
if (StringUtils.isEmpty(id)) {
id = interfaceName;
}
}

可以看出,使用的接口名作为 ServiceBean 实例的 id 值,这也解释了我们一开始遇到的问题,当同一接口有多个实现且它们都通过 Dubbo @Service 注解配置,此时它们的 ServiceBean 具有相同的 id 值,导致只有该接口的第一个实现被加入到 configsMap,也只有第一个实现进行了服务导出,后续的实现都没有被导出,最终导致了消费端的无提供者异常。

继续在 GitHub 的 issue 列表中搜索,可以看到在其中一个 issue 中作者给出了一个紧急的解决方案,该 issue 为:Dubbo 2.7.5 服务分组,provider服务接口提供多个group,注册中心只看到一个 · Issue #5779,其中介绍了一个类:DubboConfigDefaultPropertyValueBeanPostProcessor.java at master,其源码如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* The {@link BeanPostProcessor} class for the default property value of {@link AbstractConfig Dubbo's Config Beans}
*
* @since 2.7.6
*/
public class DubboConfigDefaultPropertyValueBeanPostProcessor extends GenericBeanPostProcessorAdapter<AbstractConfig>
implements MergedBeanDefinitionPostProcessor, PriorityOrdered {

/**
* The bean name of {@link DubboConfigDefaultPropertyValueBeanPostProcessor}
*/
public static final String BEAN_NAME = "dubboConfigDefaultPropertyValueBeanPostProcessor";

protected void processBeforeInitialization(AbstractConfig dubboConfigBean, String beanName) throws BeansException {
// [Feature] https://github.com/apache/dubbo/issues/5721
setBeanNameAsDefaultValue(dubboConfigBean, "id", beanName);
if (dubboConfigBean instanceof ProtocolConfig) {
ProtocolConfig config = (ProtocolConfig) dubboConfigBean;
if (StringUtils.isEmpty(config.getName())) {
config.setName("dubbo");
}
} else {
setBeanNameAsDefaultValue(dubboConfigBean, "name", beanName);
}
}

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// DO NOTHING
}

protected void setBeanNameAsDefaultValue(Object bean, String propertyName, String beanName) {

Class<?> beanClass = getTargetClass(bean);

PropertyDescriptor propertyDescriptor = getPropertyDescriptor(beanClass, propertyName);

if (propertyDescriptor != null) { // the property is present

Method getterMethod = propertyDescriptor.getReadMethod();

if (getterMethod == null) { // if The getter method is absent
return;
}

Object propertyValue = invokeMethod(getterMethod, bean);

if (propertyValue != null) { // If The return value of "getName" method is not null
return;
}

Method setterMethod = propertyDescriptor.getWriteMethod();
if (setterMethod != null) { // the getter and setter methods are present
if (Arrays.equals(of(String.class), setterMethod.getParameterTypes())) { // the param type is String
// set bean name to the value of the the property
invokeMethod(setterMethod, bean, beanName);
}
}
}

}

/**
* @return Higher than {@link InitDestroyAnnotationBeanPostProcessor#getOrder()}
* @see InitDestroyAnnotationBeanPostProcessor
* @see CommonAnnotationBeanPostProcessor
* @see PostConstruct
*/
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE + 1;
}
}

可以看出,该类是自 Dubbo 2.7.6 开始提供,其实现非常简单,即在 id 属性未被赋值的情况下,将 id 属性设置为 Spring 中的 beanName。我将该类集成到使用 Dubbo 2.7.5 的环境中以尝试解决 id 值重复的问题,发现存在两个问题,一个是该 BeanPostProcessor 实现了 PriorityOrdered 接口,导致该类作为 bean 实例化时,注意不能放入使用了 @Autowired 注解的配置类中,否则会导致该配置类的其他 bean 在 AutowiredAnnotationBeanPostProcessor 处理前执行,如果依赖 @Autowired 的字段,则会触发空指针异常,该问题其实在 PriorityOrdered (Spring Framework 5.3.13 API) 文档中已经有说明:

Note: PriorityOrdered post-processor beans are initialized in a special phase, ahead of other post-processor beans. This subtly affects their autowiring behavior: they will only be autowired against beans which do not require eager initialization for type matching.

另一个问题为根据 Dubbo 2.7.5 的源码,在执行 BeanPostProcessor 的处理逻辑之前,ServiceBeanid 属性已经被设置,这导致该 BeanPostProcessor 中的 id 属性设置逻辑并不会被执行。结合 Dubbo 2.7.5 之后的 Git 提交记录,我们知道在 2.7.5 之后的 do not set default id (#6236) · apache/dubbo@a0be776 · GitHub 这一次 Git 提交中,将设置 id 的部分进行了注释,猜测该补丁在未设置 ServiceBeanid 属性时才能正常使用,所以我对该补丁进行了调整,即将判断 id 属性是否设置的部分进行了移除,调整为强制覆盖,以修复无法覆盖 ServiceBean 实例的 id 属性的问题,为了避免该补丁产生其他影响,同时将未使用的代码进行了移除,并且限制仅覆盖所有 ServiceBeanid 属性,减少其影响面,调整后的代码如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* The {@link BeanPostProcessor} class for the default property value of {@link AbstractConfig Dubbo's Config Beans}
*
* @since 2.7.6
*/
public class DubboConfigDefaultPropertyValueBeanPostProcessor extends GenericBeanPostProcessorAdapter<AbstractConfig>
implements MergedBeanDefinitionPostProcessor, PriorityOrdered {

/**
* The bean name of {@link DubboConfigDefaultPropertyValueBeanPostProcessor}
*/
public static final String BEAN_NAME = "dubboConfigDefaultPropertyValueBeanPostProcessor";

protected void processBeforeInitialization(AbstractConfig dubboConfigBean, String beanName) throws BeansException {
if (!(dubboConfigBean instanceof ServiceBean)) {
return;
}

// [Feature] https://github.com/apache/dubbo/issues/5721
setBeanNameAsId(dubboConfigBean, beanName);
}

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// DO NOTHING
}

protected void setBeanNameAsId(Object bean, String beanName) {
Class<?> beanClass = getTargetClass(bean);
PropertyDescriptor propertyDescriptor = getPropertyDescriptor(beanClass, "id");
if (propertyDescriptor != null) { // the property is present
Method setterMethod = propertyDescriptor.getWriteMethod();
if (setterMethod != null) { // the getter and setter methods are present
if (Arrays.equals(of(String.class), setterMethod.getParameterTypes())) { // the param type is String
// set bean name to the value of the the property
invokeMethod(setterMethod, bean, beanName);
}
}
}

}

/**
* @return Higher than {@link InitDestroyAnnotationBeanPostProcessor#getOrder()}
* @see InitDestroyAnnotationBeanPostProcessor
* @see CommonAnnotationBeanPostProcessor
* @see PostConstruct
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}

}

在进行了如上的调整之后,成功支持了 Dubbo 2.7.5 @Service 注解方式使用服务分组特性。记录该问题是因为我们应用中的 Dubbo 已经做了部分调整,不能轻易升级,所以尽量在不升级 Dubbo 版本的情况下处理该问题。

Reference

注解版接口多实现分组方式bug · Issue #6383 · apache/dubbo · GitHub
对同个接口的不同实现通过@Service设置不同group无法生效,zk中只注册了其中一个group。 · Issue #6283 · apache/dubbo · GitHub
Improve the readability of the getOrder method by tianshuang · Pull Request #9361 · apache/dubbo · GitHub