diff --git a/pom.xml b/pom.xml index a73d6bec..adb813d1 100644 --- a/pom.xml +++ b/pom.xml @@ -157,6 +157,7 @@ spring-cloud-test-support spring-cloud-context spring-cloud-context-integration-tests + spring-cloud-context-webflux-integration-tests spring-cloud-commons spring-cloud-loadbalancer spring-cloud-starter diff --git a/spring-cloud-context-webflux-integration-tests/pom.xml b/spring-cloud-context-webflux-integration-tests/pom.xml new file mode 100644 index 00000000..780eef5e --- /dev/null +++ b/spring-cloud-context-webflux-integration-tests/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.springframework.cloud + spring-cloud-commons-parent + 2.1.5.BUILD-SNAPSHOT + .. + + spring-cloud-context-webflux-integration-tests + jar + spring-cloud-context-webflux-integration-tests + Spring Cloud Context Webflux Integration Tests + + + org.springframework.cloud + spring-cloud-context + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-test-support + test + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/spring-cloud-context-webflux-integration-tests/src/test/java/org/springframework/cloud/context/integration/RefreshScopeIntegrationTests.java b/spring-cloud-context-webflux-integration-tests/src/test/java/org/springframework/cloud/context/integration/RefreshScopeIntegrationTests.java new file mode 100644 index 00000000..f62f06b5 --- /dev/null +++ b/spring-cloud-context-webflux-integration-tests/src/test/java/org/springframework/cloud/context/integration/RefreshScopeIntegrationTests.java @@ -0,0 +1,268 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.context.integration; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.integration.RefreshScopeIntegrationTests.TestConfiguration; +import org.springframework.cloud.context.scope.GenericScope; +import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.BDDAssertions.then; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestConfiguration.class) +@SuppressWarnings("Duplicates") +public class RefreshScopeIntegrationTests { + + @Autowired + private Service service; + + @Autowired + private TestProperties properties; + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Before + public void init() { + then(ExampleService.getInitCount()).isEqualTo(1); + ExampleService.reset(); + } + + @After + public void close() { + ExampleService.reset(); + } + + @Test + @DirtiesContext + public void testSimpleProperties() throws Exception { + then(this.service.getMessage()).isEqualTo("Hello scope!"); + then(this.service instanceof Advised).isTrue(); + // Change the dynamic property source... + this.properties.setMessage("Foo"); + // ...but don't refresh, so the bean stays the same: + then(this.service.getMessage()).isEqualTo("Hello scope!"); + then(ExampleService.getInitCount()).isEqualTo(0); + then(ExampleService.getDestroyCount()).isEqualTo(0); + } + + @Test + @DirtiesContext + public void testRefresh() throws Exception { + then(this.service.getMessage()).isEqualTo("Hello scope!"); + String id1 = this.service.toString(); + // Change the dynamic property source... + this.properties.setMessage("Foo"); + // ...and then refresh, so the bean is re-initialized: + this.scope.refreshAll(); + String id2 = this.service.toString(); + then(this.service.getMessage()).isEqualTo("Foo"); + then(ExampleService.getInitCount()).isEqualTo(1); + then(ExampleService.getDestroyCount()).isEqualTo(1); + then(id2).isNotSameAs(id1); + then(ExampleService.event).isNotNull(); + then(ExampleService.event.getName()) + .isEqualTo(RefreshScopeRefreshedEvent.DEFAULT_NAME); + } + + @Test + @DirtiesContext + public void testRefreshBean() throws Exception { + then(this.service.getMessage()).isEqualTo("Hello scope!"); + String id1 = this.service.toString(); + // Change the dynamic property source... + this.properties.setMessage("Foo"); + // ...and then refresh, so the bean is re-initialized: + this.scope.refresh("service"); + String id2 = this.service.toString(); + then(this.service.getMessage()).isEqualTo("Foo"); + then(this.service.getMessage()).isEqualTo("Foo"); + then(ExampleService.getInitCount()).isEqualTo(1); + then(ExampleService.getDestroyCount()).isEqualTo(1); + then(id2).isNotSameAs(id1); + then(ExampleService.event).isNotNull(); + then(ExampleService.event.getName()) + .isEqualTo(GenericScope.SCOPED_TARGET_PREFIX + "service"); + } + + // see gh-349 + @Test(expected = ServiceException.class) + @DirtiesContext + public void testCheckedException() throws Exception { + this.service.throwsException(); + } + + public interface Service { + + String getMessage(); + + String throwsException() throws ServiceException; + + } + + public static class ExampleService implements Service, InitializingBean, + DisposableBean, ApplicationListener { + + private static Log logger = LogFactory.getLog(ExampleService.class); + + private volatile static int initCount = 0; + + private volatile static int destroyCount = 0; + + private volatile static RefreshScopeRefreshedEvent event; + + private String message = null; + + private volatile long delay = 0; + + public static void reset() { + initCount = 0; + destroyCount = 0; + event = null; + } + + public static int getInitCount() { + return initCount; + } + + public static int getDestroyCount() { + return destroyCount; + } + + public void setDelay(long delay) { + this.delay = delay; + } + + @Override + public void afterPropertiesSet() throws Exception { + logger.debug("Initializing message: " + this.message); + initCount++; + } + + @Override + public void destroy() throws Exception { + logger.debug("Destroying message: " + this.message); + destroyCount++; + this.message = null; + } + + @Override + public String getMessage() { + logger.debug("Getting message: " + this.message); + try { + Thread.sleep(this.delay); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.info("Returning message: " + this.message); + return this.message; + } + + public void setMessage(String message) { + logger.debug("Setting message: " + message); + this.message = message; + } + + @Override + public String throwsException() throws ServiceException { + throw new ServiceException(); + } + + @Override + public void onApplicationEvent(RefreshScopeRefreshedEvent e) { + event = e; + } + + } + + @SuppressWarnings("serial") + public static class ServiceException extends Exception { + + } + + @Configuration + @EnableConfigurationProperties(TestProperties.class) + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Autowired + private TestProperties properties; + + @Bean + @RefreshScope + public ExampleService service() { + ExampleService service = new ExampleService(); + service.setMessage(this.properties.getMessage()); + service.setDelay(this.properties.getDelay()); + return service; + } + + } + + @ConfigurationProperties + @ManagedResource + protected static class TestProperties { + + private String message; + + private int delay; + + @ManagedAttribute + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } + + @ManagedAttribute + public int getDelay() { + return this.delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + + } + +} diff --git a/spring-cloud-context-webflux-integration-tests/src/test/java/org/springframework/cloud/context/integration/webflux/RefreshEndpointIntegrationTests.java b/spring-cloud-context-webflux-integration-tests/src/test/java/org/springframework/cloud/context/integration/webflux/RefreshEndpointIntegrationTests.java new file mode 100644 index 00000000..dfb11dac --- /dev/null +++ b/spring-cloud-context-webflux-integration-tests/src/test/java/org/springframework/cloud/context/integration/webflux/RefreshEndpointIntegrationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.context.integration.webflux; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Dave Syer + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = RefreshEndpointIntegrationTests.ClientApp.class, properties = { + "management.endpoints.web.exposure.include=*" }, webEnvironment = RANDOM_PORT) +public class RefreshEndpointIntegrationTests { + + private static final String BASE_PATH = new WebEndpointProperties().getBasePath(); + + @LocalServerPort + private int port; + + @Test + public void webAccess() throws Exception { + TestRestTemplate template = new TestRestTemplate(); + ResponseEntity entity = template.postForEntity( + "http://localhost:" + this.port + BASE_PATH + "/refresh", null, + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Configuration + @EnableAutoConfiguration + protected static class ClientApp { + + } + +} diff --git a/spring-cloud-context-webflux-integration-tests/src/test/resources/application.properties b/spring-cloud-context-webflux-integration-tests/src/test/resources/application.properties new file mode 100644 index 00000000..10813755 --- /dev/null +++ b/spring-cloud-context-webflux-integration-tests/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.main.web-application-type=REACTIVE diff --git a/spring-cloud-context-webflux-integration-tests/src/test/resources/bootstrap.properties b/spring-cloud-context-webflux-integration-tests/src/test/resources/bootstrap.properties new file mode 100644 index 00000000..90319f84 --- /dev/null +++ b/spring-cloud-context-webflux-integration-tests/src/test/resources/bootstrap.properties @@ -0,0 +1 @@ +info.name:child diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java index 8856ea12..8e1e8069 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java @@ -182,6 +182,8 @@ public class ContextRefresher { Map map = new HashMap(); map.put("spring.jmx.enabled", false); map.put("spring.main.sources", ""); + // gh-678 without this apps with this property set to REACTIVE or SERVLET fail + map.put("spring.main.web-application-type", "NONE"); capturedPropertySources .addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map)); return environment;