@RefreshScope 支持配置熱變更

RefreshScope

@RefreshScope是 scopeName="refresh"的 @Scope, 用來實現配置、實例熱加載

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

對應的實現類是GenericScope的子類org.springframework.cloud.context.scope.refresh.RefreshScope,類中註釋寫明:  

  1. 所有 @RefreshScope管轄的 Bean 都是延遲加載的,只有在第一次訪問時纔會初始化
  2. 刷新 Bean 的邏輯 也是同理,下次訪問時會創建一個新的對象。

件的自動裝配類是:org.springframework.cloud.autoconfigure.RefreshAutoConfiguration

@Configuration
@ConditionalOnClass(RefreshScope.class)
@ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, matchIfMissing = true)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
public class RefreshAutoConfiguration {

	/** Name of the refresh scope name. */
	public static final String REFRESH_SCOPE_NAME = "refresh";

	/**  Name of the prefix for refresh scope. */
	public static final String REFRESH_SCOPE_PREFIX = "spring.cloud.refresh";

	/**  Name of the enabled prefix for refresh scope. */
	public static final String REFRESH_SCOPE_ENABLED = REFRESH_SCOPE_PREFIX + ".enabled";

	@Bean
	@ConditionalOnMissingBean(RefreshScope.class)
	public static RefreshScope refreshScope() {
		return new RefreshScope();
	}

    @Bean
	@ConditionalOnMissingBean
	public ContextRefresher contextRefresher(ConfigurableApplicationContext context,
			RefreshScope scope) {
		return new ContextRefresher(context, scope);
	}

	@Bean
	public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
		return new RefreshEventListener(contextRefresher);
	}
... ...
}

由類定義可知,RefreshScope實現了接口 ApplicationListener<ContextRefreshedEvent>

public class RefreshScope extends GenericScope implements ApplicationContextAware,
		ApplicationListener<ContextRefreshedEvent>, Ordered {

	private ApplicationContext context;

	private BeanDefinitionRegistry registry;

	private boolean eager = true;

	private int order = Ordered.LOWEST_PRECEDENCE - 100;

	/** Creates a scope instance and gives it the default name: "refresh". */
	public RefreshScope() {
		super.setName("refresh");
	}

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		start(event);
	}

	public void start(ContextRefreshedEvent event) {
		if (event.getApplicationContext() == this.context && this.eager
				&& this.registry != null) {
			eagerlyInitialize();
		}
	}

	private void eagerlyInitialize() {
		for (String name : this.context.getBeanDefinitionNames()) {
			BeanDefinition definition = this.registry.getBeanDefinition(name);
			if (this.getName().equals(definition.getScope())
					&& !definition.isLazyInit()) {
				Object bean = this.context.getBean(name);
				if (bean != null) {
					bean.getClass();
				}
			}
		}
	}

因爲ContextRefreshedEvent事件是在容器啓動時末處AbstractApplicationContext.finishRefresh()發佈的,所以:此時會給初始化創建並緩存由RefreshScope管轄的Bean實例!

刷新

入口在ContextRefresher.refresh

	public synchronized Set<String> refresh() {
		Set<String> keys = refreshEnvironment();
		⑥this.scope.refreshAll();
		return keys;
	}

	public synchronized Set<String> refreshEnvironment() {
		Map<String, Object> before = ①extract(
				this.context.getEnvironment().getPropertySources());
		②addConfigFilesToEnvironment();
		Set<String> keys = ④changes(before,
				③extract(this.context.getEnvironment().getPropertySources())).keySet();
		⑤this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
		return keys;
	}

首先觸發執行ContextRefresher.refreshEnvironment()

  1. 提取標準參數(SYSTEM,JNDI,SERVLET)之外所有參數變量
  2. 把原來的Environment裏的參數放到一個新建的Spring Context容器下重新加載,完事之後關閉新容器
  3. 提取更新過的參數(排除標準參數)
  4. 比較出變更項
  5. 發佈環境變更事件,接收:EnvironmentChangeListener、LoggingRebinder
  6. RefreshScope用新的環境參數重新生成Bean,重新生成的過程很簡單,清除refreshscope緩存幷銷燬Bean,下次訪問就會重新從BeanFactory獲取一個新的實例(該實例使用新的配置)

接着觸發執行RefreshScope.refreshAll()

	public void refreshAll() {
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}

ReadWriteLock.writeLock()方式銷燬BeanLifecycleWrapperCache -> StandardScopeCache中的bean實例,這裏執行的是上文提及在創建實例過程最後註冊的銷燬回調函數(registerDestructionCallback);

// GenericScope 
	public void destroy() {
		List<Throwable> errors = new ArrayList<Throwable>();
		Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
		for (BeanLifecycleWrapper wrapper : wrappers) {
			try {
				Lock lock = this.locks.get(wrapper.getName()).writeLock();
				lock.lock();
				try {
					wrapper.destroy();
				}
				finally {
					lock.unlock();
				}
			}
			catch (RuntimeException e) {
				errors.add(e);
			}
		}
		if (!errors.isEmpty()) {
			throw wrapIfNecessary(errors.get(0));
		}
		this.errors.clear();
	}

如何觸發 Refresh

bus廣播刷新各節點服務

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

# 失效管理端安全: management.security.enabled = false
# 默認事件跟蹤功能是失效,需要通過配置項激活: spring.cloud.bus.trace.enabled=true

這裏需要集成RabbitMq服務,否則報錯

2020-10-14 11:33:32.716  INFO 19348 --- [main] o.s.c.s.m.DirectWithAttributesChannel    : Channel 'application-1.springCloudBusInput' has 1 subscriber(s).
2020-10-14 11:33:32.855  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageChannel nullChannel
2020-10-14 11:33:32.874  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageChannel springCloudBusOutput
2020-10-14 11:33:32.941  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageChannel errorChannel
2020-10-14 11:33:32.982  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageChannel springCloudBusInput
2020-10-14 11:33:32.997  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageHandler org.springframework.cloud.stream.binding.StreamListenerMessageHandler@7772d266
2020-10-14 11:33:33.062  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageHandler errorLogger
2020-10-14 11:33:33.098  INFO 19348 --- [main] o.s.i.endpoint.EventDrivenConsumer       : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel
2020-10-14 11:33:33.098  INFO 19348 --- [main] o.s.i.channel.PublishSubscribeChannel    : Channel 'application-1.errorChannel' has 1 subscriber(s).
2020-10-14 11:33:33.098  INFO 19348 --- [main] o.s.i.endpoint.EventDrivenConsumer       : started _org.springframework.integration.errorLogger
2020-10-14 11:33:35.015  INFO 19348 --- [main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-14 11:33:39.041  INFO 19348 --- [main] o.s.c.s.m.DirectWithAttributesChannel    : Channel 'application-1.springCloudBusOutput' has 1 subscriber(s).
2020-10-14 11:33:39.067  INFO 19348 --- [main] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: springCloudBus.anonymous.G8UactzcQg6VXoHSlF3U9g, bound to: springCloudBus
2020-10-14 11:33:39.067  INFO 19348 --- [main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-14 11:33:43.085  INFO 19348 --- [main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-14 11:33:47.095  INFO 19348 --- [main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-14 11:33:51.119  INFO 19348 --- [main] o.s.i.monitor.IntegrationMBeanExporter   : Registering MessageChannel springCloudBus.anonymous.G8UactzcQg6VXoHSlF3U9g.errors
2020-10-14 11:33:51.197  INFO 19348 --- [main] o.s.c.stream.binder.BinderErrorChannel   : Channel 'application-1.springCloudBus.anonymous.G8UactzcQg6VXoHSlF3U9g.errors' has 1 subscriber(s).
2020-10-14 11:33:51.197  INFO 19348 --- [main] o.s.c.stream.binder.BinderErrorChannel   : Channel 'application-1.springCloudBus.anonymous.G8UactzcQg6VXoHSlF3U9g.errors' has 2 subscriber(s).
2020-10-14 11:33:51.212  INFO 19348 --- [main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-14 11:33:55.242  INFO 19348 --- [main] o.s.a.r.l.SimpleMessageListenerContainer : Broker not available; cannot force queue declarations during start
2020-10-14 11:33:55.262  INFO 19348 --- [g6VXoHSlF3U9g-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-14 11:33:59.290  INFO 19348 --- [main] o.s.i.a.i.AmqpInboundChannelAdapter      : started inbound.springCloudBus.anonymous.G8UactzcQg6VXoHSlF3U9g
2020-10-14 11:33:59.363  INFO 19348 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-14 11:33:59.366  INFO 19348 --- [main] com.noob.BootstrapApplication            : Started BootstrapApplication in 39.91 seconds (JVM running for 40.479)
2020-10-14 11:34:04.303  WARN 19348 --- [g6VXoHSlF3U9g-1] o.s.a.r.l.SimpleMessageListenerContainer : Consumer raised exception, processing can restart if the connection factory supports it. Exception summary: org.springframework.amqp.AmqpConnectException: java.net.ConnectException: Connection refused: connect
2020-10-14 11:34:04.305  INFO 19348 --- [g6VXoHSlF3U9g-1] o.s.a.r.l.SimpleMessageListenerContainer : Restarting Consumer@18e4551: tags=[[]], channel=null, acknowledgeMode=AUTO local queue size=0
2020-10-14 11:34:04.307  INFO 19348 --- [g6VXoHSlF3U9g-2] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]

BusAutoConfiguration#BusRefreshConfiguration發佈一個RefreshBusEndpoint, RefreshBusEndpoint會從http端口觸發廣播RefreshRemoteApplicationEvent事件;

BusAutoConfiguration#RefreshListener負責接收事件(所有配置bus的節點服務都能收到)。

@Configuration
@ConditionalOnClass({ Endpoint.class, RefreshScope.class })
protected static class BusRefreshConfiguration {

  @Configuration
  @ConditionalOnBean(ContextRefresher.class)
  @ConditionalOnProperty(value = "endpoints.spring.cloud.bus.refresh.enabled", matchIfMissing = true)
  protected static class BusRefreshEndpointConfiguration {
      @Bean
      public RefreshBusEndpoint refreshBusEndpoint(ApplicationContext context,
              BusProperties bus) {
          return new RefreshBusEndpoint(context, bus.getId());
      }
  }
}

@Bean
@ConditionalOnProperty(value = "spring.cloud.bus.refresh.enabled", matchIfMissing = true)
@ConditionalOnBean(ContextRefresher.class)
public RefreshListener refreshListener(ContextRefresher contextRefresher) {
  return new RefreshListener(contextRefresher);
}

@Endpoint(id = "bus-refresh")
public class RefreshBusEndpoint extends AbstractBusEndpoint {
   public void busRefresh() {
      publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null));
  }
}

RefreshListener.onApplicationEvent觸發執行方法ContextRefresher.refresh()開啓刷新過程。

public class RefreshListener implements ApplicationListener<RefreshRemoteApplicationEvent> {
  public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
    Set<String> keys = contextRefresher.refresh();
  }

手動刷新

@Endpoint相當於 @WebEndpoint (生成web的方式的端點監控)@JmxEndpoint 生成JMX的方式監控 )的整合。

@Endpoint(id = "refresh")
public class RefreshEndpoint {

	private ContextRefresher contextRefresher;

	public RefreshEndpoint(ContextRefresher contextRefresher) {
		this.contextRefresher = contextRefresher;
	}

	@WriteOperation
	public Collection<String> refresh() {
		Set<String> keys = this.contextRefresher.refresh();
		return keys;
	}
}

org.springframework.cloud.endpoint.RefreshEndpoint 的類上註解@Endpoint需要顯式引入該jar才生效。

<dependency>
    <!-- http://localhost:8080/actuator/health -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>2.1.5.RELEASE</version>
</dependency>

因爲在spring-cloud-contextpom.xml文件中:對actuator的依賴設置<optional>true</optional>表示兩個項目之間依賴不傳遞。(默認是false -> 傳遞依賴

這些 Endpoint 在org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration中被封裝爲WebMvcEndpointHandlerMapping,提供Http的路徑訪問功能。

默認的基礎路徑是/actuator。如果一個端點配置的路徑是sessions,那麼它的全路徑就是/actuator/sessions 。@Selector 的含義是讓這個路徑變成/actuator/sessions/{name} ,能從路徑上獲取一個入參。

@Endpoint(id = "sessions")
public class MyHealthEndpoint {
  @ReadOperation
  public Info get(@Selector String name) {
    return new Info(name);
  }
}

bootstrax.yml加入開啓監控:

# 默認端口和應用的端口是一致的,但是也可以通過配置的方式改變端口
management.server.port = 8081
management.server.address = 127.0.0.1
# 允許跨域的網址
management.endpoints.web.cors.allowed-origins=http://example.com
# 允許跨域的方法
management.endpoints.web.cors.allowed-methods=GET,POST
# 開啓監控接口
management:
  endpoints:
    web:
      exposure:
        include: "*" # 默認是兩個health|info
        #include: refresh,health,info # 打開部分
        #exclude: beans # 關閉部分
2020-10-14 11:43:44.443  INFO 19176 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-14 11:43:44.883  INFO 19176 --- [main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 16 endpoint(s) beneath base path '/actuator'
2020-10-14 11:43:45.023  INFO 19176 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-14 11:43:45.025  INFO 19176 --- [main] com.noob.BootstrapApplication            : Started BootstrapApplication in 5.978 seconds (JVM running for 6.534)

健康檢測: 訪問 http://localhost:8080/actuator/health , 返回: {"status":"UP"}
查看所有對象信息: http://localhost:8080/actuator/beans
查看所有環境變量: http://http://localhost:8080/actuator/env
查看不同對象的日誌級別: http://localhost:8080/actuator/loggers
刷新配置: curl -X POST http://localhost:8080/actuator/refresh    返回變更的配置信息
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章