Poison

Dubbo #9361

业务应用报错找不到 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 注解配置无法导出,在本地进行源码调试后,发现差异在于对 ServiceBean 的 id 属性的设置上。其中 XML 配置时,如果用户在 <dubbo:service /> 中配置了 id,那么会使用用户显式指定的 id 作为 ServiceBean 的 id 属性值,如果用户没有显式指定 id, 那么会依次尝试使用配置的 name 与 interface 去作为 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);
}

// ...
}

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

而当使用 Dubbo 的 @Service 注解配置服务实现时,在 @Service 的属性中,是没有提供 id 属性进行配置的,所以通过 @Service 配置 Dubbo 服务时,该 ServiceBean 的 id 属性值无法由用户显式指定,而是在实例化 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 的处理逻辑之前,ServiceBean 的 id 属性已经被设置,这导致该 BeanPostProcessor 中的 id 属性设置逻辑并不会被执行。结合 Dubbo 2.7.5 之后的 Git 提交记录,我们知道在 2.7.5 之后的 do not set default id (#6236) · apache/dubbo@a0be776 · GitHub 这一次 Git 提交中,将设置 id 的部分进行了注释,猜测该补丁在未设置 ServiceBean 的 id 属性时才能正常使用,所以我对该补丁进行了调整,即将判断 id 属性是否设置的部分进行了移除,调整为强制覆盖,以修复无法覆盖 ServiceBean 实例的 id 属性的问题,为了避免该补丁产生其他影响,同时将未使用的代码进行了移除,并且限制仅覆盖所有 ServiceBean 的 id 属性,减少其影响面,调整后的代码如下:

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 版本的情况下处理该问题。

References

注解版接口多实现分组方式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