Merge pull request #1749 from bijukunjummen/GH-1334

GH-1334: Support for eagerly initializing zuul Ribbon contexts
This commit is contained in:
Ryan Baxter
2017-03-15 11:46:34 -04:00
committed by GitHub
11 changed files with 587 additions and 3 deletions

View File

@@ -864,6 +864,21 @@ public class MyClass {
}
----
[[ribbon-child-context-eager-load]]
=== Caching of Ribbon Configuration
Each Ribbon named client has a corresponding child Application Context that Spring Cloud maintains, this application context is lazily loaded up on the first request to the named client.
This lazy loading behavior can be changed to instead eagerly load up these child Application contexts at startup by specifying the names of the Ribbon clients.
.application.yml
----
ribbon:
eager-load:
enabled: true
clients: client1, client2, client3
----
[[spring-cloud-feign]]
== Declarative REST Client: Feign
@@ -2041,6 +2056,18 @@ public class AddResponseHeaderFilter extends ZuulFilter {
If an exception is thrown during any portion of the Zuul filter lifecycle, the error filters are executed. The `SendErrorFilter` is only run if `RequestContext.getThrowable()` is not `null`. It then sets specific `javax.servlet.error.*` attributes in the request and forwards the request to the Spring Boot error page.
==== Zuul Eager Application Context Loading
Zuul internally uses Ribbon for calling the remote url's and Ribbon clients are by default lazily loaded up by Spring Cloud on first call.
This behavior can be changed for Zuul using the following configuration and will result in the child Ribbon related Application contexts being eagerly loaded up at application startup time.
.application.yml
----
zuul:
ribbon:
eager-load:
enabled: true
----
== Polyglot support with Sidecar

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2017 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.netflix.ribbon;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import java.util.List;
/**
* Responsible for eagerly creating the child application context holding the Ribbon
* related configuration
*
* @author Biju Kunjummen
*/
public class RibbonApplicationContextInitializer
implements ApplicationListener<ApplicationReadyEvent> {
private final SpringClientFactory springClientFactory;
//List of Ribbon client names
private final List<String> clientNames;
public RibbonApplicationContextInitializer(SpringClientFactory springClientFactory,
List<String> clientNames) {
this.springClientFactory = springClientFactory;
this.clientNames = clientNames;
}
private void initialize() {
if (clientNames != null) {
for (String clientName : clientNames) {
this.springClientFactory.getContext(clientName);
}
}
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
initialize();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2017 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.
@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory;
@@ -53,16 +54,21 @@ import com.netflix.ribbon.Ribbon;
*
* @author Spencer Gibb
* @author Dave Syer
* @author Biju Kunjummen
*/
@Configuration
@ConditionalOnClass({ IClient.class, RestTemplate.class, AsyncRestTemplate.class, Ribbon.class})
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties(RibbonEagerLoadProperties.class)
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Autowired
private RibbonEagerLoadProperties ribbonEagerLoadProperties;
@Bean
public HasFeatures ribbonFeature() {
@@ -99,6 +105,13 @@ public class RibbonAutoConfiguration {
public PropertiesFactory propertiesFactory() {
return new PropertiesFactory();
}
@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled", matchIfMissing = false)
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),
ribbonEagerLoadProperties.getClients());
}
@Configuration
@ConditionalOnClass(HttpRequest.class)

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017 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.netflix.ribbon;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/*
* Configuration Properties to indicate which Ribbon configurations
* should be eagerly loaded up
*
* @author Biju Kunjummen
*/
@ConfigurationProperties(prefix = "ribbon.eager-load")
public class RibbonEagerLoadProperties {
private boolean enabled = false;
private List<String> clients;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2017 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.
@@ -115,5 +115,10 @@ public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecifi
return instantiateWithConfig(getContext(name), type, config);
}
@Override
protected AnnotationConfigApplicationContext getContext(String name) {
return super.getContext(name);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2017 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.
@@ -22,6 +22,7 @@ import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
@@ -31,6 +32,7 @@ import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
import org.springframework.cloud.client.discovery.event.HeartbeatMonitor;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.zuul.filters.CompositeRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
@@ -58,6 +60,7 @@ import com.netflix.zuul.http.ZuulServlet;
/**
* @author Spencer Gibb
* @author Dave Syer
* @author Biju Kunjummen
*/
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@@ -161,6 +164,14 @@ public class ZuulConfiguration {
return new SendForwardFilter();
}
@Bean
@ConditionalOnProperty(value = "zuul.ribbon.eager-load.enabled", matchIfMissing = false)
public ZuulRouteApplicationContextInitializer zuulRoutesApplicationContextInitiazer(
SpringClientFactory springClientFactory) {
return new ZuulRouteApplicationContextInitializer(springClientFactory,
zuulProperties);
}
@Configuration
protected static class ZuulFilterConfiguration {

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2017 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.netflix.zuul;
import org.springframework.cloud.netflix.ribbon.RibbonApplicationContextInitializer;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Responsible for taking in the list of registered serviceid's (Ribbon client names)
* and creating the Spring {@link org.springframework.context.ApplicationContext} on
* start-up
*
* @author Biju Kunjummen
*/
public class ZuulRouteApplicationContextInitializer extends
RibbonApplicationContextInitializer {
public ZuulRouteApplicationContextInitializer(SpringClientFactory springClientFactory,
ZuulProperties zuulProperties) {
super(springClientFactory, getServiceIdsFromZuulProps(zuulProperties));
}
private static List<String> getServiceIdsFromZuulProps(ZuulProperties zuulProperties) {
Map<String, ZuulProperties.ZuulRoute> zuulRoutes = zuulProperties.getRoutes();
Collection<ZuulProperties.ZuulRoute> registeredRoutes = zuulRoutes.values();
List<String> serviceIds = new ArrayList<>();
if (registeredRoutes != null) {
for (ZuulProperties.ZuulRoute route: registeredRoutes) {
String serviceId = route.getServiceId();
if (serviceId != null) {
serviceIds.add(serviceId);
}
}
}
return serviceIds;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2017 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.netflix.ribbon;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Biju Kunjummen
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {RibbonAutoConfiguration.class,
ArchaiusAutoConfiguration.class, RibbonApplicationContextInitializerTests.RibbonInitializerConfig.class})
@DirtiesContext
public class RibbonApplicationContextInitializerTests {
@Autowired
private SpringClientFactory springClientFactory;
@Test
public void testContextShouldInitalizeChildContexts() {
// Context should have been initialized and an instance of Foo created
assertThat(Foo.getInstanceCount()).isEqualTo(1);
ApplicationContext ctx = springClientFactory.getContext("testspec");
assertThat(Foo.getInstanceCount()).isEqualTo(1);
Foo foo = ctx.getBean("foo", Foo.class);
assertThat(foo).isNotNull();
}
static class FooConfig {
@Bean
public Foo foo() {
return new Foo();
}
}
@Configuration
@RibbonClient(name="testspec", configuration = FooConfig.class)
static class RibbonInitializerConfig {
@Bean
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer(
SpringClientFactory springClientFactory) {
return new RibbonApplicationContextInitializer(springClientFactory,
Arrays.asList("testspec"));
}
}
static class Foo {
private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger();
public Foo() {
INSTANCE_COUNT.incrementAndGet();
}
public static int getInstanceCount() {
return INSTANCE_COUNT.get();
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017 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.netflix.ribbon;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Biju Kunjummen
*/
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
"ribbon.eager-load.enabled=true",
"ribbon.eager-load.clients=testspec1,testspec2"
})
@DirtiesContext
public class RibbonClientsEagerInitializationTest {
@Test
public void contextsShouldBeInitialized() {
assertThat(Foo1.getInstanceCount()).isEqualTo(2);
}
static class FooConfig {
@Bean
public Foo1 foo() {
return new Foo1();
}
}
@Configuration
@EnableAutoConfiguration
@RibbonClients(
value = {
@RibbonClient(name="testspec1", configuration = FooConfig.class),
@RibbonClient(name="testspec2", configuration = FooConfig.class),
@RibbonClient(name="testspec3", configuration = FooConfig.class),
})
static class RibbonConfig {
}
static class Foo1 {
private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger();
public Foo1() {
INSTANCE_COUNT.incrementAndGet();
}
public static int getInstanceCount() {
return INSTANCE_COUNT.get();
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2017 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.netflix.zuul.filters.route;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(value = { "zuul.routes.myroute.service-id=eager",
"zuul.ribbon.eager-load.enabled=true" })
@DirtiesContext
public class EagerLoadOfZuulConfigurationTests {
@Test
public void testEagerLoading() {
// Child context FooConfig should have been eagerly instantiated..
assertThat(Foo.getInstanceCount()).isEqualTo(1);
}
@EnableAutoConfiguration
@Configuration
@EnableZuulProxy
@RibbonClients(@RibbonClient(name = "eager", configuration = FooConfig.class))
static class TestConfig {
}
static class Foo {
private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger();
public Foo() {
INSTANCE_COUNT.incrementAndGet();
}
public static int getInstanceCount() {
return INSTANCE_COUNT.get();
}
}
static class FooConfig {
@Bean
public Foo foo() {
return new Foo();
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2017 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.netflix.zuul.filters.route;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, value = {
"zuul.routes.myroute.service-id=eager", "zuul.routes.myroute.path=/eager/**" })
@DirtiesContext
public class LazyLoadOfZuulConfigurationTests {
@Value("${local.server.port}")
protected int port;
@Test
public void testEagerLoading() {
// Child context FooConfig should be lazily created..
assertThat(Foo.getInstanceCount()).isEqualTo(0);
String uri = String.format("http://localhost:%d/eager/sample", this.port);
ResponseEntity<String> result = new TestRestTemplate().getForEntity(uri,
String.class);
// the instance should be available now..
assertThat(Foo.getInstanceCount()).isEqualTo(1);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("sample");
}
@EnableAutoConfiguration
@Configuration
@EnableZuulProxy
@RibbonClients(@RibbonClient(name = "eager", configuration = FooConfig.class))
static class TestConfig {
}
static class Foo {
private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger();
public Foo() {
INSTANCE_COUNT.incrementAndGet();
}
public static int getInstanceCount() {
return INSTANCE_COUNT.get();
}
}
static class FooConfig {
@Bean
public Foo foo() {
return new Foo();
}
@Value("${local.server.port}")
private int port;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
@Configuration
@RestController
static class SampleWebConfig {
@RequestMapping("/sample")
public String sampleEndpoint() {
return "sample";
}
}
}