From 321728d7d5aaf324b87b6ed4f5c60a135decb6b9 Mon Sep 17 00:00:00 2001 From: John Blum Date: Wed, 19 Feb 2014 19:01:35 -0800 Subject: [PATCH] Added appropriate synchronization around application event notification using the Spring ApplicationContext and ApplicationEventMulticaster. In addition, added support for late ContextRefreshedEvent notifications in the case where ApplicationListeners are registered after the ConfigurableApplicationContext has already been constructed, configured, initialized and refreshed. This latest feature is meant to support updates planned in the 7.5 GemFire release to the Gfsh 'deploy' command dynamically deploying and initializing Declarables in the JAR file. It is possible that Declarables are created and initialized in an unspecified order given there is not equivalent to Spring's Ordered interface in GemFire. --- ...SpringContextBootstrappingInitializer.java | 57 ++++++++++--- ...ngContextBootstrappingInitializerTest.java | 79 ++++++++++++++++--- 2 files changed, 117 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java b/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java index 1fb8b571..11cafc20 100644 --- a/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java +++ b/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java @@ -66,7 +66,9 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic /* package-private */ static volatile ConfigurableApplicationContext applicationContext; // TODO consider whether I should register a TaskExecutor to perform the event notifications in a separate Thread??? - private static ApplicationEventMulticaster eventNotifier = new SimpleApplicationEventMulticaster(); + private static final ApplicationEventMulticaster eventNotifier = new SimpleApplicationEventMulticaster(); + + private static ContextRefreshedEvent contextRefreshedEvent; /** * Gets a reference to the Spring ApplicationContext constructed, configured and initialized inside the GemFire @@ -91,14 +93,41 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @return the reference to the ApplicationListener for method call chaining purposes. * @see #unregister(org.springframework.context.ApplicationListener) * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.event.ContextRefreshedEvent * @see org.springframework.context.event.SimpleApplicationEventMulticaster * #addApplicationListener(org.springframework.context.ApplicationListener) */ - public static T register(final T listener) { - eventNotifier.addApplicationListener(listener); + public static > T register(final T listener) { + synchronized (eventNotifier) { + eventNotifier.addApplicationListener(listener); + notifyListenerOfExistingContextRefreshedEvent(listener); + } + return listener; } + /** + * Notifies any Spring ApplicationListeners of a current and existing ContextRefreshedEvent if the + * ApplicationContext was previously created, initialized and refreshed before any ApplicationListeners interested + * in ContextRefreshedEvents get registered so that application components (such as LazyWiringDeclarableSupport + * objects) arriving late to the game that also require configuration (auto-wiring) get wired accordingly too. + *

+ * @param listener a Spring ApplicationListener requiring notification of any ContextRefreshedEvents after the + * ApplicationContext has already been created, initialized and/or refreshed. + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + * @see org.springframework.context.event.ContextRefreshedEvent + */ + protected static void notifyListenerOfExistingContextRefreshedEvent( + final ApplicationListener listener) { + synchronized (eventNotifier) { + // NOTE the null check on the ApplicationContext is not absolutely necessary, but is an extra safety + // precaution none-the-less. + if (applicationContext != null && contextRefreshedEvent != null) { + listener.onApplicationEvent(contextRefreshedEvent); + } + } + } + /** * Unregisters the Spring ApplicationListener from this SpringContextBootstrappingInitializer in order to stop * receiving ApplicationEvents on Spring context refreshes. @@ -109,11 +138,15 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @return the reference to the ApplicationListener for method call chaining purposes. * @see #register(org.springframework.context.ApplicationListener) * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.event.ContextRefreshedEvent * @see org.springframework.context.event.SimpleApplicationEventMulticaster * #removeApplicationListener(org.springframework.context.ApplicationListener) */ - public static T unregister(final T listener) { - eventNotifier.removeApplicationListener(listener); + public static > T unregister(final T listener) { + synchronized (eventNotifier) { + eventNotifier.removeApplicationListener(listener); + } + return listener; } @@ -131,7 +164,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.support.ClassPathXmlApplicationContext */ protected ConfigurableApplicationContext createApplicationContext(final String[] configLocations) { - Assert.notEmpty(configLocations, "The configLocations must be specified to construct an instance" + Assert.notEmpty(configLocations, "'configLocations' must be specified to construct an instance" + " of the ClassPathXmlApplicationContext."); return createApplicationContext(null, configLocations); } @@ -185,6 +218,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see java.util.Properties */ @Override + @SuppressWarnings("null") public void init(final Properties parameters) { String basePackages = parameters.getProperty(BASE_PACKAGES_PARAMETER); String contextConfigLocations = parameters.getProperty(CONTEXT_CONFIG_LOCATIONS_PARAMETER); @@ -204,13 +238,15 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic nullSafeGetApplicationContextId(applicationContext))); applicationContext = createApplicationContext(basePackagesArray, configLocations); + Assert.notNull(applicationContext, "The 'created' ConfigurableApplicationContext cannot be null!"); applicationContext.addApplicationListener(this); applicationContext.registerShutdownHook(); applicationContext.refresh(); Assert.state(applicationContext.isActive(), String.format( - "The Spring application context (%1$s) has failed to be properly initialized with the following config files (%2$s)!", - nullSafeGetApplicationContextId(applicationContext), Arrays.toString(configLocations))); + "The Spring application context (%1$s) has failed to be properly initialized with the following config files (%2$s) or base packages (%3$s)!", + nullSafeGetApplicationContextId(applicationContext), Arrays.toString(configLocations), + Arrays.toString(basePackagesArray))); } } @@ -238,7 +274,10 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic */ @Override public void onApplicationEvent(final ContextRefreshedEvent event) { - eventNotifier.multicastEvent(event); + synchronized (eventNotifier) { + contextRefreshedEvent = event; + eventNotifier.multicastEvent(event); + } } } diff --git a/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java b/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java index 5e236b7b..77bb843d 100644 --- a/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java @@ -47,10 +47,12 @@ import org.springframework.util.ObjectUtils; * @author John Blum * @see org.junit.Test * @see org.mockito.Mockito + * @see org.springframework.context.ConfigurableApplicationContext * @see org.springframework.data.gemfire.support.SpringContextBootstrappingInitializer * @see org.springframework.data.gemfire.support.SpringContextBootstrappingInitializerIntegrationTest * @since 1.3.4 */ +@SuppressWarnings("unused") public class SpringContextBootstrappingInitializerTest { protected static Properties createParameters(final String parameter, final String value) { @@ -59,6 +61,11 @@ public class SpringContextBootstrappingInitializerTest { return parameters; } + protected static Properties createParameters(final Properties parameters, final String parameter, final String value) { + parameters.setProperty(parameter, value); + return parameters; + } + @After public void tearDown() { SpringContextBootstrappingInitializer.applicationContext = null; @@ -82,17 +89,18 @@ public class SpringContextBootstrappingInitializerTest { new SpringContextBootstrappingInitializer().createApplicationContext(null); } catch (IllegalArgumentException expected) { - assertEquals("The configLocations must be specified to construct an instance of the ClassPathXmlApplicationContext.", + assertEquals("'configLocations' must be specified to construct an instance of the ClassPathXmlApplicationContext.", expected.getMessage()); throw expected; } } @Test(expected = IllegalArgumentException.class) - public void testInitWithUnspecifiedContextConfigLocationsParameter() { + public void testInitWithUnspecifiedBasePackagesAndContextConfigLocationsParameter() { try { - new SpringContextBootstrappingInitializer().init(createParameters( - SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, "")); + new SpringContextBootstrappingInitializer().init(createParameters(createParameters( + SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, ""), + SpringContextBootstrappingInitializer.BASE_PACKAGES_PARAMETER, "")); } catch (IllegalArgumentException expected) { assertEquals("Either 'basePackages' or the 'contextConfigLocations' parameter must be specified.", @@ -122,6 +130,26 @@ public class SpringContextBootstrappingInitializerTest { } } + @Test(expected = IllegalArgumentException.class) + public void testInitWhenCreateApplicationContextReturnsNull() { + try { + SpringContextBootstrappingInitializer initializer = new SpringContextBootstrappingInitializer() { + @Override + protected ConfigurableApplicationContext createApplicationContext(final String[] basePackages, + final String[] configLocations) { + return null; + } + }; + + initializer.init(createParameters(SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, + "/path/to/spring/context/configuration/file.xml")); + } + catch (IllegalArgumentException expected) { + assertEquals("The 'created' ConfigurableApplicationContext cannot be null!", expected.getMessage()); + throw expected; + } + } + @Test(expected = IllegalStateException.class) public void testInitWithNonActiveApplicationContext() { final ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class, @@ -145,7 +173,7 @@ public class SpringContextBootstrappingInitializerTest { "/path/to/spring/context/configuration/file.xml")); } catch (IllegalStateException expected) { - assertEquals("The Spring application context (testInitWithNonActiveApplicationContext) has failed to be properly initialized with the following config files ([/path/to/spring/context/configuration/file.xml])!", + assertEquals("The Spring application context (testInitWithNonActiveApplicationContext) has failed to be properly initialized with the following config files ([/path/to/spring/context/configuration/file.xml]) or base packages ([])!", expected.getMessage()); throw expected; } @@ -199,13 +227,13 @@ public class SpringContextBootstrappingInitializerTest { } @Test - public void testNullSafeGetApplicationContextWithNonNullReference() { + public void testNullSafeGetApplicationContextIdWithNonNullReference() { ApplicationContext mockApplicationContext = mock(ApplicationContext.class, - "testNullSafeGetApplicationContextWithNonNullReference"); + "testNullSafeGetApplicationContextIdWithNonNullReference"); - when(mockApplicationContext.getId()).thenReturn("testNullSafeGetApplicationContextWithNonNullReference"); + when(mockApplicationContext.getId()).thenReturn("testNullSafeGetApplicationContextIdWithNonNullReference"); - assertEquals("testNullSafeGetApplicationContextWithNonNullReference", + assertEquals("testNullSafeGetApplicationContextIdWithNonNullReference", new SpringContextBootstrappingInitializer().nullSafeGetApplicationContextId(mockApplicationContext)); } @@ -235,7 +263,7 @@ public class SpringContextBootstrappingInitializerTest { try { ContextRefreshedEvent testEvent = new ContextRefreshedEvent(mock(ApplicationContext.class, - "testUnregisterAndOnApplicationEvent")); + "testRegisterUnregisterAndOnApplicationEvent")); new SpringContextBootstrappingInitializer().onApplicationEvent(testEvent); @@ -246,6 +274,37 @@ public class SpringContextBootstrappingInitializerTest { } } + @Test + public void testNotifyListenersOnContextRefreshedEventBeforeApplicationContextExists() { + TestApplicationListener testApplicationListener = new TestApplicationListener(); + + SpringContextBootstrappingInitializer.applicationContext = null; + SpringContextBootstrappingInitializer.register(testApplicationListener); + + testApplicationListener.assertNotCalled(); + } + + @Test + public void testNotifyListenersOnContextRefreshedEventAfterApplicationContextRefreshed() { + ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class, + "testNotifyListenersOnContextRefreshedEventAfterApplicationContextRefreshed"); + + ContextRefreshedEvent testEvent = new ContextRefreshedEvent(mockApplicationContext); + + SpringContextBootstrappingInitializer.applicationContext = mockApplicationContext; + new SpringContextBootstrappingInitializer().onApplicationEvent(testEvent); + + TestApplicationListener testApplicationListener = new TestApplicationListener(); + + SpringContextBootstrappingInitializer.register(testApplicationListener); + + testApplicationListener.assertCalled(); + testApplicationListener.assertSame(testEvent); + } + + // TODO add additional multi-thread test cases once MultithreadedTC test framework is added to the SDP project + // to properly test concurrency of the notification and registration during Spring ApplicationContext creation. + protected static class TestApplicationListener implements ApplicationListener { private volatile boolean called = false;