Added hypermedia support in the form of a RemoteResource abstraction.

This commit introduces support to easily discover resources in remote
service APIs and periodically validating they're still available.

The core concept is a RemoteResource and its primary implementation
DiscoveredResource which consists of a ServiceInstanceProvider and a
TraversalDescription. The service instance provider abstracts the location
of the service the DiscoveredResource is hosted at. Currently there are
two implementations available: a StaticServiceInstanceProvider that — as
the name suggests — returns a statically configured instance, as well as
the DynamicServiceInstanceProvider which takes a DiscoveryClient and service
name as input to dynamically discover the ServiceInstance on request.

The TraversalDefinition is a SAM type that allows to define the path traversals
that shall be executed to discover the target resource within the service
instance. This allows the following configuration:

DiscoveryClient client = …
ServiceInstanceProvider provider = new DynamicServiceInstanceProvider(client, "stores");
RemoteResource resource = new DiscoveredResource(provider, traverson -> traverson.follow("stores", "search", "by-location");

This will cause the following to happen:

1. In this default state the resource is undiscovered and calls to getLink()
   will return null.
2. A call to verifyOrDiscover() will trigger resource discovery, i.e. lookup
   the service instance using the discovery client, execute the path traversal
   as defined and store the final link discovered ("by-location") in the
   discovered resource.
3. Calls to getLink() will return that link. Calls to verifyOrDiscover()
   will issue a HEAD request to verify the resource is still available and
   reset the link if not. Subsequent calls to verifyOrDiscover() will trigger
   rediscovery.

That means that we basically shield against either the service instance becoming
unavailable or the resource becoming unavailable within the service instance
(due to some URI restructuring or the like).

The calls to verifyOrDiscover() are currently automated by a RemoteResourceRefresher
which is auto-configured if a RemoteResource is declared as Spring bean
and its initial and fixed delay can be configured via properties in the
spring.cloud.hypermedia namespace.
This commit is contained in:
Oliver Gierke
2015-12-23 12:04:35 +01:00
parent cbfd48d5fa
commit 526b338c36
14 changed files with 727 additions and 1 deletions

View File

@@ -48,6 +48,16 @@
<artifactId>spring-integration-jmx</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import lombok.Data;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration.CloudHypermediaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Registers a default {@link RemoteResourceRefresher} if at least one {@link RemoteResource} is declared in the system
* and applies verification timings defined in the application properties.
*
* @author Oliver Gierke
*/
@Configuration
@ConditionalOnBean(type = "org.springframework.cloud.client.hypermedia.RemoteResource")
@EnableConfigurationProperties(CloudHypermediaProperties.class)
public class CloudHypermediaAutoConfiguration {
@Autowired(required = false) List<RemoteResource> discoveredResources = Collections.emptyList();
@Autowired CloudHypermediaProperties properties;
@Bean
@ConditionalOnMissingBean
public RemoteResourceRefresher discoveredResourceRefresher() {
return new RemoteResourceRefresher(discoveredResources, properties.getRefresh().getFixedDelay(),
properties.getRefresh().getInitialDelay());
}
@Data
@ConfigurationProperties(prefix = "spring.cloud.hypermedia")
public static class CloudHypermediaProperties {
private Refresh refresh = new Refresh();
@Data
public static class Refresh {
private int fixedDelay = 5000;
private int initialDelay = 10000;
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.client.Traverson;
import org.springframework.util.Assert;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* A REST resource that is defined by a service reference and a traversal operation within that service.
*
* @author Oliver Gierke
*/
@Slf4j
@RequiredArgsConstructor
public class DiscoveredResource implements RemoteResource {
private final ServiceInstanceProvider provider;
private final TraversalDefinition traversal;
private RestOperations restOperations = new RestTemplate();
private @Getter Link link = null;
/**
* Configures the {@link RestOperations} to use to execute the traversal and verifying HEAD calls.
*
* @param restOperations can be {@literal null}, resorting to a default {@link RestTemplate} in that case.
*/
public void setRestOperations(RestOperations restOperations) {
this.restOperations = restOperations == null ? new RestTemplate() : restOperations;
}
/**
* Verifies the link to the current
*/
public void verifyOrDiscover() {
this.link = link == null ? discoverLink() : verify(link);
}
/**
* Verifies the given {@link Link} by issuing an HTTP HEAD request to the resource.
*
* @param link must not be {@literal null}.
* @return
*/
private Link verify(Link link) {
Assert.notNull(link, "Link must not be null!");
try {
String uri = link.expand().getHref();
log.debug("Verifying link pointing to {}…", uri);
restOperations.headForHeaders(uri);
log.debug("Successfully verified link!");
return link;
} catch (RestClientException o_O) {
log.debug("Verification failed, marking as outdated!");
return null;
}
}
private Link discoverLink() {
try {
ServiceInstance service = provider.getServiceInstance();
if (service == null) {
return null;
}
URI uri = service.getUri();
String serviceId = service.getServiceId();
log.debug("Discovered {} system at {}. Discovering resource…", serviceId, uri);
Traverson traverson = new Traverson(uri, MediaTypes.HAL_JSON);
Link link = traversal.buildTraversal(traverson).asTemplatedLink();
log.debug("Found link pointing to {}.", link.getHref());
return link;
} catch (RuntimeException o_O) {
this.link = null;
log.debug("Target system unavailable. Got: ", o_O.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
/**
* {@link ServiceInstanceProvider} to work with a {@link DiscoveryClient} to lookup a service by name. Will pick the
* first one returned by the configured {@link DiscoveryClient}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
public class DynamicServiceInstanceProvider implements ServiceInstanceProvider {
private final DiscoveryClient client;
private final String serviceName;
/*
* (non-Javadoc)
* @see example.customers.integration.ServiceInstanceProvider#getServiceInstance()
*/
@Override
public ServiceInstance getServiceInstance() {
try {
List<ServiceInstance> instances = client.getInstances(serviceName);
return instances.isEmpty() ? null : instances.get(0);
} catch (RuntimeException o_O) {
return null;
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import org.springframework.hateoas.Link;
/**
* A REST resource that can be discovered and can be either gone or available.
*
* @author Oliver Gierke
*/
public interface RemoteResource {
/**
* Returns the {@link Link} to the resource in case it is available or {@literal null} in case it's gone, i.e. either
* generally unavailable or can't be discovered.
*
* @return
*/
Link getLink();
/**
* Discovers the the resource in case it hasn't been yet or became unavailable. In case a link has been discovered
* previously, it is verified and either confirmed or the link is removed to indicate it's not available anymore.
*/
void verifyOrDiscover();
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.scheduling.config.ContextLifecycleScheduledTaskRegistrar;
import org.springframework.scheduling.config.IntervalTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
* A {@link ScheduledTaskRegistrar} that verifies all {@link DiscoveredResource} instances in the system based
* on the given timing configuration.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
public class RemoteResourceRefresher extends ContextLifecycleScheduledTaskRegistrar {
private final List<RemoteResource> discoveredResources;
private final int fixedDelay, initialDelay;
/*
* (non-Javadoc)
* @see org.springframework.scheduling.config.ContextLifecycleScheduledTaskRegistrar#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
for (final RemoteResource resource : discoveredResources) {
addFixedDelayTask(new IntervalTask(new Runnable() {
@Override
public void run() {
resource.verifyOrDiscover();
}
}, fixedDelay, initialDelay));
}
super.afterPropertiesSet();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import org.springframework.cloud.client.ServiceInstance;
/**
* A component that will provide a {@link ServiceInstance} or can express the absence of one by returning
* {@literal null}.
*
* @author Oliver Gierke
*/
public interface ServiceInstanceProvider {
/**
* Returns the service instance or {@literal null} in case the service is currently unavailable.
*
* @return the service instance or {@literal null} in case the service is currently unavailable.
*/
ServiceInstance getServiceInstance();
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.client.ServiceInstance;
/**
* A {@link ServiceInstanceProvider} that will always return the configured {@link ServiceInstance}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
public class StaticServiceInstanceProvider implements ServiceInstanceProvider {
private final ServiceInstance instance;
/*
* (non-Javadoc)
* @see example.customers.integration.ServiceInstanceProvider#getServiceInstance()
*/
@Override
public ServiceInstance getServiceInstance() {
return instance;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import org.springframework.hateoas.client.Traverson;
import org.springframework.hateoas.client.Traverson.TraversalBuilder;
/**
* Callback to define the traversal to a resource.
*
* @author Oliver Gierke
*/
public interface TraversalDefinition {
/**
* @param traverson the Traverson instance to run the traversal on.
* @return
*/
TraversalBuilder buildTraversal(Traverson traverson);
}

View File

@@ -2,6 +2,7 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.client.CommonsClientAutoConfiguration,\
org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\
org.springframework.cloud.util.UtilAutoConfiguration

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration.CloudHypermediaProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.client.Traverson;
import org.springframework.hateoas.client.Traverson.TraversalBuilder;
/**
* Integration tests for {@link CloudHypermediaAutoConfiguration}.
*
* @author Oliver Gierke
*/
public class CloudHypermediaAutoConfigurationIntegrationTests {
@Test
public void picksUpHypermediaProperties() {
try (ConfigurableApplicationContext context = getApplicationContext(ConfigWithRemoteResource.class)) {
CloudHypermediaProperties properties = context.getBean(CloudHypermediaProperties.class);
assertThat(properties.getRefresh().getInitialDelay(), is(50000));
assertThat(properties.getRefresh().getFixedDelay(), is(10000));
}
}
@Test
public void doesNotCreateCloudHypermediaPropertiesifNotActive() {
try (ConfigurableApplicationContext context = getApplicationContext(Config.class)) {
assertThat(context.getBeanNamesForType(CloudHypermediaProperties.class), is(arrayWithSize(0)));
}
}
@Test
public void doesNotRegisterResourceRefresherIfNoDiscoveredResourceIsDefined() {
try (ConfigurableApplicationContext context = getApplicationContext(Config.class)) {
assertThat(context.getBeansOfType(RemoteResource.class).values(), hasSize(0));
assertThat(context.getBeanNamesForType(RemoteResourceRefresher.class), is(arrayWithSize(0)));
}
}
@Test
public void registersResourceRefresherIfDiscoverredResourceIsDefined() {
try (ConfigurableApplicationContext context = getApplicationContext(ConfigWithRemoteResource.class)) {
assertThat(context.getBeansOfType(RemoteResource.class).values(), hasSize(1));
assertThat(context.getBean(RemoteResourceRefresher.class), is(notNullValue()));
}
}
private static ConfigurableApplicationContext getApplicationContext(Class<?> configuration) {
return SpringApplication.run(configuration, new String[0]);
}
@Configuration
@EnableAutoConfiguration
static class Config {}
@Configuration
@EnableAutoConfiguration
static class ConfigWithRemoteResource {
@Bean
public RemoteResource resource() {
ServiceInstanceProvider provider = new StaticServiceInstanceProvider(
new DefaultServiceInstance("service", "localhost", 80, false));
return new DiscoveredResource(provider, new TraversalDefinition() {
@Override
public TraversalBuilder buildTraversal(Traverson traverson) {
return traverson.follow("rel");
}
});
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.client.Traverson;
import org.springframework.hateoas.client.Traverson.TraversalBuilder;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
/**
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class DiscoveredResourceUnitTests {
@Mock ServiceInstanceProvider provider;
@Mock TraversalDefinition traversal;
@Mock TraversalBuilder builder;
@Mock RestOperations operations;
DiscoveredResource resource;
@Before
public void setUp() {
when(traversal.buildTraversal(Matchers.any(Traverson.class))).thenReturn(builder);
this.resource = new DiscoveredResource(provider, traversal);
this.resource.setRestOperations(operations);
}
@Test
public void isUndiscoveredByDefault() {
assertThat(resource.getLink(), is(nullValue()));
}
@Test
public void verificationTriggersDiscovery() {
Link link = new Link("target", "rel");
when(provider.getServiceInstance()).thenReturn(new DefaultServiceInstance("service", "localhost", 8080, false));
when(builder.asTemplatedLink()).thenReturn(link);
resource.verifyOrDiscover();
assertThat(resource.getLink(), is(link));
verify(provider, times(1)).getServiceInstance();
verify(traversal, times(1)).buildTraversal(Matchers.any(Traverson.class));
}
@Test
public void triggersVerificationOnSubsequentCall() {
verificationTriggersDiscovery();
resource.verifyOrDiscover();
assertThat(resource.getLink(), is(notNullValue()));
verify(operations, times(1)).headForHeaders(anyString());
}
@Test
public void resetsLinkOnFailedVerification() {
verificationTriggersDiscovery();
doThrow(RestClientException.class).when(operations).headForHeaders(anyString());
resource.verifyOrDiscover();
assertThat(resource.getLink(), is(nullValue()));
}
@Test
public void failedDiscoveryTraversalCausesLinkToStayNull() {
doThrow(RuntimeException.class).when(provider).getServiceInstance();
resource.verifyOrDiscover();
assertThat(resource.getLink(), is(nullValue()));
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.client.hypermedia;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
/**
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class DynamicServiceInstanceProviderUnitTests {
@Mock DiscoveryClient client;
@Test
public void returnsNoServiceInCaseNoneIsAvailable() {
assertThat(new DynamicServiceInstanceProvider(client, "service").getServiceInstance(), is(nullValue()));
}
@Test
public void returnsFirstServiceInCaseMultipleOnesAreAvailable() {
ServiceInstance first = mock(ServiceInstance.class);
ServiceInstance second = mock(ServiceInstance.class);
when(client.getInstances(anyString())).thenReturn(Arrays.asList(first, second));
assertThat(new DynamicServiceInstanceProvider(client, "service").getServiceInstance(), is(first));
}
}

View File

@@ -2,4 +2,7 @@ message: Hello scope!
delay: 0
debug: true
#logging.level.org.springframework.web: DEBUG
#logging.level.org.springframework.context.annotation: DEBUG
#logging.level.org.springframework.context.annotation: DEBUG
spring.cloud.hypermedia.refresh.initial-delay=50000
spring.cloud.hypermedia.refresh.fixed-delay=10000