Merge branch '2.0.x'

This commit is contained in:
Spencer Gibb
2018-10-22 16:00:25 -04:00
10 changed files with 246 additions and 36 deletions

View File

@@ -313,6 +313,15 @@ To disable that behavior, you can set:
* `@EnableDiscoveryClient(autoRegister=false)` to permanently disable auto-registration.
* `spring.cloud.service-registry.auto-registration.enabled=false` to disable the behavior through configuration.
===== ServiceRegistry Auto-Registration Events
There are two events that will be fired when a service auto-registers. The first event, called
`InstancePreRegisteredEvent`, is fired before the service is registered. The second
event, called `InstanceRegisteredEvent`, is fired after the service is registered. You can register an
`ApplicationListener`(s) to listen to and react to these events.
NOTE: These events will not be fired if `spring.cloud.service-registry.auto-registration.enabled` is set to `false`.
==== Service Registry Actuator Endpoint
Spring Cloud Commons provides a `/service-registry` actuator endpoint.
@@ -399,15 +408,15 @@ You can use `client.ribbon.MaxAutoRetries`, `client.ribbon.MaxAutoRetriesNextSer
If you would like to disable the retry logic with Spring Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
See the https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties[Ribbon documentation] for a description of what these properties do.
If you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedBackOffPolicyFactory` and return the `BackOffPolicy` you would like to use for a given service, as shown in the following example:
If you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy` method:
[source,java,indent=0]
----
@Configuration
public class MyConfiguration {
@Bean
LoadBalancedBackOffPolicyFactory backOffPolciyFactory() {
return new LoadBalancedBackOffPolicyFactory() {
LoadBalancedRetryFactory retryFactory() {
return new LoadBalancedRetryFactory() {
@Override
public BackOffPolicy createBackOffPolicy(String service) {
return new ExponentialBackOffPolicy();

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2018 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.discovery.event;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.context.ApplicationEvent;
/**
* An event to fire before a service is registered.
* @author Ryan Baxter
*/
public class InstancePreRegisteredEvent extends ApplicationEvent {
private Registration registration;
/**
* Create a new pre registration event.
*
* @param source the object on which the event initially occurred (never {@code null})
*/
public InstancePreRegisteredEvent(Object source, Registration registration) {
super(source);
this.registration = registration;
}
/**
* Get the registration data.
* @return the registration data
*/
public Registration getRegistration() {
return registration;
}
}

View File

@@ -2,9 +2,11 @@ package org.springframework.cloud.client.loadbalancer.reactive;
import java.net.URI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.util.Assert;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
@@ -14,9 +16,13 @@ import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
* @author Ryan Baxter
*/
public class LoadBalancerExchangeFilterFunction implements ExchangeFilterFunction {
private static Log logger = LogFactory
.getLog(LoadBalancerExchangeFilterFunction.class);
private final LoadBalancerClient loadBalancerClient;
public LoadBalancerExchangeFilterFunction(LoadBalancerClient loadBalancerClient) {
@@ -27,10 +33,18 @@ public class LoadBalancerExchangeFilterFunction implements ExchangeFilterFunctio
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI originalUrl = request.url();
String serviceId = originalUrl.getHost();
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUrl);
if(serviceId == null) {
String msg = String.format("Request URI does not contain a valid hostname: %s", originalUrl.toString());
logger.warn(msg);
return Mono.just(ClientResponse.create(HttpStatus.BAD_REQUEST).body(msg).build());
}
//TODO: reactive lb client
ServiceInstance instance = this.loadBalancerClient.choose(serviceId);
if(instance == null) {
String msg = String.format("Load balancer does not contain an instance for the service %s", serviceId);
logger.warn(msg);
return Mono.just(ClientResponse.create(HttpStatus.SERVICE_UNAVAILABLE).body(msg).build());
}
URI uri = this.loadBalancerClient.reconstructURI(instance, originalUrl);
ClientRequest newRequest = ClientRequest.method(request.method(), uri)
.headers(headers -> headers.addAll(request.headers()))

View File

@@ -4,7 +4,6 @@ import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2013-2018 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.loadbalancer.reactive;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Callback interface that can be used to customize a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder}.
*
* See original {@link org.springframework.boot.web.reactive.function.client.WebClientCustomizer}
*
* @author Brian Clozel
* @since 2.1.0
*/
@FunctionalInterface
public interface WebClientCustomizer {
/**
* Callback to customize a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder} instance.
* @param webClientBuilder the client builder to customize
*/
void customize(WebClient.Builder webClientBuilder);
}

View File

@@ -12,6 +12,7 @@ import org.springframework.beans.BeansException;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.cloud.client.discovery.ManagementServerPortUtils;
import org.springframework.cloud.client.discovery.event.InstancePreRegisteredEvent;
import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -113,6 +114,7 @@ public abstract class AbstractAutoServiceRegistration<R extends Registration>
// only initialize if nonSecurePort is greater than 0 and it isn't already running
// because of containerPortInitializer below
if (!this.running.get()) {
this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
register();
if (shouldRegisterManagement()) {
registerManagement();

View File

@@ -22,9 +22,11 @@ import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperti
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
@@ -33,6 +35,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
/**
* @author Spencer Gibb
* @author Ryan Baxter
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -68,6 +71,26 @@ public class LoadBalancerExchangeFilterFunctionTests {
assertThat(value).isEqualTo("Hello World");
}
@Test
public void testNoInstance() {
ClientResponse clientResponse = WebClient.builder()
.baseUrl("http://foobar")
.filter(lbFunction)
.build()
.get().exchange().block();
assertThat(clientResponse.statusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
}
@Test
public void testNoHostName() {
ClientResponse clientResponse = WebClient.builder()
.baseUrl("http:///foobar")
.filter(lbFunction)
.build()
.get().exchange().block();
assertThat(clientResponse.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@EnableDiscoveryClient
@EnableAutoConfiguration
@SpringBootConfiguration
@@ -106,6 +129,9 @@ public class LoadBalancerExchangeFilterFunctionTests {
@Override
public ServiceInstance choose(String serviceId) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if(instances.size() == 0) {
return null;
}
int instanceIdx = random.nextInt(instances.size());
return instances.get(instanceIdx);
}

View File

@@ -16,9 +16,17 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
@@ -34,12 +42,6 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.assertj.core.api.Assertions.assertThat;
@@ -90,20 +92,29 @@ public class ReactiveLoadBalancerAutoConfigurationTests {
assertThat(getFilters(two.nonLoadBalanced)).isNullOrEmpty();
}
@Test
public void noCustomWebClientBuilders() {
ConfigurableApplicationContext context = init(NoWebClientBuilder.class);
final Map<String, WebClient.Builder> webClientBuilders = context
.getBeansOfType(WebClient.Builder.class);
assertThat(webClientBuilders).hasSize(1);
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
assertThat(builder).isNotNull();
assertThat(getFilters(builder)).isNullOrEmpty();
}
protected ConfigurableApplicationContext init(Class<?> config) {
return new SpringApplicationBuilder().web(WebApplicationType.NONE)
// .properties("spring.aop.proxyTargetClass=true")
.sources(config, ReactiveLoadBalancerAutoConfiguration.class).run();
.sources(config, WebClientAutoConfiguration.class,
ReactiveLoadBalancerAutoConfiguration.class).run();
}
@Configuration
protected static class OneWebClientBuilder {
@Bean
@LoadBalanced
WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
protected static class NoWebClientBuilder {
@Bean
LoadBalancerClient loadBalancerClient() {
@@ -116,7 +127,18 @@ public class ReactiveLoadBalancerAutoConfigurationTests {
}
@Configuration
protected static class TwoWebClientBuilders {
protected static class OneWebClientBuilder extends NoWebClientBuilder {
@Bean
@LoadBalanced
WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
@Configuration
protected static class TwoWebClientBuilders extends OneWebClientBuilder {
@Primary
@Bean
@@ -124,17 +146,6 @@ public class ReactiveLoadBalancerAutoConfigurationTests {
return WebClient.builder();
}
@LoadBalanced
@Bean
WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
@Bean
LoadBalancerClient loadBalancerClient() {
return new NoopLoadBalancerClient();
}
@Configuration
protected static class Two {
@Autowired

View File

@@ -11,8 +11,12 @@ import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagement
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.discovery.event.InstancePreRegisteredEvent;
import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import static org.hamcrest.Matchers.instanceOf;
@@ -34,6 +38,12 @@ public class AbstractAutoServiceRegistrationTests {
@Autowired
private TestAutoServiceRegistration autoRegistration;
@Autowired
private PreEventListener preEventListener;
@Autowired
public PostEventListener postEventListener;
@LocalServerPort
private int port;
@@ -52,6 +62,14 @@ public class AbstractAutoServiceRegistrationTests {
assertEquals("Lifecycle appName is wrong", "application", autoRegistration.getAppName());
}
@Test
public void eventsFireTest() {
assertTrue(preEventListener.wasFired);
assertEquals("testRegistration2", preEventListener.registration.getServiceId());
assertTrue(postEventListener.wasFired);
assertEquals("testRegistration2", postEventListener.config.getServiceId());
}
@EnableAutoConfiguration
@Configuration
public static class Config {
@@ -59,6 +77,36 @@ public class AbstractAutoServiceRegistrationTests {
public TestAutoServiceRegistration testAutoServiceRegistration() {
return new TestAutoServiceRegistration();
}
@Bean
public PreEventListener preRegisterListener() {
return new PreEventListener();
}
@Bean
public PostEventListener postEventListener() {
return new PostEventListener();
}
}
public static class PreEventListener implements ApplicationListener<InstancePreRegisteredEvent> {
public boolean wasFired = false;
public Registration registration;
@Override
public void onApplicationEvent(InstancePreRegisteredEvent event) {
this.registration = event.getRegistration();
this.wasFired = true;
}
}
public static class PostEventListener implements ApplicationListener<InstanceRegisteredEvent> {
public boolean wasFired = false;
public Registration config;
@Override
public void onApplicationEvent(InstanceRegisteredEvent event) {
this.config = (Registration)event.getConfig();
this.wasFired = true;
}
}
public static class TestRegistration implements Registration {
@@ -188,7 +236,7 @@ public class AbstractAutoServiceRegistrationTests {
@Override
protected Object getConfiguration() {
return null;
return getRegistration();
}
@Override

View File

@@ -173,13 +173,24 @@ public class BootstrapApplicationListener
// Don't use the default properties in this builder
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
final SpringApplication builderApplication = builder.application();
if(builderApplication.getMainApplicationClass() == null){
// gh_425:
// SpringApplication cannot deduce the MainApplicationClass here
// if it is booted from SpringBootServletInitializer due to the
// absense of the "main" method in stackTraces.
// But luckily this method's second parameter "application" here
// carries the real MainApplicationClass which has been explicitly
// set by SpringBootServletInitializer itself already.
builder.main(application.getMainApplicationClass());
}
if (environment.getPropertySources().contains("refreshArgs")) {
// If we are doing a context refresh, really we only want to refresh the
// Environment, and there are some toxic listeners (like the
// LoggingApplicationListener) that affect global static state, so we need a
// way to switch those off.
builder.application()
.setListeners(filterListeners(builder.application().getListeners()));
builderApplication
.setListeners(filterListeners(builderApplication.getListeners()));
}
List<Class<?>> sources = new ArrayList<>();
for (String name : names) {