From 2d54b54d8119b954192f402700d3f27573a6b95a Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 27 Jan 2014 17:02:01 +0000 Subject: [PATCH] Listen for parent close events and close child context If the context hierarchy is from a SpringApplication we can control the shutdown semantics a bit. Specifically we need a listener in the child context that will shut it down when the parent closes (since assummably the child relies on beans in the arent that may now be disposed). Fixes gh-275 --- .../endpoint/ShutdownParentEndpointTests.java | 90 +++++++++++++++++++ ...tContextApplicationContextInitializer.java | 32 ++++++- .../listener/ParentContextCloserListener.java | 74 +++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + 4 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShutdownParentEndpointTests.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/listener/ParentContextCloserListener.java diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShutdownParentEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShutdownParentEndpointTests.java new file mode 100644 index 0000000000..c782dfe335 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShutdownParentEndpointTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2013 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.boot.actuate.endpoint; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link ShutdownEndpoint}. + * + * @author Dave Syer + */ +public class ShutdownParentEndpointTests { + + private ConfigurableApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void shutdownChild() throws Exception { + this.context = new SpringApplicationBuilder(Config.class).child(Empty.class) + .web(false).run(); + assertThat((String) getEndpointBean().invoke().get("message"), + startsWith("Shutting down")); + assertTrue(this.context.isActive()); + Thread.sleep(600); + assertFalse(this.context.isActive()); + } + + @Test + public void shutdownParent() throws Exception { + this.context = new SpringApplicationBuilder(Empty.class).child(Config.class) + .web(false).run(); + assertThat((String) getEndpointBean().invoke().get("message"), + startsWith("Shutting down")); + assertTrue(this.context.isActive()); + Thread.sleep(600); + assertFalse(this.context.isActive()); + } + + private ShutdownEndpoint getEndpointBean() { + return this.context.getBean(ShutdownEndpoint.class); + } + + @Configuration + @EnableConfigurationProperties + public static class Config { + + @Bean + public ShutdownEndpoint endpoint() { + ShutdownEndpoint endpoint = new ShutdownEndpoint(); + endpoint.setEnabled(true); + return endpoint; + } + + } + + @Configuration + public static class Empty { + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/initializer/ParentContextApplicationContextInitializer.java b/spring-boot/src/main/java/org/springframework/boot/context/initializer/ParentContextApplicationContextInitializer.java index 719de7ce82..4cd5d70381 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/initializer/ParentContextApplicationContextInitializer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/initializer/ParentContextApplicationContextInitializer.java @@ -18,16 +18,22 @@ package org.springframework.boot.context.initializer; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.Ordered; /** - * {@link ApplicationContextInitializer} for setting the parent context. + * {@link ApplicationContextInitializer} for setting the parent context. Also publishes + * {@link ParentContextAvailableEvent} when the context is refreshed to signal to other + * listeners that the context is available and has a parent. * * @author Dave Syer */ public class ParentContextApplicationContextInitializer implements - ApplicationContextInitializer, Ordered { + ApplicationContextInitializer, + ApplicationListener, Ordered { private int order = Integer.MIN_VALUE; @@ -46,9 +52,31 @@ public class ParentContextApplicationContextInitializer implements return this.order; } + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + ApplicationContext context = event.getApplicationContext(); + if (context instanceof ConfigurableApplicationContext) { + context.publishEvent(new ParentContextAvailableEvent( + (ConfigurableApplicationContext) context)); + } + } + @Override public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.setParent(this.parent); } + public static class ParentContextAvailableEvent extends ApplicationEvent { + + public ParentContextAvailableEvent( + ConfigurableApplicationContext applicationContext) { + super(applicationContext); + } + + public ConfigurableApplicationContext getApplicationContext() { + return (ConfigurableApplicationContext) getSource(); + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/listener/ParentContextCloserListener.java b/spring-boot/src/main/java/org/springframework/boot/context/listener/ParentContextCloserListener.java new file mode 100644 index 0000000000..afe631646d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/listener/ParentContextCloserListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2013 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.boot.context.listener; + +import org.springframework.boot.context.initializer.ParentContextApplicationContextInitializer.ParentContextAvailableEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.core.Ordered; + +/** + * Listener that closes the application context if its parent is closed. It listens for + * refresh events and grabs the current context from there, and then listens for closed + * events and propagates it down the hierarchy. + * + * @author Dave Syer + */ +public class ParentContextCloserListener implements + ApplicationListener, Ordered { + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public void onApplicationEvent(ParentContextAvailableEvent event) { + maybeInstallListenerInParent(event.getApplicationContext()); + } + + private void maybeInstallListenerInParent(ConfigurableApplicationContext child) { + if (child.getParent() instanceof ConfigurableApplicationContext) { + ConfigurableApplicationContext parent = (ConfigurableApplicationContext) child + .getParent(); + parent.addApplicationListener(new ContextCloserListener(child)); + } + } + + protected static class ContextCloserListener implements + ApplicationListener { + + private ConfigurableApplicationContext context; + + public ContextCloserListener(ConfigurableApplicationContext context) { + this.context = context; + + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + if (this.context != null + && event.getApplicationContext() == this.context.getParent() + && this.context.isActive()) { + this.context.close(); + } + } + + } + +} \ No newline at end of file diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index f1f56e8994..02303c4140 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -11,5 +11,6 @@ org.springframework.boot.context.listener.EnvironmentDelegateApplicationListener org.springframework.boot.context.listener.FileEncodingApplicationListener,\ org.springframework.boot.context.listener.LoggingApplicationListener,\ org.springframework.boot.context.listener.VcapApplicationListener,\ +org.springframework.boot.context.listener.ParentContextCloserListener,\ org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer