项目场景
Nacos作为配置中心,添加Netty并启动后,Nacos管理端修改配置后,客户端未感知到变化。
问题描述
- 引入相应版本依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.8.RELEASE</spring-cloud-alibaba.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.38.Final </version>
<scope>compile</scope>
</dependency>
</dependencies>
- 通过如下方式启动Netty:
@SpringBootApplication
public class Application implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
server.start();//启动Netty
}
@Autowired
EchoServer server;
}
- 添加如下测试配置参数的代码:
@RestController
@RequestMapping("/test")
@Slf4j
@RefreshScope
public class TestController {
@Value("${address.port}")
String port;
@GetMapping("/port")
public String get(){
return port;
}
}
- 在Nacos管理端修改
address.port
参数后,访问/test/port
无变化。
原因分析
Nacos是通过
NacosContextRefresher
来注册监听器,进而监听服务端dataId的配置信息的变化,并更新客户端内存中的数据
分析过程
SpringApplication.run
在启动容器完成时,会调用listeners.ready(...)
发布ApplicationStartedEvent
事件,源码如下:
//步骤一 SpringApplicaiton.java
public ConfigurableApplicationContext run(String... args) {
//...
listeners.ready(context, timeTakenToReady);//发布容器已启动并准备好事件
//...
}
//步骤二 SpringApplicationRunListeners.java
void ready(ConfigurableApplicationContext context, Duration timeTaken) {
doWithListeners("spring.boot.application.ready", (listener) -> listener.ready(context, timeTaken));
}
//步骤三 EventPublishingRunListener.java
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
//在这里发布 容器已启动并准备好了的 事件
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}
Nacos的NacosContextRefresher
监听器会监听ApplicationReadyEvent
事件,并注册自己的监听器,用来监听Nacos服务端(即管理台)中配置信息的变化。
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
//...
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {//通过CAS完成
this.registerNacosListenersForApplications();
}
}
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {//重点:创建监听器监听服务端dataKey的配置信息变化
@Override
public void innerReceive(String dataId, String group,
String configInfo) {//重点:具体处理dataKey的配置信息变化,主要是更新内存的值
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
//....
}
//...
}
- Netty启动时,为了
优雅
的关闭服务端,是如下完成的:
try {
//异步绑定到服务器,通过sync阻塞保证启动成功
ChannelFuture future = bootstrap.bind(9080).sync();
log.info("netty 启动成功");
// 通过sync方法同步等待通道关闭处理完毕,这里会 阻塞 等待通道关闭完成
future.channel().closeFuture().sync();//优雅的关闭服务端,当被唤醒后,线程就会结束阻塞,最终正常执行finally里的方法
}finally {
boss.shutdownGracefully().sync();
}
- 由于
future.channel().closeFuture().sync()
阻塞线程,并且Netty我们是在主线程
,即Spirng容器启动的线程
来启动的,所以Spring启动过程会被阻塞
。换句话说不再会发布ApplicationReadyEvent
事件,那么自然,Nacos服务端的配置修改不会再被监听到,也不会更新客户端中内存的值了。
解决方案
通过新建一个线程或交由线程池来启动Netty
下例演示新建一个线程来启动Netty:
@SpringBootApplication
public class Application implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
new Thread(()->{//新建一个线程方式来启动
server.start();
}).start();
}
@Autowired
EchoServer server;
}