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;