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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user