Я настроил среду Spring Cloud, включая Eureka Server, Config Server с использованием git в качестве источника данных, клиент Eureka с использованием Config Server и периодический опрос конфигурации. Я видел, как каждый раз, когда я меняю конфигурацию через git, клиент сначала отменяет регистрацию на сервере Eureka, а затем снова регистрируется. Как избежать процесса отмены регистрации-регистрации?
В клиенте я использовал @EnableScheduling для включения Spring Scheduling и создал класс ConfigGitClientWatch для опроса сервера Config, другой класс MyContextRefresher для применения опрошенных изменений конфигурации в Spring Environment.
ConfigGitClientWatch:
@Component
@Slf4j
public class ConfigGitClientWatch implements Closeable, EnvironmentAware {
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicReference<String> version = new AtomicReference<>();
private final MyContextRefresher refresher;
private final ConfigServicePropertySourceLocator locator;
private Environment environment;
public ConfigGitClientWatch(
MyContextRefresher refresher, ConfigServicePropertySourceLocator locator) {
this.refresher = refresher;
this.locator = locator;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@PostConstruct
public void start() {
running.compareAndSet(false, true);
}
@Scheduled(
initialDelayString = "${spring.cloud.config.watch.git.initialDelay:180000}",
fixedDelayString = "${spring.cloud.config.watch.git.delay:500}"
)
public void watchConfigServer() {
if (running.get()) {
String newVersion = fetchNewVersion();
String oldVersion = version.get();
if (versionChanged(oldVersion, newVersion)) {
version.set(newVersion);
final Set<String> refreshedProperties = refresher.refresh();
if(!refreshedProperties.isEmpty()) {
log.info("Refreshed properties:{}", String.join(",", refreshedProperties));
}
}
}
}
private String fetchNewVersion() {
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(environment);
return (String) propertySource.getProperty("config.client.version");
}
private static boolean versionChanged(String oldVersion, String newVersion) {
return !hasText(oldVersion) && hasText(newVersion)
|| hasText(oldVersion) && !oldVersion.equals(newVersion);
}
@Override
public void close() {
running.compareAndSet(true, false);
}
}
MyContextRefresher:
@Component
@Slf4j
public class MyContextRefresher {
private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";
private static final String[] DEFAULT_PROPERTY_SOURCES = new String[] {
// order matters, if cli args aren't first, things get messy
CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
"defaultProperties" };
private Set<String> standardSources = new HashSet<>(
Arrays.asList(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
"configurationProperties"));
private ConfigurableApplicationContext context;
private RefreshScope scope;
public MyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
this.context = context;
this.scope = scope;
}
protected ConfigurableApplicationContext getContext() {
return this.context;
}
protected RefreshScope getScope() {
return this.scope;
}
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
if(!keys.isEmpty()) {
this.scope.refreshAll();
}
return keys;
}
private final List<String> skippedKeys = Arrays.asList(
"config.client.version",
"spring.cloud.client.hostname",
"local.server.port"
);
public synchronized Set<String> refreshEnvironment() {
return addConfigFilesToEnvironment();
}
private Set<String> addConfigFilesToEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
ConfigurableApplicationContext capture = null;
Set<String> changedKeys = new HashSet<>();
try {
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
capture = builder.run();
if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
target.addAfter(targetName, source);
}
else {
// targetName was null so we are at the start of the list
target.addFirst(source);
targetName = name;
}
}
}
}
final Map<String, Object> after = extract(environment.getPropertySources());
changedKeys = changes(before, after).keySet();
changedKeys.removeAll(skippedKeys);
}
finally {
if(!changedKeys.isEmpty()) {
ConfigurableApplicationContext closeable = capture;
while (closeable != null) {
try {
closeable.close();
} catch (Exception e) {
// Ignore;
}
if (closeable.getParent() instanceof ConfigurableApplicationContext) {
closeable = (ConfigurableApplicationContext) closeable.getParent();
} else {
break;
}
}
this.context.publishEvent(new EnvironmentChangeEvent(this.context, changedKeys));
}
}
return changedKeys;
}
// Don't use ConfigurableEnvironment.merge() in case there are clashes with property
// source names
private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
StandardEnvironment environment = new StandardEnvironment();
MutablePropertySources capturedPropertySources = environment.getPropertySources();
// Only copy the default property source(s) and the profiles over from the main
// environment (everything else should be pristine, just like it was on startup).
for (String name : DEFAULT_PROPERTY_SOURCES) {
if (input.getPropertySources().contains(name)) {
if (capturedPropertySources.contains(name)) {
capturedPropertySources.replace(name,
input.getPropertySources().get(name));
}
else {
capturedPropertySources.addLast(input.getPropertySources().get(name));
}
}
}
environment.setActiveProfiles(input.getActiveProfiles());
environment.setDefaultProfiles(input.getDefaultProfiles());
Map<String, Object> map = new HashMap<String, Object>();
map.put("spring.jmx.enabled", false);
map.put("spring.main.sources", "");
capturedPropertySources
.addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
return environment;
}
private Map<String, Object> changes(Map<String, Object> before,
Map<String, Object> after) {
Map<String, Object> result = new HashMap<String, Object>();
for (String key : before.keySet()) {
if (!after.containsKey(key)) {
result.put(key, null);
}
else if (!equal(before.get(key), after.get(key))) {
result.put(key, after.get(key));
}
}
for (String key : after.keySet()) {
if (!before.containsKey(key)) {
result.put(key, after.get(key));
}
}
return result;
}
private boolean equal(Object one, Object two) {
if (one == null && two == null) {
return true;
}
if (one == null || two == null) {
return false;
}
return one.equals(two);
}
private Map<String, Object> extract(MutablePropertySources propertySources) {
Map<String, Object> result = new HashMap<String, Object>();
List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> source : propertySources) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
if (!this.standardSources.contains(source.getName())) {
extract(source, result);
}
}
return result;
}
private void extract(PropertySource<?> parent, Map<String, Object> result) {
if (parent instanceof CompositePropertySource) {
try {
List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> source : ((CompositePropertySource) parent)
.getPropertySources()) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
extract(source, result);
}
}
catch (Exception e) {
return;
}
}
else if (parent instanceof EnumerablePropertySource) {
for (String key : ((EnumerablePropertySource<?>) parent).getPropertyNames()) {
result.put(key, parent.getProperty(key));
}
}
}
@Configuration
protected static class Empty {
}
}
Журнал клиента следующий:
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://172.39.8.118:14102/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=user-service, profiles=[peer2], label=null, version=3961593acd49e60c194aebc224adc6a4dfa9f530, state=null
trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$39b14aa7] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING
com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1
c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml
c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml
c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
com.netflix.discovery.DiscoveryClient : Disable delta property : false
com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null
com.netflix.discovery.DiscoveryClient : Force full registry fetch : false
com.netflix.discovery.DiscoveryClient : Application is null : false
com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true
com.netflix.discovery.DiscoveryClient : Application version is -1: true
com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server
com.netflix.discovery.DiscoveryClient : The response status is 200
com.netflix.discovery.DiscoveryClient : Not registering with Eureka server per configuration
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1570780512926 with initial instances count: 6
o.s.c.n.e.s.EurekaServiceRegistry : Registering application USER-SERVICE with eureka with status UP
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://172.39.8.118:14102/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=user-service, profiles=[peer2], label=null, version=3961593acd49e60c194aebc224adc6a4dfa9f530, state=null
b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}, MapPropertySource {name='http://localhost:3000/longqinsi/demo-config.git/user-service.yml'}, MapPropertySource {name='http://localhost:3000/longqinsi/demo-config.git/application.yml'}]}
o.s.boot.SpringApplication : The following profiles are active: peer2
com.example.userservice.HelloController : Received heartbeat from user service at port 14105.
o.s.boot.SpringApplication : Started application in 0.299 seconds (JVM running for 10313.932)
o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application USER-SERVICE with eureka with status DOWN
com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient ...
com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient
com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient ...
com.example.userservice.HelloController : Received heartbeat from user service at port 14105.
com.netflix.discovery.DiscoveryClient : Unregistering ...
com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104 - deregister status: 200
com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient
o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING
com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1
c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml
c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml
c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
com.netflix.discovery.DiscoveryClient : Disable delta property : false
com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null
com.netflix.discovery.DiscoveryClient : Force full registry fetch : false
com.netflix.discovery.DiscoveryClient : Application is null : false
com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true
com.netflix.discovery.DiscoveryClient : Application version is -1: true
com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server
com.netflix.discovery.DiscoveryClient : The response status is 200
com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 30
c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 4
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1570780516317 with initial instances count: 6
o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application USER-SERVICE with eureka with status DOWN
com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1570780516320, current=DOWN, previous=STARTING]
com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104: registering service...
o.s.c.n.e.s.EurekaServiceRegistry : Registering application USER-SERVICE with eureka with status UP
com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1570780516320, current=UP, previous=DOWN]
o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application USER-SERVICE with eureka with status DOWN
o.s.c.n.e.s.EurekaServiceRegistry : Registering application USER-SERVICE with eureka with status UP
com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104 - registration status: 204