From 9449edf93b64c691fc8511601402b7114bd6e3e0 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 21 Sep 2023 16:16:16 +0200 Subject: [PATCH] Refresh Scope on restart. (#1266) * Refresh Scope on restart. --- .../application-context-services.adoc | 6 ++ docs/modules/ROOT/partials/_configprops.adoc | 1 + .../RefreshAutoConfiguration.java | 8 ++ .../refresh/RefreshScopeLifecycle.java | 77 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 6 ++ .../RefreshAutoConfigurationTests.java | 26 +++++-- .../refresh/RefreshScopeLifecycleTests.java | 51 ++++++++++++ 7 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycle.java create mode 100644 spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycleTests.java diff --git a/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc b/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc index ef2b6d60..d2391fe0 100644 --- a/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc @@ -217,6 +217,12 @@ The configuration property must be present in order to update the value after a a value in your application you might want to switch your logic to rely on its absence instead. Another option would be to rely on the value changing rather than not being present in the application's configuration. +[refresh-scope-on-restart] +=== Refresh Scope on Restart + +In order to allow seamlessly refreshing beans on restart, which is especially useful for applications running with JVM Checkpoint Restore (for example with https://github.com/CRaC[Project CRaC]), we now instantiate a `RefreshScopeLifecycle` bean that will trigger Context Refresh on restart, resulting in rebinding configuration properties and refreshing any `@RefreshScope`-annotated beans. This behaviour can be disabled by setting the value of `spring.cloud.refresh.on-restart.enabled` to `false`. + + [[encryption-and-decryption]] == Encryption and Decryption diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 3faf91a6..4447b227 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -75,6 +75,7 @@ |spring.cloud.refresh.enabled | `+++true+++` | Enables autoconfiguration for the refresh scope and associated features. |spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional class names for beans to post process into refresh scope. |spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of class names for beans to never be refreshed or rebound. +|spring.cloud.refresh.on-restart.enabled | `+++true+++` | Enable refreshing context on start. |spring.cloud.service-registry.auto-registration.enabled | `+++true+++` | Whether service auto-registration is enabled. Defaults to true. |spring.cloud.service-registry.auto-registration.fail-fast | `+++false+++` | Whether startup fails if there is no AutoServiceRegistration. Defaults to false. |spring.cloud.service-registry.auto-registration.register-management | `+++true+++` | Whether to register the management as a service. Defaults to true. diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java index ecc7eb1f..7bba9649 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java @@ -43,6 +43,7 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.context.refresh.ConfigDataContextRefresher; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.cloud.context.refresh.LegacyContextRefresher; +import org.springframework.cloud.context.refresh.RefreshScopeLifecycle; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.endpoint.event.RefreshEventListener; import org.springframework.cloud.logging.LoggingRebinder; @@ -66,6 +67,7 @@ import org.springframework.util.StringUtils; * * @author Dave Syer * @author Venil Noronha + * @author Olga Maciaszek-Sharma */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RefreshScope.class) @@ -117,6 +119,12 @@ public class RefreshAutoConfiguration { return new ConfigDataContextRefresher(context, scope, properties); } + @ConditionalOnProperty(value = "spring.cloud.refresh.on-restart.enabled", matchIfMissing = true) + @Bean + RefreshScopeLifecycle refreshScopeLifecycle(ContextRefresher contextRefresher) { + return new RefreshScopeLifecycle(contextRefresher); + } + @Bean public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) { return new RefreshEventListener(contextRefresher); diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycle.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycle.java new file mode 100644 index 00000000..6b9d0fb5 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycle.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.refresh; + +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.Lifecycle; + +/** + * A {@link Lifecycle} implementation that triggers {@link ContextRefresher#refresh()} to + * be called on restart. + * + * @author Olga Maciaszek-Sharma + * @since 4.1.0 + */ +public class RefreshScopeLifecycle implements Lifecycle { + + private static final Log LOG = LogFactory.getLog(RefreshScopeLifecycle.class); + + private final ContextRefresher contextRefresher; + + private final Object lifecycleMonitor = new Object(); + + private volatile boolean running = true; + + public RefreshScopeLifecycle(ContextRefresher contextRefresher) { + this.contextRefresher = contextRefresher; + } + + @Override + public void start() { + synchronized (lifecycleMonitor) { + if (!isRunning()) { + if (LOG.isInfoEnabled()) { + LOG.info("Refreshing context on restart."); + } + Set keys = contextRefresher.refresh(); + if(LOG.isInfoEnabled()){ + LOG.info("Refreshed keys: " + keys); + } + } + running = true; + } + } + + @Override + public void stop() { + synchronized (lifecycleMonitor) { + if (isRunning()) { + running = false; + } + } + } + + @Override + public boolean isRunning() { + return running; + } + +} diff --git a/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 835dae75..448ce8fe 100644 --- a/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -53,6 +53,12 @@ "type": "java.lang.Boolean", "description": "Enable the DecryptEnvironmentPostProcessor.", "defaultValue": true + }, + { + "name": "spring.cloud.refresh.on-restart.enabled", + "type": "java.lang.Boolean", + "description": "Enable refreshing context on start.", + "defaultValue": true } ] } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java index 04bf003c..91b72018 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java @@ -22,11 +22,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.context.refresh.ContextRefresher; @@ -38,9 +40,10 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Dave Syer + * @author Olga Maciaszek-Sharma */ @ExtendWith(OutputCaptureExtension.class) -public class RefreshAutoConfigurationTests { +class RefreshAutoConfigurationTests { private static ConfigurableApplicationContext getApplicationContext(WebApplicationType type, Class configuration, String... properties) { @@ -49,7 +52,7 @@ public class RefreshAutoConfigurationTests { } @Test - public void noWarnings(CapturedOutput output) { + void noWarnings(CapturedOutput output) { try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class)) { then(context.containsBean("refreshScope")).isTrue(); then(output.toString()).doesNotContain("WARN"); @@ -57,7 +60,7 @@ public class RefreshAutoConfigurationTests { } @Test - public void disabled() { + void disabled() { try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.SERVLET, Config.class, "spring.cloud.refresh.enabled:false")) { then(context.containsBean("refreshScope")).isFalse(); @@ -65,7 +68,7 @@ public class RefreshAutoConfigurationTests { } @Test - public void refreshables() { + void refreshables() { try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class, "config.foo=bar", "spring.cloud.refresh.refreshable:" + SealedConfigProps.class.getName())) { context.getBean(SealedConfigProps.class); @@ -84,7 +87,7 @@ public class RefreshAutoConfigurationTests { } @Test - public void neverRefreshable() { + void neverRefreshable() { try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class, "countingconfig.foo=bar", "spring.cloud.refresh.never-refreshable:" + CountingConfigProps.class.getName())) { @@ -94,6 +97,19 @@ public class RefreshAutoConfigurationTests { } } + @Test + void refreshScopeLifecylePresentByDefault() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class)) + .run(context -> assertThat(context).hasBean("refreshScopeLifecycle")); + } + + @Test + void refreshScopeLifecyleDisabledWithProp() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class)) + .withPropertyValues("spring.cloud.refresh.on-restart.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean("refreshScopeLifecycle")); + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) @EnableConfigurationProperties({ SealedConfigProps.class, CountingConfigProps.class }) diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycleTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycleTests.java new file mode 100644 index 00000000..09eb3171 --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/refresh/RefreshScopeLifecycleTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.refresh; + +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link RefreshScopeLifecycle}. + * + * @author Olga Maciaszek-Sharma + */ +class RefreshScopeLifecycleTests { + + ContextRefresher contextRefresher = mock(); + + private final RefreshScopeLifecycle lifecycle = new RefreshScopeLifecycle(contextRefresher); + + @Test + void shouldRefreshContextOnRestart() { + lifecycle.stop(); + lifecycle.start(); + + verify(contextRefresher).refresh(); + } + + @Test + void shouldNotRefreshContextOnStart() { + lifecycle.start(); + + verifyNoInteractions(contextRefresher); + } + +}