/* * Copyright 2010-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.data.gemfire.support; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Properties; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.geode.cache.Cache; import org.apache.geode.cache.Declarable; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.event.ApplicationContextEvent; import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * The {@link SpringContextBootstrappingInitializer} class is a GemFire configuration initializer used to bootstrap * a Spring {@link ApplicationContext} inside a GemFire Server JVM-based process. This enables a GemFire Server * resource to be mostly configured with Spring Data GemFire's configuration meta-data. The GemFire {@link Cache} * itself is the only resource that cannot be configured and initialized in a Spring context since the initializer * is not invoked until after GemFire creates and initializes the GemFire {@link Cache} for use. * * @author John Blum * @see org.springframework.context.ApplicationContext * @see org.springframework.context.ApplicationListener * @see org.springframework.context.ConfigurableApplicationContext * @see org.springframework.context.annotation.AnnotationConfigApplicationContext * @see org.springframework.context.event.ApplicationContextEvent * @see org.springframework.context.event.ApplicationEventMulticaster * @see org.springframework.context.support.ClassPathXmlApplicationContext * @see org.springframework.core.io.DefaultResourceLoader * @see org.apache.geode.cache.Cache * @see org.apache.geode.cache.Declarable * @link http://gemfire.docs.pivotal.io/latest/userguide/index.html#basic_config/the_cache/setting_cache_initializer.html * @link https://jira.springsource.org/browse/SGF-248 * @since 1.4.0 */ @SuppressWarnings("unused") public class SpringContextBootstrappingInitializer implements Declarable, ApplicationListener<ApplicationContextEvent> { public static final String BASE_PACKAGES_PARAMETER = "basePackages"; public static final String CONTEXT_CONFIG_LOCATIONS_PARAMETER = "contextConfigLocations"; protected static final String CHARS_TO_DELETE = " \n\t"; protected static final String COMMA_DELIMITER = ","; private static final ApplicationEventMulticaster applicationEventNotifier = new SimpleApplicationEventMulticaster(); private static final AtomicReference<ClassLoader> beanClassLoaderReference = new AtomicReference<>(null); static volatile ConfigurableApplicationContext applicationContext; static volatile ContextRefreshedEvent contextRefreshedEvent; private static final List<Class<?>> registeredAnnotatedClasses = new CopyOnWriteArrayList<>(); protected final Log logger = initLogger(); /** * Gets a reference to the Spring ApplicationContext constructed, configured and initialized inside the GemFire * Server-based JVM process. * * @return a reference to the Spring ApplicationContext bootstrapped by GemFire. * @see org.springframework.context.ConfigurableApplicationContext */ public static synchronized ConfigurableApplicationContext getApplicationContext() { Assert.state(applicationContext != null, "A Spring ApplicationContext was not configured and initialized properly"); return applicationContext; } /** * Sets the ClassLoader used by the Spring ApplicationContext, created by this GemFire Initializer, when creating * bean definition classes. * * @param beanClassLoader the ClassLoader used by the Spring ApplicationContext to load bean definition classes. * @throws java.lang.IllegalStateException if the Spring ApplicationContext has already been created * and initialized. * @see java.lang.ClassLoader */ public static void setBeanClassLoader(ClassLoader beanClassLoader) { if (applicationContext == null || !applicationContext.isActive()) { beanClassLoaderReference.set(beanClassLoader); } else { throw new IllegalStateException("A Spring ApplicationContext has already been initialized"); } } /** * Notifies any Spring ApplicationListeners of a current and existing ContextRefreshedEvent if the * ApplicationContext had been previously created, initialized and refreshed before any ApplicationListeners * interested in ContextRefreshedEvents were registered so that application components (such as the * GemFire CacheLoaders extending LazyWiringDeclarableSupport objects) registered late, requiring configuration * (auto-wiring), also get notified and wired accordingly. * * @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 notifyOnExistingContextRefreshedEvent(ApplicationListener<ContextRefreshedEvent> listener) { synchronized (applicationEventNotifier) { if (contextRefreshedEvent != null) { listener.onApplicationEvent(contextRefreshedEvent); } } } /** * Registers a Spring ApplicationListener to be notified when the Spring ApplicationContext is created by GemFire * when instantiating and initializing Declarables declared inside the <initializer> block inside GemFire's * cache.xml file. * * @param <T> the Class type of the Spring ApplicationListener. * @param listener the ApplicationListener to register for ContextRefreshedEvents multi-casted by this * SpringContextBootstrappingInitializer. * @return the reference to the ApplicationListener for method call chaining purposes. * @see #notifyOnExistingContextRefreshedEvent(org.springframework.context.ApplicationListener) * @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 extends ApplicationListener<ContextRefreshedEvent>> T register(T listener) { synchronized (applicationEventNotifier) { applicationEventNotifier.addApplicationListener(listener); notifyOnExistingContextRefreshedEvent(listener); } return listener; } /** * Registers the specified Spring annotated POJO class, which will be used to configure and initialize * the Spring ApplicationContext. * * @param annotatedClass the Spring annotated (@Configuration) POJO class to register. * @return a boolean value indicating whether the Spring annotated POJO class was successfully registered. * @see #unregister(Class) */ public static boolean register(Class<?> annotatedClass) { Assert.notNull(annotatedClass, "The Spring annotated class to register must not be null"); return registeredAnnotatedClasses.add(annotatedClass); } /** * Un-registers the Spring ApplicationListener from this SpringContextBootstrappingInitializer in order to stop * receiving ApplicationEvents on Spring context refreshes. * * @param <T> the Class type of the Spring ApplicationListener. * @param listener the ApplicationListener to unregister from receiving ContextRefreshedEvents by this * SpringContextBootstrappingInitializer. * @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 extends ApplicationListener<ContextRefreshedEvent>> T unregister(T listener) { synchronized (applicationEventNotifier) { applicationEventNotifier.removeApplicationListener(listener); } return listener; } /** * Un-registers the specified Spring annotated POJO class used to configure and initialize * the Spring ApplicationContext. * * @param annotatedClass the Spring annotated (@Configuration) POJO class to unregister. * @return a boolean value indicating whether the Spring annotated POJO class was successfully un-registered. * @see #register(Class) */ public static boolean unregister(Class<?> annotatedClass) { return registeredAnnotatedClasses.remove(annotatedClass); } /** * Initialization method for the logger used to log important messages from this initializer. * * @return a Apache Commons Log used to log messages from this initializer * @see org.apache.commons.logging.LogFactory#getLog(Class) * @see org.apache.commons.logging.Log */ protected Log initLogger() { return LogFactory.getLog(getClass()); } /* (non-Javadoc) */ private boolean isConfigurable(Collection<Class<?>> annotatedClasses, String[] basePackages, String[] contextConfigLocations) { return !(CollectionUtils.isEmpty(annotatedClasses) && ObjectUtils.isEmpty(basePackages) && ObjectUtils.isEmpty(contextConfigLocations)); } /** * Creates (constructs and configures) an instance of the ConfigurableApplicationContext based on either the * specified base packages containing @Configuration, @Component or JSR 330 annotated classes to scan, or the * specified locations of context configuration meta-data files. The created ConfigurableApplicationContext * is not automatically "refreshed" and therefore must be "refreshed" by the caller manually. * * When basePackages are specified, an instance of AnnotationConfigApplicationContext is constructed and a scan * is performed; otherwise an instance of the ClassPathXmlApplicationContext is initialized with the * configLocations. This method prefers the ClassPathXmlApplicationContext to the * AnnotationConfigApplicationContext when both basePackages and configLocations are specified. * * @param basePackages the base packages to scan for application @Components and @Configuration classes. * @param configLocations a String array indicating the locations of the context configuration meta-data files * used to configure the ClassPathXmlApplicationContext instance. * @return an instance of ConfigurableApplicationContext configured and initialized with either configLocations * or the basePackages when configLocations is unspecified. Note, the "refresh" method must be called manually * before using the context. * @throws IllegalArgumentException if both the basePackages and configLocation parameter arguments * are null or empty. * @see #createApplicationContext(String[]) * @see org.springframework.context.annotation.AnnotationConfigApplicationContext * @see org.springframework.context.annotation.AnnotationConfigApplicationContext#scan(String...) * @see org.springframework.context.support.ClassPathXmlApplicationContext */ protected ConfigurableApplicationContext createApplicationContext(String[] basePackages, String[] configLocations) { Assert.isTrue(isConfigurable(registeredAnnotatedClasses, basePackages, configLocations), "'AnnotatedClasses', 'basePackages' or 'configLocations' must be specified in order to" + " construct and configure an instance of the ConfigurableApplicationContext"); Class<?>[] annotatedClasses = registeredAnnotatedClasses.toArray( new Class<?>[registeredAnnotatedClasses.size()]); return scanBasePackages(registerAnnotatedClasses(createApplicationContext(configLocations), annotatedClasses), basePackages); } /* (non-Javadoc) - used for testing purposes only */ ConfigurableApplicationContext createApplicationContext(String[] configLocations) { return (ObjectUtils.isEmpty(configLocations) ? new AnnotationConfigApplicationContext() : new ClassPathXmlApplicationContext(configLocations, false)); } /** * Initializes the given ApplicationContext by registering this SpringContextBootstrappingInitializer as an * ApplicationListener and registering a runtime shutdown hook. * * @param applicationContext the ConfigurableApplicationContext to initialize. * @return the initialized ApplicationContext. * @see org.springframework.context.ConfigurableApplicationContext * @see org.springframework.context.ConfigurableApplicationContext#addApplicationListener(org.springframework.context.ApplicationListener) * @see org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() * @throws java.lang.IllegalArgumentException if the ApplicationContext reference is null! */ protected ConfigurableApplicationContext initApplicationContext(ConfigurableApplicationContext applicationContext) { Assert.notNull(applicationContext, "ConfigurableApplicationContext must not be null"); applicationContext.addApplicationListener(this); applicationContext.registerShutdownHook(); return setClassLoader(applicationContext); } /** * Refreshes the given ApplicationContext making the context active. * * @param applicationContext the ConfigurableApplicationContext to refresh. * @return the refreshed ApplicationContext. * @see org.springframework.context.ConfigurableApplicationContext * @see org.springframework.context.ConfigurableApplicationContext#refresh() * @throws java.lang.IllegalArgumentException if the ApplicationContext reference is null! */ protected ConfigurableApplicationContext refreshApplicationContext(ConfigurableApplicationContext applicationContext) { Assert.notNull(applicationContext, "ConfigurableApplicationContext must not be null"); applicationContext.refresh(); return applicationContext; } /** * Registers the given Spring annotated (@Configuration) POJO classes with the specified * AnnotationConfigApplicationContext. * * @param applicationContext the AnnotationConfigApplicationContext used to register the Spring annotated, * POJO classes. * @param annotatedClasses a Class array of Spring annotated (@Configuration) classes used to configure * and initialize the Spring AnnotationConfigApplicationContext. * @return the given AnnotationConfigApplicationContext. * @see org.springframework.context.annotation.AnnotationConfigApplicationContext#register(Class[]) */ ConfigurableApplicationContext registerAnnotatedClasses(ConfigurableApplicationContext applicationContext, Class<?>[] annotatedClasses) { if (applicationContext instanceof AnnotationConfigApplicationContext && !ObjectUtils.isEmpty(annotatedClasses)) { ((AnnotationConfigApplicationContext) applicationContext).register(annotatedClasses); } return applicationContext; } /** * Configures classpath component scanning using the specified base packages on the specified * AnnotationConfigApplicationContext. * * @param applicationContext the AnnotationConfigApplicationContext to setup with classpath component scanning * using the specified base packages. * @param basePackages an array of Strings indicating the base packages to use in the classpath component scan. * @return the given AnnotationConfigApplicationContext. * @see org.springframework.context.annotation.AnnotationConfigApplicationContext#scan(String...) */ ConfigurableApplicationContext scanBasePackages(ConfigurableApplicationContext applicationContext, String[] basePackages) { if (applicationContext instanceof AnnotationConfigApplicationContext && !ObjectUtils.isEmpty(basePackages)) { ((AnnotationConfigApplicationContext) applicationContext).scan(basePackages); } return applicationContext; } /** * Sets the ClassLoader used to load bean definition classes on the Spring ApplicationContext. * * @param applicationContext the Spring ApplicationContext in which to configure the ClassLoader. * @return the given Spring ApplicationContext. * @see org.springframework.core.io.DefaultResourceLoader#setClassLoader(ClassLoader) * @see java.lang.ClassLoader */ ConfigurableApplicationContext setClassLoader(ConfigurableApplicationContext applicationContext) { ClassLoader beanClassLoader = beanClassLoaderReference.get(); if (applicationContext instanceof DefaultResourceLoader && beanClassLoader != null) { ((DefaultResourceLoader) applicationContext).setClassLoader(beanClassLoader); } return applicationContext; } /** * Initializes a Spring ApplicationContext with the given parameters specified with a GemFire <initializer> * block in cache.xml. * * @param parameters a Properties object containing the configuration parameters and settings defined in the * GemFire cache.xml <initializer> block for the declared SpringContextBootstrappingInitializer * GemFire Declarable object. * @throws org.springframework.context.ApplicationContextException if the Spring ApplicationContext could not be * successfully created, configured and initialized. * @see #createApplicationContext(String[], String[]) * @see #initApplicationContext(org.springframework.context.ConfigurableApplicationContext) * @see #refreshApplicationContext(org.springframework.context.ConfigurableApplicationContext) * @see java.util.Properties */ @Override public void init(Properties parameters) { try { synchronized (SpringContextBootstrappingInitializer.class) { if (applicationContext == null || !applicationContext.isActive()) { String basePackages = parameters.getProperty(BASE_PACKAGES_PARAMETER); String contextConfigLocations = parameters.getProperty(CONTEXT_CONFIG_LOCATIONS_PARAMETER); String[] basePackagesArray = StringUtils.delimitedListToStringArray( StringUtils.trimWhitespace(basePackages), COMMA_DELIMITER, CHARS_TO_DELETE); String[] contextConfigLocationsArray = StringUtils.delimitedListToStringArray( StringUtils.trimWhitespace(contextConfigLocations), COMMA_DELIMITER, CHARS_TO_DELETE); ConfigurableApplicationContext localApplicationContext = refreshApplicationContext( initApplicationContext(createApplicationContext(basePackagesArray, contextConfigLocationsArray))); Assert.state(localApplicationContext.isRunning(), String.format( "The Spring ApplicationContext (%1$s) failed to be properly initialized with the context config files (%2$s) or base packages (%3$s)!", nullSafeGetApplicationContextId(localApplicationContext), Arrays.toString(contextConfigLocationsArray), Arrays.toString(basePackagesArray))); applicationContext = localApplicationContext; } } } catch (Throwable cause) { String message = "Failed to bootstrap the Spring ApplicationContext"; logger.error(message, cause); throw new ApplicationContextException(message, cause); } } /** * Null-safe operation used to get the ID of the Spring ApplicationContext. * * @param applicationContext the Spring ApplicationContext from which to get the ID. * @return the ID of the given Spring ApplicationContext or null if the ApplicationContext reference is null. * @see org.springframework.context.ApplicationContext#getId() */ String nullSafeGetApplicationContextId(ApplicationContext applicationContext) { return (applicationContext != null ? applicationContext.getId() : null); } /** * Gets notified when the Spring ApplicationContext gets created and refreshed by GemFire, once the * <initializer> block is processed and the SpringContextBootstrappingInitializer Declarable component * is initialized. This handler method proceeds in notifying any other GemFire components that need to be aware * that the Spring ApplicationContext now exists and is ready for use, such as other Declarable GemFire objects * requiring auto-wiring support, etc. * * In addition, this method handles the ContextClosedEvent by removing the ApplicationContext reference. * * @param event the ApplicationContextEvent signaling that the Spring ApplicationContext has been created * and refreshed by GemFire, or closed when the JVM process exits. * @see org.springframework.context.event.ContextClosedEvent * @see org.springframework.context.event.ContextRefreshedEvent * @see org.springframework.context.event.ApplicationEventMulticaster * #multicastEvent(org.springframework.context.ApplicationEvent) */ @Override public void onApplicationEvent(ApplicationContextEvent event) { if (event instanceof ContextRefreshedEvent) { synchronized (applicationEventNotifier) { contextRefreshedEvent = (ContextRefreshedEvent) event; applicationEventNotifier.multicastEvent(event); } } else if (event instanceof ContextClosedEvent) { synchronized (applicationEventNotifier) { contextRefreshedEvent = null; } } } }