异常
Dubbo自动关停自我销毁
11:39:12.242 [main] INFO o.a.d.r.t.AbstractServer - [,73] - [DUBBO] Start NettyServer bind /0.0.0.0:20880, export /10.10.10.2:20880, dubbo version: 2.7.8, current host: 10.10.10.2
11:39:12.264 [main] INFO c.a.c.d.s.DubboMetadataServiceExporter - [unexport,106] - The Dubbo service[<dubbo:service exported=“true” unexported=“true” />] has been unexported.
11:39:12.266 [main] INFO o.a.d.r.s.AbstractRegistryFactory - [destroyAll,81] - [DUBBO] Close all registries [], dubbo version: 2.7.8, current host: 10.10.10.2
11:39:12.266 [main] INFO o.a.d.r.p.d.DubboProtocol - [destroy,615] - [DUBBO] Close dubbo server: /10.10.10.2:20880, dubbo version: 2.7.8, current host: 10.10.10.2
11:39:12.267 [main] INFO o.a.d.r.t.AbstractServer - [close,128] - [DUBBO] Close NettyServer bind /0.0.0.0:20880, export /10.10.10.2:20880, dubbo version: 2.7.8, current host: 10.10.10.2然后抛出异常:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘org.springframework.cloud.client.discovery.DiscoveryClient’ available
环境
spring-cloud-stream-binder-kafka:3.0.8.RELEASE
spring-cloud-starter-dubbo:2.2.5.RELEASE
spring-cloud-dependencies:Hoxton.SR8
spring-cloud-alibaba-dependencies:2.2.5.RELEASE
nacos-client: 2.0.0
问题分析
那么我是用了spring.cloud.stream.binders
配置来设定多个Binder,一旦设定了就会发现启动不了。
经过几轮源码调试,终于发现了问题所在。
-
Dubbo中的
DubboServiceRegistrationApplicationContextInitializer
类通过ApplicationContextInitializer方式来注入ApplicationContext应用上下文到SpringCloudRegistryFactory
中。
-
Spring Cloud项目启动,实例化各种Bean…(包括了DiscoveryClient)
-
在Spring Cloud Stream在处理自定义的Binder配置时,会为其特定的environment配置独立一个ApplicationContext应用上下文(这个有点特殊,时间原因,没有细究),然后会触发ApplicationContextInitializer,Dubbo中的
DubboServiceRegistrationApplicationContextInitializer
类就获取到了这个特殊的ApplicationContext应用上下文(这个是获取不到实例化好的Bean的)。 -
SpringCloudRegistryFactory
然后在注册Dubbo服务时,使用上一步获取到的ApplicationContext来getBean(DiscoveryClient.class)
,那自然就报错了。
解决方案
针对上面的情况的话,大体思路是有另外一个Bean通过ApplicationContextAware
注入最完整的ApplicationContext应用上下文,然后自己再定义一个新的MyDubboServiceRegistrationApplicationContextInitializer
类,实际注入到SpringCloudRegistryFactory
的ApplicationContext应用上下文就用之前ApplicationContextAware
注入的到那个。
当然要注意验证MyDubboServiceRegistrationApplicationContextInitializer
的加载顺序必须在Dubbo中的DubboServiceRegistrationApplicationContextInitializer
后面才行。
方式一:SpringBoot Starter
其实看到上述提到的源码也看到了DubboServiceRegistrationApplicationContextInitializer
类是没有定义Order优先级的,那么他在Spring内部默认优先级就是最低的(也就是等价于@Order(Integer.MAX_VALUE)
),所以在这个时候就很难保证我们自定义的Bean一定能排在它之后。
这个时候可能就需要个取巧的方案,把这个类放到另一个自定义的Spring Boot Starter项目中,然后再引入到自己的业务项目里。
这里要注意下依赖包加载优先级问题,我记得Maven加载顺序好像默认以groupId+artifactId的级别深度,深度越浅那么优先级就越高,如果是想自己的自定义依赖包里面的MyDubboServiceRegistrationApplicationContextInitializer
类是最后加载处理的话,就需要把groupId写深点。比如下面这样:
Dubbo:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency>
自定义的:
<dependency> <groupId>org.zze0.cloud.dubbo</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency>
代码实现:
package org.zze0;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static Optional<ApplicationContext> tryGetApplicationContext() {
return Optional.ofNullable(applicationContext);
}
}
package org.zze0;
import com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
@Order
public class MyDubboServiceRegistrationApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
SpringUtils.tryGetApplicationContext()
.filter(context -> context instanceof ConfigurableApplicationContext)
.map(context -> (ConfigurableApplicationContext) context)
.ifPresent(SpringCloudRegistryFactory::setApplicationContext);
}
}
spring.factories:
org.springframework.context.ApplicationContextInitializer=\
org.zze0.MyDubboServiceRegistrationApplicationContextInitializer
方式二:覆盖类
除了上述曲线救国的方式,那么有没有另外一种方式是不需要另外开项目的呢?
据我目前了解到的内容和查看源码(org.springframework.web.servlet.FrameworkServlet#applyInitializers
)后的感想来看,通过调整DubboServiceRegistrationApplicationContextInitializer
类实例化、调用顺序在自定义的之前几乎是做不到的,因为无论是使用@Order
注解还是Ordered
接口都是需要修改代码才能达到的,而且应用在启动的时候是默认先加载依赖包里的类。我也尝试过使用AOP切面去处理,结果发现其实例化ApplicationContextInitializer的时候并没有使用代理模式生成代理对象,所以基本该方案也做不了。
那么只剩下比较粗暴直接的方式:直接复制DubboServiceRegistrationApplicationContextInitializer
类的源码到自己本地项目里面,包名和阿里巴巴的包名一致,相当于覆盖依赖包里面的类。因为遇到同全限定名的类时,类加载器默认会优先加载使用本地项目的类。如下:
package org.zze0;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static Optional<ApplicationContext> tryGetApplicationContext() {
return Optional.ofNullable(applicationContext);
}
}
package com.alibaba.cloud.dubbo.context;
import com.alibaba.cloud.dubbo.registry.SpringCloudRegistryFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.zze0.SpringUtils;
public class DubboServiceRegistrationApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
SpringUtils.tryGetApplicationContext()
.filter(context -> context instanceof ConfigurableApplicationContext)
.map(context -> (ConfigurableApplicationContext) context)
.ifPresent(SpringCloudRegistryFactory::setApplicationContext);
}
}