diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index dacac0e5..f9961236 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -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 diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonApplicationContextInitializer.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonApplicationContextInitializer.java new file mode 100644 index 00000000..1c4881a6 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonApplicationContextInitializer.java @@ -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 { + + private final SpringClientFactory springClientFactory; + + //List of Ribbon client names + private final List clientNames; + + public RibbonApplicationContextInitializer(SpringClientFactory springClientFactory, + List 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(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java index ca3a2714..3eb7737e 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java @@ -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 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) diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonEagerLoadProperties.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonEagerLoadProperties.java new file mode 100644 index 00000000..4589edb3 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonEagerLoadProperties.java @@ -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 clients; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getClients() { + return clients; + } + + public void setClients(List clients) { + this.clients = clients; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/SpringClientFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/SpringClientFactory.java index 7dbccf5d..1f9dcdda 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/SpringClientFactory.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/SpringClientFactory.java @@ -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 getServiceIdsFromZuulProps(ZuulProperties zuulProperties) { + Map zuulRoutes = zuulProperties.getRoutes(); + Collection registeredRoutes = zuulRoutes.values(); + List serviceIds = new ArrayList<>(); + if (registeredRoutes != null) { + for (ZuulProperties.ZuulRoute route: registeredRoutes) { + String serviceId = route.getServiceId(); + if (serviceId != null) { + serviceIds.add(serviceId); + } + } + } + return serviceIds; + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonApplicationContextInitializerTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonApplicationContextInitializerTests.java new file mode 100644 index 00000000..9dc129b4 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonApplicationContextInitializerTests.java @@ -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(); + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientsEagerInitializationTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientsEagerInitializationTest.java new file mode 100644 index 00000000..55fc9676 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonClientsEagerInitializationTest.java @@ -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(); + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/EagerLoadOfZuulConfigurationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/EagerLoadOfZuulConfigurationTests.java new file mode 100644 index 00000000..2f14ea83 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/EagerLoadOfZuulConfigurationTests.java @@ -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(); + } + + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/LazyLoadOfZuulConfigurationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/LazyLoadOfZuulConfigurationTests.java new file mode 100644 index 00000000..003f4acf --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/LazyLoadOfZuulConfigurationTests.java @@ -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 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 ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } + + } + + @Configuration + @RestController + static class SampleWebConfig { + + @RequestMapping("/sample") + public String sampleEndpoint() { + return "sample"; + } + + } +}