diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersGrailsPlugin.groovy b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersGrailsPlugin.groovy index f4e3ba512b0..42fbd0b7a2a 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersGrailsPlugin.groovy +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersGrailsPlugin.groovy @@ -22,8 +22,6 @@ import groovy.util.logging.Slf4j import org.springframework.beans.factory.support.AbstractBeanDefinition import org.springframework.context.ApplicationContext -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping import grails.config.Settings import grails.core.GrailsControllerClass @@ -72,14 +70,6 @@ class ControllersGrailsPlugin extends Plugin { "${CompositeViewResolver.BEAN_NAME}"(CompositeViewResolver) - def handlerInterceptors = springConfig.containsBean('localeChangeInterceptor') ? [ref('localeChangeInterceptor')] : [] - def interceptorsClosure = { - interceptors = handlerInterceptors - } - // allow @Controller annotated beans - annotationHandlerMapping(RequestMappingHandlerMapping, interceptorsClosure) - annotationHandlerAdapter(RequestMappingHandlerAdapter) - for (controller in application.getArtefacts(ControllerArtefactHandler.TYPE)) { log.debug('Configuring controller {}', controller.fullName) if (controller.available) { diff --git a/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy b/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy index 2b7c644247f..0adc913a078 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy @@ -34,6 +34,7 @@ import org.springframework.context.ConfigurableApplicationContext import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.io.ResourceLoader +import grails.boot.config.GrailsEarlyPluginRegistrationPostProcessor import grails.compiler.ast.ClassInjector import grails.config.Settings import grails.core.GrailsApplication @@ -142,6 +143,40 @@ class GrailsApp extends SpringApplication { } } + /** + * Stashes the application source classes as a well-known singleton so that + * {@code GrailsEarlyPluginRegistrationPostProcessor} can perform artefact discovery before + * Spring Boot auto-configuration is processed. Runs before the context initializers are + * applied, so the singleton is available by the time the early registration phase executes. + */ + @Override + protected void postProcessApplicationContext(ConfigurableApplicationContext applicationContext) { + super.postProcessApplicationContext(applicationContext) + ClassLoader loader = getClassLoader() ?: Thread.currentThread().contextClassLoader + List> sourceClasses = [] + for (Object source in getAllSources()) { + if (source instanceof Class) { + sourceClasses.add((Class) source) + } + else if (source instanceof String) { + try { + // sources may be supplied as class names (spring.main.sources); resolve them so a + // String-specified application class is still available for early artefact discovery + sourceClasses.add(Class.forName((String) source, false, loader)) + } + catch (ClassNotFoundException | LinkageError ignored) { + // a String source that is not a resolvable class (e.g. a resource location) is not + // an application class, so there is nothing to stash for it + } + } + } + if (!sourceClasses.isEmpty()) { + applicationContext.beanFactory.registerSingleton( + GrailsEarlyPluginRegistrationPostProcessor.APPLICATION_SOURCE_CLASSES_BEAN_NAME, + sourceClasses.toArray(new Class[0])) + } + } + @Override protected ConfigurableApplicationContext createApplicationContext() { setAllowBeanDefinitionOverriding(configuredEnvironment.getProperty(Settings.SPRING_MAIN_ALLOW_BEAN_DEFINITION_OVERRIDING, Boolean, Boolean.TRUE)) diff --git a/grails-core/src/main/groovy/grails/boot/config/ApplicationArtefactScanner.groovy b/grails-core/src/main/groovy/grails/boot/config/ApplicationArtefactScanner.groovy new file mode 100644 index 00000000000..a69cfef692b --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/ApplicationArtefactScanner.groovy @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.boot.config + +import groovy.transform.CompileStatic + +import grails.boot.config.tools.ClassPathScanner +import org.grails.compiler.injection.AbstractGrailsArtefactTransformer + +/** + * Discovers the artefact classes that make up a Grails application by scanning the classpath relative + * to an application class. This is the single implementation of the default scanning performed by + * {@link GrailsAutoConfiguration#classes()}, also used by + * {@link GrailsEarlyPluginRegistrationPostProcessor} to perform artefact discovery before Spring + * Boot auto-configuration is processed. + * + * @since 8.0 + */ +@CompileStatic +final class ApplicationArtefactScanner { + + private ApplicationArtefactScanner() { + } + + /** + * Scans for application classes in the package of the given application class, including any + * classes registered by Grails artefact transformations. + * + * @param applicationClass The application class to scan relative to + * @return The classes that constitute the Grails application + */ + static Collection scanApplicationClasses(Class applicationClass) { + Package applicationPackage = applicationClass.package + Collection packageNames = applicationPackage != null ? [applicationPackage.name] : new ArrayList() + return scanApplicationClasses(applicationClass, packageNames) + } + + /** + * Scans for application classes in the given packages relative to the given application class, + * including any classes registered by Grails artefact transformations. + * + * @param applicationClass The application class to scan relative to + * @param packageNames The package names to scan + * @return The classes that constitute the Grails application + */ + static Collection scanApplicationClasses(Class applicationClass, Collection packageNames) { + Collection classes = new HashSet<>() + classes.addAll(new ClassPathScanner().scan(applicationClass, packageNames)) + classes.addAll(loadTransformedClasses(applicationClass.classLoader)) + return classes + } + + /** + * Loads the classes registered by Grails artefact transformations at compile time. + * + * @param classLoader The class loader to load the classes with + * @return The transformed classes resolvable by the given class loader + */ + static Collection loadTransformedClasses(ClassLoader classLoader) { + Collection classes = [] + for (String className in AbstractGrailsArtefactTransformer.transformedClassNames) { + try { + classes << classLoader.loadClass(className) + } catch (ClassNotFoundException ignored) { + } + } + return classes + } +} diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy b/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy index 2734ce81fcb..297ae6046fd 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy @@ -24,10 +24,12 @@ import groovy.util.logging.Slf4j import org.springframework.beans.BeansException import org.springframework.beans.factory.BeanFactory +import org.springframework.beans.factory.BeanRegistrar import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.beans.factory.config.ConfigurableListableBeanFactory import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor +import org.springframework.beans.factory.support.BeanRegistryAdapter import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware import org.springframework.context.ApplicationListener @@ -48,6 +50,7 @@ import grails.core.GrailsApplication import grails.core.GrailsApplicationClass import grails.core.GrailsApplicationLifeCycle import grails.plugins.DefaultGrailsPluginManager +import grails.plugins.GrailsPlugin import grails.plugins.GrailsPluginManager import grails.spring.BeanBuilder import grails.util.Environment @@ -78,6 +81,7 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces final GrailsApplicationClass applicationClass final Class[] classes protected final GrailsPluginManager pluginManager + protected final boolean earlyPluginRegistrationRan protected ApplicationContext applicationContext boolean loadExternalBeans = true boolean reloadingEnabled = RELOADING_ENABLED @@ -91,13 +95,36 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces this.applicationClass = null } this.classes = classes != null ? classes : [] as Class[] - grailsApplication = applicationClass != null ? new DefaultGrailsApplication(applicationClass) : new DefaultGrailsApplication() + this.earlyPluginRegistrationRan = hasEarlyPluginRegistrationRun(applicationContext) + if (earlyPluginRegistrationRan) { + grailsApplication = applicationContext.getBean(GrailsApplication.APPLICATION_ID, GrailsApplication) + if (applicationClass != null && grailsApplication instanceof DefaultGrailsApplication) { + ((DefaultGrailsApplication) grailsApplication).setApplicationClass(applicationClass) + } + } + else { + grailsApplication = applicationClass != null ? new DefaultGrailsApplication(applicationClass) : new DefaultGrailsApplication() + } pluginManager = applicationContext?.getBeanNamesForType(GrailsPluginManager) ? applicationContext.getBean(GrailsPluginManager) : new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery) if (applicationContext != null) { setApplicationContext(applicationContext) } } + /** + * Determines whether {@link GrailsEarlyPluginRegistrationPostProcessor} already built the + * {@code grailsApplication} and {@code pluginManager} singletons and drained the plugin + * runtime configuration for this context. Checked on the local bean factory only, so a + * parent context's early phase never short-circuits a child context's lifecycle. + */ + private static boolean hasEarlyPluginRegistrationRun(ApplicationContext applicationContext) { + if (applicationContext instanceof ConfigurableApplicationContext) { + return ((ConfigurableApplicationContext) applicationContext).beanFactory + .containsSingleton(GrailsEarlyPluginRegistrationPostProcessor.EARLY_REGISTRATION_COMPLETE_BEAN_NAME) + } + return false + } + /** * Resolves the {@link PluginDiscovery} from the application context. * The bootstrap registry promotes the discovery bean before context refresh, @@ -122,11 +149,32 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces Environment.setInitializing(true) grailsApplication.applicationContext = applicationContext grailsApplication.mainContext = applicationContext - pluginManager.loadPlugins() - pluginManager.applicationContext = applicationContext + if (!earlyPluginRegistrationRan) { + pluginManager.loadPlugins() + pluginManager.applicationContext = applicationContext + } loadApplicationConfig() customizeGrailsApplication(grailsApplication) - performGrailsInitializationSequence() + if (earlyPluginRegistrationRan) { + registerRemainingApplicationClasses() + } + else { + performGrailsInitializationSequence() + } + } + + /** + * When the early plugin registration phase already performed artefact discovery, only the + * application classes it could not resolve (e.g. a customized {@code classes()} implementation) + * still need to be registered. + */ + private void registerRemainingApplicationClasses() { + Set registeredClassNames = grailsApplication.allArtefacts*.name as Set + for (cls in classes) { + if (!registeredClassNames.contains(cls.name)) { + grailsApplication.addArtefact(cls) + } + } } protected void customizeGrailsApplication(GrailsApplication grailsApplication) { @@ -194,8 +242,11 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces def application = grailsApplication Holders.setGrailsApplication(application) - // first register plugin beans - pluginManager.doRuntimeConfiguration(springConfig) + if (!earlyPluginRegistrationRan) { + // first register plugin beans; when the early phase ran they were + // already drained into the registry ahead of auto-configuration + pluginManager.doRuntimeConfiguration(springConfig) + } if (loadExternalBeans) { // now allow overriding via application @@ -233,6 +284,42 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces } springConfig.registerBeansWithRegistry(registry) + + if (!earlyPluginRegistrationRan) { + // the early phase applies plugin registrars itself; on the fallback path (contexts not + // booted through GrailsApp) apply them here so a plugin's beanRegistrar() behaves the same + applyPluginBeanRegistrars(registry) + } + + if (lifeCycle) { + // the application's BeanRegistrar drains at the same point as its doWithSpring closure, + // after the DSL flush so registrar beans win any name conflicts with the deprecated DSL + BeanRegistrar registrar = lifeCycle.beanRegistrar() + if (registrar != null) { + new BeanRegistryAdapter(registry, applicationContext, applicationContext.environment, registrar.getClass()) + .register(registrar) + } + } + } + + /** + * Applies each enabled plugin's {@link BeanRegistrar} on the fallback path where the early + * plugin registration phase did not run, mirroring that phase so a plugin's {@code beanRegistrar()} + * is honoured in every context rather than only those booted through {@code GrailsApp}. Runs after + * the DSL flush so registrar beans win name conflicts with the deprecated {@code doWithSpring} DSL. + */ + private void applyPluginBeanRegistrars(BeanDefinitionRegistry registry) { + String[] activeProfiles = applicationContext.environment.activeProfiles + for (GrailsPlugin plugin in pluginManager.allPlugins) { + if (!plugin.supportsCurrentScopeAndEnvironment() || !plugin.isEnabled(activeProfiles)) { + continue + } + BeanRegistrar registrar = plugin.beanRegistrar + if (registrar != null) { + new BeanRegistryAdapter(registry, applicationContext, applicationContext.environment, registrar.getClass()) + .register(registrar) + } + } } @Override @@ -240,11 +327,21 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory() if (parentBeanFactory instanceof ConfigurableBeanFactory) { ConfigurableBeanFactory configurableBeanFactory = parentBeanFactory - configurableBeanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, grailsApplication) - configurableBeanFactory.registerSingleton(GrailsPluginManager.BEAN_NAME, pluginManager) + registerSingletonIfAbsent(configurableBeanFactory, GrailsApplication.APPLICATION_ID, grailsApplication) + registerSingletonIfAbsent(configurableBeanFactory, GrailsPluginManager.BEAN_NAME, pluginManager) } else { - beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, grailsApplication) - beanFactory.registerSingleton(GrailsPluginManager.BEAN_NAME, pluginManager) + registerSingletonIfAbsent(beanFactory, GrailsApplication.APPLICATION_ID, grailsApplication) + registerSingletonIfAbsent(beanFactory, GrailsPluginManager.BEAN_NAME, pluginManager) + } + } + + /** + * The early plugin registration phase may have already promoted these singletons; + * {@code registerSingleton} throws {@code IllegalStateException} on double registration. + */ + private static void registerSingletonIfAbsent(ConfigurableBeanFactory beanFactory, String beanName, Object singleton) { + if (!beanFactory.containsSingleton(beanName)) { + beanFactory.registerSingleton(beanName, singleton) } } diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsAutoConfiguration.groovy b/grails-core/src/main/groovy/grails/boot/config/GrailsAutoConfiguration.groovy index 7025bbcec17..91190446d36 100644 --- a/grails-core/src/main/groovy/grails/boot/config/GrailsAutoConfiguration.groovy +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsAutoConfiguration.groovy @@ -33,7 +33,6 @@ import grails.config.Config import grails.core.GrailsApplication import grails.core.GrailsApplicationClass import org.apache.grails.core.plugins.PluginDiscovery -import org.grails.compiler.injection.AbstractGrailsArtefactTransformer import org.grails.spring.aop.autoproxy.GroovyAwareAspectJAwareAdvisorAutoProxyCreator import org.grails.spring.aop.autoproxy.GroovyAwareInfrastructureAdvisorAutoProxyCreator @@ -80,25 +79,13 @@ class GrailsAutoConfiguration implements GrailsApplicationClass, ApplicationCont * @return The classes that constitute the Grails application */ Collection classes() { - Collection classes = new HashSet() - - ClassPathScanner scanner = new ClassPathScanner() if (limitScanningToApplication()) { - classes.addAll(scanner.scan(getClass(), packageNames())) - } - else { - classes.addAll(scanner.scan(new PathMatchingResourcePatternResolver(applicationContext), packageNames())) - } - - ClassLoader classLoader = getClass().getClassLoader() - for (cls in AbstractGrailsArtefactTransformer.transformedClassNames) { - try { - classes << classLoader.loadClass(cls) - } catch (ClassNotFoundException cnfe) { - // ignore - } + return ApplicationArtefactScanner.scanApplicationClasses(getClass(), packageNames()) } + Collection classes = new HashSet() + classes.addAll(new ClassPathScanner().scan(new PathMatchingResourcePatternResolver(applicationContext), packageNames())) + classes.addAll(ApplicationArtefactScanner.loadTransformedClasses(getClass().classLoader)) return classes } diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsEarlyPluginRegistrationPostProcessor.java b/grails-core/src/main/groovy/grails/boot/config/GrailsEarlyPluginRegistrationPostProcessor.java new file mode 100644 index 00000000000..e4c8ac44153 --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsEarlyPluginRegistrationPostProcessor.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.boot.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.BeanRegistryAdapter; +import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.Resource; +import org.springframework.util.ClassUtils; + +import grails.core.DefaultGrailsApplication; +import grails.core.GrailsApplication; +import grails.core.GrailsApplicationClass; +import grails.plugins.DefaultGrailsPluginManager; +import grails.plugins.GrailsPlugin; +import grails.plugins.GrailsPluginManager; +import grails.util.Environment; +import grails.util.Holders; +import org.apache.grails.core.plugins.PluginDiscovery; +import org.grails.config.NavigableMap; +import org.grails.config.PropertySourcesConfig; +import org.grails.spring.DefaultRuntimeSpringConfiguration; +import org.grails.spring.RuntimeSpringConfiguration; + +/** + * Runs the plugin bean-registration phase of the Grails lifecycle before Spring Boot's + * auto-configuration is processed, so that beans contributed by plugins via {@code doWithSpring} + * are already present in the registry when Boot evaluates its {@code @ConditionalOnMissingBean} + * guards — auto-configured defaults then back off in favour of the plugin beans, without any + * override or removal afterwards. + * + *

It is added to the context programmatically (see {@link GrailsPluginLifecycleInitializer}), so its + * {@code postProcessBeanDefinitionRegistry} runs ahead of Boot's {@code ConfigurationClassPostProcessor} + * (which expands the {@code @AutoConfiguration} imports). Manually-added + * {@code BeanDefinitionRegistryPostProcessor}s always run before registry-discovered ones; Spring does + * not sort manually-added post-processors by {@code getOrder()}, so this class deliberately does not + * implement {@code PriorityOrdered}. + * + *

This phase builds the one true {@link GrailsApplication} and {@link GrailsPluginManager}: plugins + * are discovered via the promoted {@link PluginDiscovery} singleton and instantiated exactly once. + * Artefact discovery also happens here, mirroring + * {@code GrailsApplicationPostProcessor.performGrailsInitializationSequence()}, because core plugins + * (controllers, services, interceptors) iterate {@code grailsApplication} artefacts inside their + * {@code doWithSpring} closures. Application classes are resolved from the source classes stashed by + * {@link grails.boot.GrailsApp} (see {@link #APPLICATION_SOURCE_CLASSES_BEAN_NAME}) and scanned with the + * same logic {@link GrailsAutoConfiguration#classes()} uses; when the application was not started + * through {@code GrailsApp} the phase proceeds without application classes. + * + *

Once complete, the {@code grailsApplication} and {@code pluginManager} singletons are promoted to + * the bean factory together with the {@link #EARLY_REGISTRATION_COMPLETE_BEAN_NAME} marker, so + * {@link GrailsApplicationPostProcessor} reuses them instead of rebuilding and skips the already-drained + * plugin runtime configuration. + * + * @since 8.0 + */ +public class GrailsEarlyPluginRegistrationPostProcessor + implements BeanDefinitionRegistryPostProcessor, ApplicationListener { + + /** + * Name of the {@code Class[]} singleton under which {@link grails.boot.GrailsApp} stashes the + * application source classes so this phase can perform early artefact discovery. + */ + public static final String APPLICATION_SOURCE_CLASSES_BEAN_NAME = "grailsApplicationSourceClasses"; + + /** + * Name of the marker singleton registered once this phase has completed, checked by + * {@link GrailsApplicationPostProcessor} to reuse the promoted singletons and skip the + * already-performed lifecycle steps. Always checked on the local bean factory only. + */ + public static final String EARLY_REGISTRATION_COMPLETE_BEAN_NAME = "grailsEarlyPluginRegistrationComplete"; + + private static final Logger LOG = LoggerFactory.getLogger(GrailsEarlyPluginRegistrationPostProcessor.class); + + private final ConfigurableApplicationContext applicationContext; + + public GrailsEarlyPluginRegistrationPostProcessor(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + // Check the LOCAL singleton only — a parent context's discovery must not cause us to re-run + // the early phase in a child context (containsBean/getBean would delegate to the parent). + Object discovery = applicationContext.getBeanFactory().getSingleton(PluginDiscovery.BEAN_NAME); + if (!(discovery instanceof PluginDiscovery pluginDiscovery)) { + // No plugin discovery promoted to this context (e.g. unit-test slice) — nothing to do. + return; + } + + // The initializing flag is a system property, so a leak on failure poisons every subsequent + // context in the same JVM (test forks especially). Reset it if anything below throws; the + // success path leaves it set and resets on refresh via the listener added at the end. + Environment.setInitializing(true); + try { + DefaultGrailsApplication grailsApplication = new DefaultGrailsApplication(); + grailsApplication.setConfig(buildConfig()); + grailsApplication.setApplicationContext(applicationContext); + grailsApplication.setMainContext(applicationContext); + + DefaultGrailsPluginManager pluginManager = new DefaultGrailsPluginManager(grailsApplication, pluginDiscovery); + pluginManager.loadPlugins(); + pluginManager.setApplicationContext(applicationContext); + + pluginManager.doArtefactConfiguration(); + grailsApplication.initialise(); + // register plugin provided classes first, this gives the opportunity + // for application classes to override those provided by a plugin + pluginManager.registerProvidedArtefacts(grailsApplication); + registerApplicationArtefacts(grailsApplication, registry); + // the source-classes stash has been consumed; drop it so it does not linger as an + // autowire-by-type candidate for the life of the context + if (applicationContext.getBeanFactory() instanceof DefaultSingletonBeanRegistry singletonRegistry) { + singletonRegistry.destroySingleton(APPLICATION_SOURCE_CLASSES_BEAN_NAME); + } + + RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration(); + pluginManager.doRuntimeConfiguration(springConfig); + springConfig.registerBeansWithRegistry(registry); + applyBeanRegistrars(pluginManager, registry); + + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, grailsApplication); + beanFactory.registerSingleton(GrailsPluginManager.BEAN_NAME, pluginManager); + beanFactory.registerSingleton(EARLY_REGISTRATION_COMPLETE_BEAN_NAME, Boolean.TRUE); + Holders.setGrailsApplication(grailsApplication); + + // GrailsApplicationPostProcessor resets the initializing flag on refresh, but it is not + // present in every context that runs this phase — reset here as well so the flag does not + // leak once the context is up. + applicationContext.addApplicationListener(this); + } + catch (RuntimeException | Error e) { + Environment.setInitializing(false); + throw e; + } + } + + /** + * Applies the {@link BeanRegistrar} exposed by each enabled plugin through + * {@link grails.core.GrailsApplicationLifeCycle#beanRegistrar()}, in plugin order, using the + * same adapter Spring uses for {@code GenericApplicationContext.register(BeanRegistrar...)}. + * Runs after the {@code doWithSpring} drain so registrar beans win any name conflicts with the + * deprecated DSL. + */ + private void applyBeanRegistrars(DefaultGrailsPluginManager pluginManager, BeanDefinitionRegistry registry) { + String[] activeProfiles = applicationContext.getEnvironment().getActiveProfiles(); + for (GrailsPlugin plugin : pluginManager.getAllPlugins()) { + if (!plugin.supportsCurrentScopeAndEnvironment() || !plugin.isEnabled(activeProfiles)) { + continue; + } + BeanRegistrar registrar = plugin.getBeanRegistrar(); + if (registrar != null) { + new BeanRegistryAdapter(registry, applicationContext.getBeanFactory(), + applicationContext.getEnvironment(), registrar.getClass()).register(registrar); + } + } + } + + private void registerApplicationArtefacts(DefaultGrailsApplication grailsApplication, BeanDefinitionRegistry registry) { + Class[] sources = resolveApplicationSourceClasses(registry); + if (sources.length == 0) { + LOG.debug("No application source classes available — proceeding without early application artefact discovery"); + return; + } + for (Class source : sources) { + if (!GrailsApplicationClass.class.isAssignableFrom(source)) { + // non-application sources (plain configuration classes) never contribute artefacts + continue; + } + for (Object applicationClass : scanApplicationSource(source)) { + grailsApplication.addArtefact((Class) applicationClass); + } + } + } + + /** + * Resolves the classes that constitute the application for the given source class using the + * same code path {@code GrailsApplicationPostProcessor} relies on: {@code classes()} invoked + * on a {@link GrailsAutoConfiguration} instance. This matters because the Grails compiler + * injects a {@code packageNames()} override into the application class listing every project + * package, so scanning only the application class's own package would miss artefacts living + * in other packages. The instance created here is used solely to compute the scan; the + * lifecycle bean the application interacts with is still created by Spring later. + */ + private Collection scanApplicationSource(Class source) { + if (GrailsAutoConfiguration.class.isAssignableFrom(source)) { + try { + GrailsAutoConfiguration application = (GrailsAutoConfiguration) source.getDeclaredConstructor().newInstance(); + application.setApplicationContext(applicationContext); + return application.classes(); + } catch (Exception | LinkageError e) { + LOG.warn("Unable to resolve application classes from [{}], falling back to package scan: {}", + source.getName(), e.toString()); + } + } + return ApplicationArtefactScanner.scanApplicationClasses(source); + } + + /** + * Resolves the application source classes to scan for artefacts, preferring the singleton + * stashed by {@link grails.boot.GrailsApp}. When the stash yields no {@link GrailsApplicationClass} + * — no stash exists (a plain {@code SpringApplication}), or the application class was supplied as + * a {@code String} source alongside other {@code Class} sources — any {@link GrailsApplicationClass} + * is additionally recovered from the registry, where the primary sources are already registered + * as bean definitions by the time this phase runs. + */ + private Class[] resolveApplicationSourceClasses(BeanDefinitionRegistry registry) { + List> sources = new ArrayList<>(); + boolean haveApplicationClass = false; + Object stashedSources = applicationContext.getBeanFactory().getSingleton(APPLICATION_SOURCE_CLASSES_BEAN_NAME); + if (stashedSources instanceof Class[] stashed) { + for (Class source : stashed) { + sources.add(source); + if (GrailsApplicationClass.class.isAssignableFrom(source)) { + haveApplicationClass = true; + } + } + } + if (!haveApplicationClass) { + for (String beanDefinitionName : registry.getBeanDefinitionNames()) { + String beanClassName = registry.getBeanDefinition(beanDefinitionName).getBeanClassName(); + if (beanClassName == null) { + continue; + } + try { + Class beanClass = ClassUtils.forName(beanClassName, applicationContext.getClassLoader()); + if (GrailsApplicationClass.class.isAssignableFrom(beanClass) && !sources.contains(beanClass)) { + sources.add(beanClass); + } + } catch (ClassNotFoundException | LinkageError ignored) { + // not resolvable here — cannot be an application class + } + } + } + return sources.toArray(new Class[0]); + } + + /** + * Builds the {@link PropertySourcesConfig} that backs {@code grailsApplication.config} in this + * phase, registering the same conversion-service converters that + * {@code GrailsApplicationPostProcessor.loadApplicationConfig} registers for the main lifecycle. + * This gives {@code doWithSpring} closures parity when reading config — null-safe navigation of + * missing paths and {@code String -> Resource} coercion — not just scalar + * {@code getProperty(...)} access. + */ + private PropertySourcesConfig buildConfig() { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + ConfigurableConversionService conversionService = null; + if (environment instanceof AbstractEnvironment) { + conversionService = ((AbstractEnvironment) environment).getConversionService(); + conversionService.addConverter(String.class, Resource.class, applicationContext::getResource); + conversionService.addConverter(NavigableMap.NullSafeNavigator.class, String.class, source -> null); + conversionService.addConverter(NavigableMap.NullSafeNavigator.class, Object.class, source -> null); + } + PropertySourcesConfig config = new PropertySourcesConfig(environment.getPropertySources()); + if (conversionService != null) { + config.setConversionService(conversionService); + } + return config; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (event.getApplicationContext() == applicationContext) { + Environment.setInitializing(false); + } + } +} diff --git a/grails-core/src/main/groovy/grails/boot/config/GrailsPluginLifecycleInitializer.java b/grails-core/src/main/groovy/grails/boot/config/GrailsPluginLifecycleInitializer.java new file mode 100644 index 00000000000..1d3ca88234d --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/config/GrailsPluginLifecycleInitializer.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.boot.config; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Registers the {@link GrailsEarlyPluginRegistrationPostProcessor} on the context via + * {@code addBeanFactoryPostProcessor}, which guarantees it runs ahead of Spring Boot's + * registry-discovered post-processors (including {@code ConfigurationClassPostProcessor}, + * which expands auto-configuration). That ordering is what lets plugin beans contributed via + * {@code doWithSpring} be present in the registry before Boot's {@code @ConditionalOnMissingBean} + * guards are evaluated, so auto-configured defaults back off in favour of plugin beans. + * + * @since 8.0 + */ +public class GrailsPluginLifecycleInitializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + applicationContext.addBeanFactoryPostProcessor(new GrailsEarlyPluginRegistrationPostProcessor(applicationContext)); + } +} diff --git a/grails-core/src/main/groovy/grails/core/DefaultGrailsApplication.java b/grails-core/src/main/groovy/grails/core/DefaultGrailsApplication.java index 8d5e708de42..2c579b1810e 100644 --- a/grails-core/src/main/groovy/grails/core/DefaultGrailsApplication.java +++ b/grails-core/src/main/groovy/grails/core/DefaultGrailsApplication.java @@ -219,6 +219,23 @@ public GrailsApplicationClass getApplicationClass() { return applicationClass; } + /** + * Sets the application class. May be set once — to adopt the application class when this + * instance was constructed before the {@link GrailsApplicationClass} was known, e.g. during + * early plugin registration ahead of Spring Boot auto-configuration — and cannot afterwards be + * reassigned to a different application class. + * + * @param applicationClass The application class + * @throws IllegalStateException if a different application class has already been set + * @since 8.0 + */ + public void setApplicationClass(GrailsApplicationClass applicationClass) { + if (this.applicationClass != null && this.applicationClass != applicationClass) { + throw new IllegalStateException("The application class has already been set and cannot be changed"); + } + this.applicationClass = applicationClass; + } + /** * Initialises the default set of ArtefactHandler instances. * diff --git a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy index 7f3ca551ec3..517c2a7cea8 100644 --- a/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy +++ b/grails-core/src/main/groovy/grails/core/GrailsApplicationLifeCycle.groovy @@ -18,10 +18,12 @@ */ package grails.core +import org.springframework.beans.factory.BeanRegistrar + /** * API which plugins implement to provide behavior in defined application lifecycle hooks. * - * The {@link GrailsApplicationLifeCycle#doWithSpring()} method can be used register Spring beans. + * The {@link GrailsApplicationLifeCycle#beanRegistrar()} method can be used to register Spring beans. * * @since 3.0 * @see {@link grails.plugins.Plugin} @@ -32,9 +34,28 @@ interface GrailsApplicationLifeCycle { * Sub classes should override to provide implementations * * @return A closure that defines beans to be registered by Spring + * @deprecated since 8.0 in favour of {@link #beanRegistrar()}. The underlying bean builder DSL + * remains available but is no longer actively supported and will not receive fixes for new + * issues — you are strongly urged to migrate to {@link #beanRegistrar()}. */ + @Deprecated(since = '8.0') Closure doWithSpring() + /** + * Sub classes should override to register beans with the Spring Framework + * {@link org.springframework.beans.factory.BeanRegistry} using a {@link BeanRegistrar}. + * This is the modern, Spring-native replacement for the {@link #doWithSpring()} bean builder DSL. + * + *

The returned registrar is applied before Spring Boot auto-configuration is processed, so + * beans registered here take precedence over Boot's {@code @ConditionalOnMissingBean} defaults.

+ * + * @return A {@link BeanRegistrar} that registers beans, or {@code null} if none (the default) + * @since 8.0 + */ + default BeanRegistrar beanRegistrar() { + return null + } + /** * Invoked once the {@link org.springframework.context.ApplicationContext} has been refreshed in a phase where plugins can add dynamic methods. Subclasses should override */ diff --git a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java index 3dcac050977..877e6835cbb 100644 --- a/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java +++ b/grails-core/src/main/groovy/grails/plugins/GrailsPlugin.java @@ -24,6 +24,7 @@ import groovy.lang.GroovyObject; +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.env.PropertySource; @@ -193,6 +194,20 @@ public interface GrailsPlugin extends ApplicationContextAware, Comparable, Grail */ void doWithRuntimeConfiguration(RuntimeSpringConfiguration springConfig); + /** + * Returns the {@link BeanRegistrar} exposed by this plugin's + * {@link grails.core.GrailsApplicationLifeCycle#beanRegistrar()} hook, if any. The registrar + * is applied to the bean definition registry before Spring Boot auto-configuration is + * processed, so its beans take precedence over Boot's {@code @ConditionalOnMissingBean} + * defaults. + * + * @return The plugin's {@link BeanRegistrar}, or {@code null} if the plugin does not define one + * @since 8.0 + */ + default BeanRegistrar getBeanRegistrar() { + return null; + } + /** * Makes the plugin excluded for a particular Environment * @param env The Environment diff --git a/grails-core/src/main/groovy/grails/plugins/Plugin.groovy b/grails-core/src/main/groovy/grails/plugins/Plugin.groovy index 21335ba6c88..9660602b6ae 100644 --- a/grails-core/src/main/groovy/grails/plugins/Plugin.groovy +++ b/grails-core/src/main/groovy/grails/plugins/Plugin.groovy @@ -21,6 +21,7 @@ package grails.plugins import groovy.transform.CompileStatic import org.springframework.beans.BeansException +import org.springframework.beans.factory.BeanRegistrar import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware @@ -105,10 +106,30 @@ abstract class Plugin implements GrailsApplicationLifeCycle, GrailsApplicationAw * Sub classes should override to provide implementations * * @return A closure that defines beans to be executed by Spring + * @deprecated since 8.0 in favour of {@link #beanRegistrar()}. The underlying bean builder DSL + * remains available but is no longer actively supported and will not receive fixes for new + * issues — you are strongly urged to migrate to {@link #beanRegistrar()}. */ + @Deprecated(since = '8.0') @Override Closure doWithSpring() { null } + /** + * Sub classes should override to register beans with the Spring Framework + * {@link org.springframework.beans.factory.BeanRegistry} using a + * {@link org.springframework.beans.factory.BeanRegistrar}. This is the modern, Spring-native + * replacement for the {@link #doWithSpring()} bean builder DSL. + * + *

The returned registrar is applied before Spring Boot auto-configuration is processed, so + * beans registered here take precedence over Boot's {@code @ConditionalOnMissingBean} defaults.

+ * + * @return A {@link org.springframework.beans.factory.BeanRegistrar} that registers beans, + * or {@code null} if none (the default) + * @since 8.0 + */ + @Override + BeanRegistrar beanRegistrar() { null } + /** * Registers Spring beans directly against the supplied {@link BeanBuilder}. This is the statically-compilable * alternative to {@link #doWithSpring()}: instead of returning a closure whose delegate the container wires up, diff --git a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java index b6839c07823..aee00d2f9ca 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java +++ b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java @@ -43,6 +43,7 @@ import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; @@ -56,6 +57,7 @@ import grails.core.ArtefactHandler; import grails.core.GrailsApplication; +import grails.core.GrailsApplicationLifeCycle; import grails.core.support.GrailsApplicationAware; import grails.core.support.ParentApplicationContextAware; import grails.plugins.GrailsPlugin; @@ -445,6 +447,14 @@ private boolean isDoWithSpringMethodOverridden(Plugin pluginObject) { } } + @Override + public BeanRegistrar getBeanRegistrar() { + if (plugin instanceof GrailsApplicationLifeCycle) { + return ((GrailsApplicationLifeCycle) plugin).beanRegistrar(); + } + return null; + } + @Override public String getName() { return pluginGrailsClass.getLogicalPropertyName(); diff --git a/grails-core/src/main/resources/META-INF/spring.factories b/grails-core/src/main/resources/META-INF/spring.factories index 824ecc7a0ea..25ae99728e2 100644 --- a/grails-core/src/main/resources/META-INF/spring.factories +++ b/grails-core/src/main/resources/META-INF/spring.factories @@ -22,4 +22,5 @@ org.springframework.boot.env.PropertySourceLoader=\ org.grails.config.yaml.YamlPropertySourceLoader org.springframework.boot.EnvironmentPostProcessor=grails.boot.config.GrailsEnvironmentPostProcessor org.springframework.boot.SpringApplicationRunListener=grails.config.external.ExternalConfigRunListener -org.springframework.boot.bootstrap.BootstrapRegistryInitializer=org.apache.grails.core.GrailsBootstrapRegistryInitializer \ No newline at end of file +org.springframework.boot.bootstrap.BootstrapRegistryInitializer=org.apache.grails.core.GrailsBootstrapRegistryInitializer +org.springframework.context.ApplicationContextInitializer=grails.boot.config.GrailsPluginLifecycleInitializer diff --git a/grails-core/src/test/groovy/grails/boot/DevelopmentModeWatchSpec.groovy b/grails-core/src/test/groovy/grails/boot/DevelopmentModeWatchSpec.groovy index 9f9e35d6605..26f9589dc0f 100644 --- a/grails-core/src/test/groovy/grails/boot/DevelopmentModeWatchSpec.groovy +++ b/grails-core/src/test/groovy/grails/boot/DevelopmentModeWatchSpec.groovy @@ -18,14 +18,14 @@ */ package grails.boot -import grails.plugins.GrailsPlugin import grails.plugins.GrailsPluginManager import grails.plugins.Plugin import grails.util.Environment -import org.grails.plugins.DefaultGrailsPlugin -import org.grails.plugins.MockGrailsPluginManager +import org.apache.grails.core.plugins.DefaultPluginDiscovery +import org.apache.grails.core.plugins.PluginDiscovery +import org.springframework.boot.bootstrap.BootstrapRegistry +import org.springframework.boot.bootstrap.BootstrapRegistryInitializer import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import spock.lang.Specification import spock.util.concurrent.PollingConditions @@ -42,8 +42,17 @@ class DevelopmentModeWatchSpec extends Specification { System.setProperty(Environment.KEY, Environment.DEVELOPMENT.getName()) System.setProperty("base.dir", ".") GrailsApp app = new GrailsApp(GrailsTestConfigurationClass.class) + DefaultPluginDiscovery discovery = new DefaultPluginDiscovery([WatchedResourcesGrailsPlugin] as Class[]) + discovery.loadPluginsFromClasspath = false + app.addBootstrapRegistryInitializer(new BootstrapRegistryInitializer() { + @Override + void initialize(BootstrapRegistry registry) { + registry.register(PluginDiscovery, BootstrapRegistry.InstanceSupplier.of(discovery)) + } + }) ConfigurableApplicationContext context = app.run() - WatchedResourcesGrailsPlugin plugin = context.getBean('grailsPluginManager').plugins.values().first().instance + GrailsPluginManager pluginManager = context.getBean(GrailsPluginManager.BEAN_NAME, GrailsPluginManager) + WatchedResourcesGrailsPlugin plugin = (WatchedResourcesGrailsPlugin) pluginManager.getGrailsPlugin('watchedResources').instance when: File watchedFile = new File('testWatchedFile.properties') @@ -56,6 +65,8 @@ class DevelopmentModeWatchSpec extends Specification { } cleanup: + GrailsApp.setDevelopmentModeActive(false) + context?.close() if(watchedFile != null) { watchedFile.delete() } @@ -64,14 +75,6 @@ class DevelopmentModeWatchSpec extends Specification { @Configuration class GrailsTestConfigurationClass { - - @Bean(name = "grailsPluginManager") - GrailsPluginManager getGrailsPluginManager() { - MockGrailsPluginManager mockGrailsPluginManager = new MockGrailsPluginManager() - GrailsPlugin watchedPlugin = new DefaultGrailsPlugin(WatchedResourcesGrailsPlugin.class, mockGrailsPluginManager.application) - mockGrailsPluginManager.registerMockPlugin(watchedPlugin) - return mockGrailsPluginManager - } } class WatchedResourcesGrailsPlugin extends Plugin { diff --git a/grails-core/src/test/groovy/grails/boot/config/EarlyPluginRegistrationOrderingSpec.groovy b/grails-core/src/test/groovy/grails/boot/config/EarlyPluginRegistrationOrderingSpec.groovy new file mode 100644 index 00000000000..4a65e7f80c8 --- /dev/null +++ b/grails-core/src/test/groovy/grails/boot/config/EarlyPluginRegistrationOrderingSpec.groovy @@ -0,0 +1,396 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.boot.config + +import java.util.concurrent.atomic.AtomicInteger + +import org.springframework.beans.factory.BeanRegistrar +import org.springframework.beans.factory.BeanRegistry +import org.springframework.beans.factory.support.BeanDefinitionOverrideException +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +import org.springframework.core.env.StandardEnvironment + +import grails.core.DefaultGrailsApplication +import grails.core.GrailsApplication +import grails.plugins.DefaultGrailsPluginManager +import grails.plugins.GrailsPluginManager +import grails.plugins.Plugin +import grails.util.Environment +import grails.util.Holders +import org.apache.grails.core.plugins.DefaultPluginDiscovery +import org.apache.grails.core.plugins.PluginDiscovery +import spock.lang.Specification + +/** + * Proves the architectural linchpin of {@link GrailsEarlyPluginRegistrationPostProcessor}: a plugin + * bean registered via {@code doWithSpring} lands in the registry before + * {@code ConfigurationClassPostProcessor} evaluates {@code @ConditionalOnMissingBean}, so the + * matching Boot auto-config bean backs off — instead of the plugin having to override or remove + * it afterwards. + */ +class EarlyPluginRegistrationOrderingSpec extends Specification { + + void 'a plugin doWithSpring bean registered early makes a @ConditionalOnMissingBean auto-config bean defer'() { + given: 'a context whose configuration would, by itself, register a conditional myResolver' + def ctx = new AnnotationConfigApplicationContext() + ctx.register(EarlyOrderingAutoConfigLikeConfig) + + and: 'a plugin discovery promoted to the context, exactly as the bootstrap registry does' + def discovery = new DefaultPluginDiscovery([earlyOrderingPluginClass] as Class[]) + discovery.loadPluginsFromClasspath = false + discovery.init(ctx.environment) + ctx.beanFactory.registerSingleton(PluginDiscovery.BEAN_NAME, discovery) + + and: 'the early registration phase installed through its real entry point' + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: 'the context refreshes' + ctx.refresh() + + then: 'the early (plugin) bean is the one named myResolver...' + ctx.getBean('myResolver') instanceof EarlyOrderingPluginResolver + + and: '...and the conditional default was never created' + ctx.getBeansOfType(EarlyOrderingBootDefaultResolver).isEmpty() + + and: 'the one true grailsApplication and pluginManager singletons were promoted' + ctx.getBean(GrailsApplication.APPLICATION_ID) instanceof GrailsApplication + ctx.getBean(GrailsPluginManager.BEAN_NAME) instanceof GrailsPluginManager + ctx.getBean(GrailsPluginManager.BEAN_NAME, GrailsPluginManager).getGrailsPlugin('earlyOrdering') != null + + and: 'the environment initializing flag was reset on refresh' + !Environment.isInitializing() + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'without a promoted plugin discovery the early phase is a no-op and the conditional bean is created (control)'() { + given: + def ctx = new AnnotationConfigApplicationContext() + ctx.register(EarlyOrderingAutoConfigLikeConfig) + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: + ctx.refresh() + + then: 'the conditional default wins when nothing registered the bean first' + ctx.getBean('myResolver') instanceof EarlyOrderingBootDefaultResolver + + and: 'no Grails singletons were promoted' + !ctx.beanFactory.containsSingleton(GrailsApplication.APPLICATION_ID) + !ctx.beanFactory.containsSingleton(GrailsPluginManager.BEAN_NAME) + + cleanup: + ctx.close() + } + + void 'a plugin beanRegistrar bean registered early makes a @ConditionalOnMissingBean auto-config bean defer'() { + given: 'a context whose configuration would, by itself, register a conditional myResolver' + def ctx = new AnnotationConfigApplicationContext() + ctx.register(EarlyOrderingAutoConfigLikeConfig) + + and: 'a plugin exposing a BeanRegistrar promoted through plugin discovery' + registerDiscovery(ctx, EarlyOrderingRegistrarGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: 'the context refreshes' + ctx.refresh() + + then: 'the registrar (plugin) bean is the one named myResolver and the conditional default backed off' + ctx.getBean('myResolver') instanceof EarlyOrderingPluginResolver + ctx.getBeansOfType(EarlyOrderingBootDefaultResolver).isEmpty() + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'plugin beanRegistrar and doWithSpring can coexist on one plugin'() { + given: + def ctx = new AnnotationConfigApplicationContext() + registerDiscovery(ctx, EarlyOrderingDualGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: + ctx.refresh() + + then: 'both the DSL bean and the registrar bean are present' + ctx.getBean('dualDslBean') instanceof EarlyOrderingPluginResolver + ctx.getBean('dualRegistrarBean') instanceof EarlyOrderingPluginResolver + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'a plugin beanRegistrar defined as a coerced closure registers its bean'() { + given: 'a plugin whose beanRegistrar() is a closure coerced to BeanRegistrar (no separate class)' + def ctx = new AnnotationConfigApplicationContext() + registerDiscovery(ctx, EarlyOrderingClosureRegistrarGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: + ctx.refresh() + + then: 'the bean registered by the closure-coerced registrar is present' + ctx.getBean('closureRegistrarBean') instanceof EarlyOrderingPluginResolver + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'an application doWithSpring bean still overrides a plugin bean of the same name under the default settings'() { + given: 'a plugin registering overrideProbe and an application registering a different bean under the same name' + def ctx = new AnnotationConfigApplicationContext() + registerDiscovery(ctx, EarlyOrderingOverrideGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + ctx.register(EarlyOrderingOverrideApplication) + + when: + ctx.refresh() + + then: 'the application bean wins, replacing the plugin bean registered in the early phase' + ctx.getBean('overrideProbe') instanceof EarlyOrderingAppResolver + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'with bean-definition overriding disabled an application bean shadowing a plugin bean fails'() { + given: 'overriding disabled, a plugin registering overrideProbe and an app registering the same name' + def ctx = new AnnotationConfigApplicationContext() + ctx.allowBeanDefinitionOverriding = false + registerDiscovery(ctx, EarlyOrderingOverrideGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + ctx.register(EarlyOrderingOverrideApplication) + + when: 'the context refreshes' + ctx.refresh() + + then: 'the colliding application bean is rejected rather than silently merging (see the upgrade notes)' + thrown(BeanDefinitionOverrideException) + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'the early phase loads plugins with a single manager pass, not a throwaway extra one'() { + given: 'the plugin construction count from one bare plugin-manager load, as a baseline' + // Grails wraps each plugin in a GrailsClass (a reference instance) and then creates the + // real instance, so a single load constructs the plugin more than once; what the retimed + // lifecycle guarantees is a single manager pass rather than a throwaway pass plus the real + // one (the rejected earlier approach), which would multiply this count. + EarlyOrderingCountingGrailsPlugin.INSTANCE_COUNT.set(0) + def baselineDiscovery = new DefaultPluginDiscovery([EarlyOrderingCountingGrailsPlugin] as Class[]) + baselineDiscovery.loadPluginsFromClasspath = false + baselineDiscovery.init(new StandardEnvironment()) + new DefaultGrailsPluginManager(new DefaultGrailsApplication(), baselineDiscovery).loadPlugins() + int singleLoadCount = EarlyOrderingCountingGrailsPlugin.INSTANCE_COUNT.get() + + and: 'the early phase booted through the real initializer' + EarlyOrderingCountingGrailsPlugin.INSTANCE_COUNT.set(0) + def ctx = new AnnotationConfigApplicationContext() + registerDiscovery(ctx, EarlyOrderingCountingGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: 'the context refreshes' + ctx.refresh() + + then: 'the early phase instantiates the plugin no more than a single manager load — no throwaway second pass' + singleLoadCount > 0 + EarlyOrderingCountingGrailsPlugin.INSTANCE_COUNT.get() == singleLoadCount + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + void 'the initializing flag is reset when the early phase throws'() { + given: 'a plugin whose doWithSpring throws while the early phase drains it' + def ctx = new AnnotationConfigApplicationContext() + registerDiscovery(ctx, EarlyOrderingThrowingGrailsPlugin) + new GrailsPluginLifecycleInitializer().initialize(ctx) + + when: 'the context refreshes' + ctx.refresh() + + then: 'the refresh fails...' + thrown(Exception) + + and: '...but the initializing flag (a system property) was reset, not leaked to later contexts' + !Environment.isInitializing() + + cleanup: + ctx.close() + Holders.clear() + Environment.setInitializing(false) + } + + private static void registerDiscovery(AnnotationConfigApplicationContext ctx, Class pluginClass) { + def discovery = new DefaultPluginDiscovery([pluginClass] as Class[]) + discovery.loadPluginsFromClasspath = false + discovery.init(ctx.environment) + ctx.beanFactory.registerSingleton(PluginDiscovery.BEAN_NAME, discovery) + } + + private static Class getEarlyOrderingPluginClass() { + new GroovyClassLoader(EarlyPluginRegistrationOrderingSpec.classLoader).parseClass(''' +class EarlyOrderingGrailsPlugin { + def version = '1.0' + def doWithSpring = { + myResolver(grails.boot.config.EarlyOrderingPluginResolver) + } +} +''') + } + + /** Stands in for a Spring Boot auto-configuration: a name-guarded conditional bean. */ + @Configuration + static class EarlyOrderingAutoConfigLikeConfig { + + @Bean + @ConditionalOnMissingBean(name = 'myResolver') + EarlyOrderingBootDefaultResolver myResolver() { + new EarlyOrderingBootDefaultResolver() + } + } +} + +class EarlyOrderingBootDefaultResolver { +} + +class EarlyOrderingPluginResolver { +} + +class EarlyOrderingRegistrarGrailsPlugin extends Plugin { + + def version = '1.0' + + @Override + BeanRegistrar beanRegistrar() { + new EarlyOrderingResolverRegistrar() + } +} + +class EarlyOrderingResolverRegistrar implements BeanRegistrar { + + @Override + void register(BeanRegistry registry, org.springframework.core.env.Environment environment) { + registry.registerBean('myResolver', EarlyOrderingPluginResolver) + } +} + +class EarlyOrderingDualGrailsPlugin extends Plugin { + + def version = '1.0' + + @Override + Closure doWithSpring() { + { -> + dualDslBean(EarlyOrderingPluginResolver) + } + } + + @Override + BeanRegistrar beanRegistrar() { + new EarlyOrderingDualRegistrar() + } +} + +class EarlyOrderingDualRegistrar implements BeanRegistrar { + + @Override + void register(BeanRegistry registry, org.springframework.core.env.Environment environment) { + registry.registerBean('dualRegistrarBean', EarlyOrderingPluginResolver) + } +} + +class EarlyOrderingClosureRegistrarGrailsPlugin extends Plugin { + + def version = '1.0' + + @Override + BeanRegistrar beanRegistrar() { + { BeanRegistry registry, org.springframework.core.env.Environment environment -> + registry.registerBean('closureRegistrarBean', EarlyOrderingPluginResolver) + } as BeanRegistrar + } +} + +class EarlyOrderingCountingGrailsPlugin extends Plugin { + + static final AtomicInteger INSTANCE_COUNT = new AtomicInteger() + + def version = '1.0' + + EarlyOrderingCountingGrailsPlugin() { + INSTANCE_COUNT.incrementAndGet() + } +} + +class EarlyOrderingThrowingGrailsPlugin extends Plugin { + + def version = '1.0' + + @Override + Closure doWithSpring() { + { -> throw new IllegalStateException('boom from doWithSpring') } + } +} + +class EarlyOrderingAppResolver { +} + +class EarlyOrderingOverrideGrailsPlugin extends Plugin { + + def version = '1.0' + + @Override + Closure doWithSpring() { + { -> + overrideProbe(EarlyOrderingPluginResolver) + } + } +} + +class EarlyOrderingOverrideApplication extends GrailsAutoConfiguration { + + @Override + Closure doWithSpring() { + { -> + overrideProbe(EarlyOrderingAppResolver) + } + } +} diff --git a/grails-core/src/test/groovy/grails/core/DefaultGrailsApplicationSpec.groovy b/grails-core/src/test/groovy/grails/core/DefaultGrailsApplicationSpec.groovy new file mode 100644 index 00000000000..01b216489e9 --- /dev/null +++ b/grails-core/src/test/groovy/grails/core/DefaultGrailsApplicationSpec.groovy @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.core + +import spock.lang.Specification + +class DefaultGrailsApplicationSpec extends Specification { + + void 'setApplicationClass adopts the class once and rejects reassignment to a different class'() { + given: 'an application constructed without an application class (as the early phase does)' + def application = new DefaultGrailsApplication() + GrailsApplicationClass first = Mock(GrailsApplicationClass) + GrailsApplicationClass second = Mock(GrailsApplicationClass) + + when: 'the application class is adopted for the first time' + application.applicationClass = first + + then: 'it is set' + application.applicationClass.is(first) + + when: 'the same class is set again' + application.applicationClass = first + + then: 'the set-once guard treats an identical value as idempotent' + noExceptionThrown() + application.applicationClass.is(first) + + when: 'a different application class is set' + application.applicationClass = second + + then: 'reassignment is rejected and the original is retained' + thrown(IllegalStateException) + application.applicationClass.is(first) + } +} diff --git a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy index b21856d0a85..62727e13dc1 100644 --- a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy +++ b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy @@ -183,7 +183,7 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { } boolean osivEnabled = config.getProperty('hibernate.osiv.enabled', Boolean, true) - boolean isWebApplication = beanDefinitionRegistry?.containsBeanDefinition('dispatcherServlet') || + boolean isWebApplication = isWebApplicationRegistry(beanDefinitionRegistry) || beanDefinitionRegistry?.containsBeanDefinition('grailsControllerHelper') if (isWebApplication && osivEnabled && ClassUtils.isPresent('org.grails.plugin.hibernate.support.GrailsOpenSessionInViewInterceptor')) { diff --git a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy index 554ec612f46..58940a847fb 100644 --- a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy +++ b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy @@ -216,7 +216,7 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { } boolean osivEnabled = config.getProperty('hibernate.osiv.enabled', Boolean, true) - boolean isWebApplication = beanDefinitionRegistry?.containsBeanDefinition('dispatcherServlet') || + boolean isWebApplication = isWebApplicationRegistry(beanDefinitionRegistry) || beanDefinitionRegistry?.containsBeanDefinition('grailsControllerHelper') if (isWebApplication && osivEnabled && ClassUtils.isPresent('org.grails.plugin.hibernate.support.GrailsOpenSessionInViewInterceptor')) { diff --git a/grails-data-mongodb/core/build.gradle b/grails-data-mongodb/core/build.gradle index af14d921b77..37099e2efff 100644 --- a/grails-data-mongodb/core/build.gradle +++ b/grails-data-mongodb/core/build.gradle @@ -132,6 +132,12 @@ dependencies { testImplementation 'org.apache.grails.testing:grails-testing-support-core' testImplementation 'org.spockframework:spock-core' testImplementation project(':grails-testing-support-mongodb') + testImplementation project(':grails-datastore-web'), { + // test: OpenSessionInViewInterceptor, spring-web's GenericWebApplicationContext + } + testImplementation 'jakarta.servlet:jakarta.servlet-api', { + // test: GenericWebApplicationContext requires ServletContext on the classpath + } testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:testcontainers-mongodb' diff --git a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy index 46b39854444..68dfe37d573 100644 --- a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy +++ b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy @@ -126,7 +126,7 @@ class MongoDbDataStoreSpringInitializer extends AbstractDatastoreInitializer { } def classLoader = getClass().getClassLoader() - if (beanDefinitionRegistry.containsBeanDefinition('dispatcherServlet') && ClassUtils.isPresent(OSIV_CLASS_NAME, classLoader)) { + if (isWebApplicationRegistry(beanDefinitionRegistry) && ClassUtils.isPresent(OSIV_CLASS_NAME, classLoader)) { String interceptorName = 'mongoOpenSessionInViewInterceptor' "${interceptorName}"(ClassUtils.forName(OSIV_CLASS_NAME, classLoader)) { datastore = ref('mongoDatastore') diff --git a/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoOpenSessionInViewSpec.groovy b/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoOpenSessionInViewSpec.groovy new file mode 100644 index 00000000000..0c991037f3f --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoOpenSessionInViewSpec.groovy @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.mongodb.bootstrap + +import org.apache.grails.testing.mongo.AutoStartedMongoSpec +import org.grails.datastore.mapping.mongo.MongoDatastore +import org.grails.datastore.mapping.mongo.config.MongoSettings +import org.springframework.context.support.GenericApplicationContext +import org.springframework.web.context.support.GenericWebApplicationContext + +/** + * Verifies the MongoDB open-session-in-view interceptor registers whenever the target registry is a + * {@code WebApplicationContext}, independent of whether a {@code dispatcherServlet} bean definition has + * been registered yet. This is the registration-order-independent branch of + * {@code AbstractDatastoreInitializer#isWebApplicationRegistry} that lets the interceptor keep registering + * once plugin beans drain before Spring Boot auto-configuration (which is what registers + * {@code dispatcherServlet}). + */ +class MongoOpenSessionInViewSpec extends AutoStartedMongoSpec { + + void "the mongo open session in view interceptor registers in a web application context"() { + given: "the initializer targeting a WebApplicationContext with no dispatcherServlet bean definition" + def initializer = makeInitializer() + def applicationContext = new GenericWebApplicationContext() + + when: "the context is configured and refreshed" + initializer.configureForBeanDefinitionRegistry(applicationContext) + applicationContext.refresh() + + then: "the OSIV interceptor is registered" + applicationContext.containsBean('mongoOpenSessionInViewInterceptor') + + cleanup: + applicationContext.getBean(MongoDatastore).destroy() + applicationContext.close() + } + + void "the mongo open session in view interceptor is not registered outside a web application context (control)"() { + given: "the same initializer targeting a plain, non-web ApplicationContext" + def initializer = makeInitializer() + def applicationContext = new GenericApplicationContext() + + when: "the context is configured and refreshed" + initializer.configureForBeanDefinitionRegistry(applicationContext) + applicationContext.refresh() + + then: "the OSIV interceptor is not registered" + !applicationContext.containsBean('mongoOpenSessionInViewInterceptor') + + cleanup: + applicationContext.getBean(MongoDatastore).destroy() + applicationContext.close() + } + + private MongoDbDataStoreSpringInitializer makeInitializer() { + new MongoDbDataStoreSpringInitializer([ + (MongoSettings.SETTING_HOST): mongoHost, + (MongoSettings.SETTING_PORT): mongoPort, + ]) { + @Override + protected Map> loadDataServices(String secondaryDatastore = null) { + [:] + } + } + } +} diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy index 112f993c091..5655eebb7e0 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy @@ -71,6 +71,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { public static final String TRANSACTION_MANAGER_BEAN = 'transactionManager' public static final String ENTITY_CLASS_RESOURCE_PATTERN = '/**/*.class' public static final String OSIV_CLASS_NAME = 'org.grails.datastore.mapping.web.support.OpenSessionInViewInterceptor' + public static final String WEB_APPLICATION_CONTEXT_CLASS_NAME = 'org.springframework.web.context.WebApplicationContext' PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver() Collection persistentClasses = [] @@ -304,7 +305,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { "${type}PersistenceContextInterceptorAggregator"(PersistenceContextInterceptorAggregator) def classLoader = Thread.currentThread().contextClassLoader - if (registry.containsBeanDefinition('dispatcherServlet') && ClassUtils.isPresent(OSIV_CLASS_NAME, classLoader)) { + if (isWebApplicationRegistry(registry) && ClassUtils.isPresent(OSIV_CLASS_NAME, classLoader)) { String interceptorName = "${type}OpenSessionInViewInterceptor" "${interceptorName}"(ClassUtils.forName(OSIV_CLASS_NAME, classLoader)) { datastore = ref("${type}Datastore") @@ -360,6 +361,27 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { registry.containsBeanDefinition(beanName) || (builder.hasProperty('springConfig') && builder.springConfig.containsBean(beanName)) } + /** + * Determines whether the given registry belongs to a web application. The presence of the + * {@code dispatcherServlet} bean definition is only a reliable signal after Spring Boot + * auto-configuration has been processed; Grails plugin bean definitions register before + * that, so the type of the registry (the web application context itself) is checked as well. + * + * @param registry The bean definition registry + * @return true if the registry belongs to a web application + */ + protected boolean isWebApplicationRegistry(BeanDefinitionRegistry registry) { + if (registry == null) { + return false + } + if (registry.containsBeanDefinition('dispatcherServlet')) { + return true + } + ClassLoader classLoader = getClass().classLoader + return ClassUtils.isPresent(WEB_APPLICATION_CONTEXT_CLASS_NAME, classLoader) && + ClassUtils.forName(WEB_APPLICATION_CONTEXT_CLASS_NAME, classLoader).isInstance(registry) + } + /** * @return The class used to define the persistence interceptor */ diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy new file mode 100644 index 00000000000..0a76590f3ab --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.grails.datastore.gorm.bootstrap + +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RootBeanDefinition +import spock.lang.Specification + +/** + * Unit coverage for the web-application detection that guards open-session-in-view registration in + * the Hibernate and MongoDB datastore initializers. Because plugin beans now register before Spring + * Boot auto-configuration, the check can no longer rely on the {@code dispatcherServlet} bean being + * present, so it also treats the web application context itself as the signal. + * + *

The web-application-context branch depends on {@code spring-web}, which this module does not + * pull in (that is exactly why the check resolves the type reflectively); that branch is covered + * end-to-end by the Hibernate open-session-in-view integration tests in a real web application. + */ +class AbstractDatastoreInitializerWebApplicationSpec extends Specification { + + private static boolean isWeb(BeanDefinitionRegistry registry) { + new TestDatastoreInitializer().isWebApplicationRegistry(registry) + } + + void 'a null registry is not a web application'() { + expect: + !isWeb(null) + } + + void 'a registry with a dispatcherServlet bean definition is a web application'() { + given: + def registry = new DefaultListableBeanFactory() + registry.registerBeanDefinition('dispatcherServlet', new RootBeanDefinition(Object)) + + expect: + isWeb(registry) + } + + void 'a plain registry with neither a dispatcherServlet nor a web context type is not a web application'() { + expect: + !isWeb(new DefaultListableBeanFactory()) + } + + static class TestDatastoreInitializer extends AbstractDatastoreInitializer { + Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { { -> } } + protected Class getPersistenceInterceptorClass() { null } + } +} diff --git a/grails-doc/src/en/guide/introduction/whatsNew.adoc b/grails-doc/src/en/guide/introduction/whatsNew.adoc index d2004bbc5c3..d827e4f6a38 100644 --- a/grails-doc/src/en/guide/introduction/whatsNew.adoc +++ b/grails-doc/src/en/guide/introduction/whatsNew.adoc @@ -39,6 +39,35 @@ This brings the Spring Boot 4 modular artifact layout, Spring Framework 7 API re The Grails 8 upgrade guide calls out the major application-impacting changes and links to the Spring Boot 4.0 migration guide, Spring Boot 4.1 release notes and Spring Framework 7.0 release notes. +==== Plugin Beans Register Before Spring Boot Auto-Configuration + +Grails {grailsMajorVersion} unifies and retimes the plugin lifecycle so that the beans a plugin contributes are registered before Spring Boot processes its auto-configurations. +A plugin bean now takes precedence over a Spring Boot default guarded by `@ConditionalOnMissingBean` — Boot backs off in favour of the plugin's bean, with no need to override or remove it afterwards. + +Plugins register beans through the new `beanRegistrar()` hook, which returns a Spring Framework {springapi}org/springframework/beans/factory/BeanRegistrar.html[BeanRegistrar]. Because `BeanRegistrar` is a functional interface, a closure coerced with `as BeanRegistrar` is all that is required: + +[source,groovy] +---- +import org.springframework.beans.factory.BeanRegistrar +import org.springframework.beans.factory.BeanRegistry +import org.springframework.core.env.Environment +import grails.plugins.Plugin + +class MyGrailsPlugin extends Plugin { + + @Override + BeanRegistrar beanRegistrar() { + { BeanRegistry registry, Environment environment -> + registry.registerBean('myService', MyServiceImpl) + } as BeanRegistrar + } +} +---- + +`beanRegistrar()` replaces the `doWithSpring` bean builder DSL, which is deprecated in Grails {grailsMajorVersion}. +The DSL continues to work for now, but you are strongly urged to migrate. +See the xref:upgrading#upgrading80x[upgrade guide] for details. + ==== GSP Tag Library Improvements Grails {grailsMajorVersion} continues the move toward method-based TagLib handlers while preserving compatibility with existing closure-based tags. diff --git a/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc b/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc index bd3b8a7932d..4413ca03168 100644 --- a/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc +++ b/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc @@ -23,7 +23,46 @@ Grails provides a number of hooks to leverage the different parts of the system ==== Hooking into the Grails Spring configuration -First, you can hook in Grails runtime configuration overriding the `doWithSpring` method from the link:{api}grails/plugins/Plugin.html[Plugin] class and returning a closure that defines additional beans. For example the following snippet is from one of the core Grails plugins that provides link:i18n.html[i18n] support: +The recommended way to register beans from a plugin is to override the `beanRegistrar` method from the link:{api}grails/plugins/Plugin.html[Plugin] class and return a Spring Framework {springapi}org/springframework/beans/factory/BeanRegistrar.html[BeanRegistrar]. `BeanRegistrar` is a functional interface, so a closure coerced with `as BeanRegistrar` is all that is required — no separate class: + +[source,groovy] +---- +import org.springframework.beans.factory.BeanRegistrar +import org.springframework.beans.factory.BeanRegistry +import org.springframework.core.env.Environment +import org.springframework.web.servlet.i18n.CookieLocaleResolver +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor +import org.springframework.context.support.ReloadableResourceBundleMessageSource +import grails.plugins.Plugin + +class I18nGrailsPlugin extends Plugin { + + def version = "0.1" + + @Override + BeanRegistrar beanRegistrar() { + { BeanRegistry registry, Environment environment -> + registry.registerBean('messageSource', ReloadableResourceBundleMessageSource) { spec -> + spec.supplier { context -> + new ReloadableResourceBundleMessageSource(basename: 'WEB-INF/grails-app/i18n/messages') + } + } + registry.registerBean('localeChangeInterceptor', LocaleChangeInterceptor) { spec -> + spec.supplier { context -> + new LocaleChangeInterceptor(paramName: 'lang') + } + } + registry.registerBean('localeResolver', CookieLocaleResolver) + } as BeanRegistrar + } +} +---- + +For more involved or reusable registration you can instead implement `BeanRegistrar` as its own class and return an instance of it from `beanRegistrar()`. + +Plugin beans are registered before Spring Boot processes its auto-configurations, so a bean registered here takes precedence over any Spring Boot default guarded by `@ConditionalOnMissingBean` — there is no need to override or remove Boot's beans afterwards. + +Alternatively, plugins may override the `doWithSpring` method and return a closure that defines beans using the link:spring.html#theBeanBuilderDSLExplained[Spring Bean Builder] syntax: [source,groovy] ---- @@ -48,7 +87,7 @@ class I18nGrailsPlugin extends Plugin { } ---- -This plugin configures the Grails `messageSource` bean and a couple of other beans to manage Locale resolution and switching. It using the link:spring.html#theBeanBuilderDSLExplained[Spring Bean Builder] syntax to do so. +NOTE: `doWithSpring` is deprecated as of Grails 8 in favour of `beanRegistrar`. It continues to work — the beans it defines register at the same early point in the lifecycle — but new code should prefer the `BeanRegistrar` API. Alternatively, you can override the `doWithSpring(BeanBuilder)` method and register beans directly against the supplied `BeanBuilder` instead of returning a closure. This avoids the closure indirection and is the recommended form for statically compiled plugins: diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 26a0b0deca5..eb80fee1404 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -1446,5 +1446,71 @@ The framework beans still register by default; this only changes what happens wh Previously, defining one of these beans produced a bean-definition override (with a startup warning, or a failure when `spring.main.allow-bean-definition-overriding` is `false`). Now the framework bean simply backs off. -NOTE: The back-off applies to beans that are registered before auto-configuration conditions are evaluated — `@Bean` methods in application configuration classes. -Beans contributed later, through a plugin's `doWithSpring` or the application's `resources.groovy`, continue to take effect through bean-definition overriding, exactly as in previous releases. +NOTE: The clean back-off applies to beans that are registered before auto-configuration conditions are evaluated: `@Bean` methods in application configuration classes, and — as of the retimed plugin lifecycle (see _Plugin Beans Now Register Before Spring Boot Auto-Configuration_ below) — beans contributed through a plugin's `doWithSpring` or `beanRegistrar()`. +Beans contributed through the application's `resources.groovy` or `resources.xml` register later and continue to take effect through bean-definition overriding, exactly as in previous releases. + +==== 33. Plugin Beans Now Register Before Spring Boot Auto-Configuration + +In Grails 7 and earlier, the beans a plugin contributed through `doWithSpring` were registered *after* Spring Boot had processed its auto-configurations. +A plugin that wanted to replace a Boot-provided bean therefore had to rely on bean-definition overriding, and Boot beans guarded by `@ConditionalOnMissingBean` never saw the plugin's bean in time to back off. + +In Grails 8 the plugin lifecycle is unified and retimed: plugin bean definitions — both `doWithSpring` and the new `beanRegistrar()` hook — are drained into the bean definition registry *before* Spring Boot expands its auto-configurations. The practical effects are: + +* **Plugin beans win `@ConditionalOnMissingBean` races.** When a plugin registers a bean, a Boot auto-configuration bean of the same name or type that is guarded by `@ConditionalOnMissingBean` now backs off in favor of the plugin's bean. Plugins no longer need to override or remove Boot's defaults after the fact. +* **A single plugin manager and `GrailsApplication`.** The plugin manager and the `grailsApplication` are built once, early in the context lifecycle, and reused for the rest of startup, so plugins are loaded by a single manager pass rather than a throwaway pass followed by the real one. `Holders.grailsApplication` is available earlier than before. +* **Artefact discovery happens earlier.** Controllers, services, domain classes and other artefacts are discovered before plugin beans are registered, because core plugins iterate the application's artefacts while defining beans. The set of discovered artefacts is unchanged — only the timing moved. +* **Side-effectful `doWithSpring` closures still run once**, just earlier. Closures that read `grailsApplication.config` continue to work; the configuration is fully loaded (including plugin-contributed configuration) before any bean-definition code runs. +* **The application class is instantiated a second time during early artefact discovery.** A throwaway instance is created solely to compute the application's class scan; the real lifecycle bean is still created by Spring. An application-class constructor with side effects therefore runs twice per boot, so keep application-class constructors free of side effects. +* **Plugins that inspect the bean definition registry see a pre-auto-configuration registry.** A `doWithSpring` closure that checks for the presence of a bean definition (for example `dispatcherServlet` as a "web application" signal) now runs before Spring Boot has registered its auto-configured beans, so such a check returns `false` where it previously returned `true`. Plugin code should test a registration-order-independent signal instead — for example whether the application context is a web application context. +* **The `annotationHandlerMapping` and `annotationHandlerAdapter` beans were removed.** These duplicated the request-mapping infrastructure that Spring's own MVC configuration already provides and were never consulted at runtime, but with plugin beans now registering first the bare duplicate adapter started shadowing Spring's fully configured one. `@Controller` annotated beans continue to be handled by Spring Boot's standard MVC infrastructure. + +**The bean builder DSL is deprecated.** `doWithSpring()` on `GrailsApplicationLifeCycle` and `grails.plugins.Plugin` is deprecated as of Grails 8 — it continues to work, but the recommended way to register beans is the new `beanRegistrar()` hook, which returns a Spring Framework `org.springframework.beans.factory.BeanRegistrar`. + +Before (deprecated bean builder DSL): + +[source,groovy] +---- +import grails.plugins.Plugin + +class MyGrailsPlugin extends Plugin { + + @Override + Closure doWithSpring() { + { -> + myService(MyServiceImpl) { + timeout = config.getProperty('myPlugin.timeout', Integer, 30) + } + } + } +} +---- + +After (Spring `BeanRegistrar`). `BeanRegistrar` is a functional interface, so a closure coerced with `as BeanRegistrar` is all that is required — a separate class is optional and only worth it for more involved or reusable registration: + +[source,groovy] +---- +import grails.plugins.Plugin +import org.springframework.beans.factory.BeanRegistrar +import org.springframework.beans.factory.BeanRegistry +import org.springframework.core.env.Environment + +class MyGrailsPlugin extends Plugin { + + @Override + BeanRegistrar beanRegistrar() { + { BeanRegistry registry, Environment environment -> + registry.registerBean('myService', MyServiceImpl) { spec -> + spec.supplier { context -> + new MyServiceImpl(timeout: environment.getProperty('myPlugin.timeout', Integer, 30)) + } + } + } as BeanRegistrar + } +} +---- + +Both hooks may be used on the same plugin during migration; when a registrar and the DSL register a bean under the same name, the registrar's definition wins. + +**Bean-definition overriding edge case.** Grails defaults `spring.main.allow-bean-definition-overriding` to `true`, and under that default nothing changes: an application bean (from the application class, `resources.groovy` or `resources.xml`) that uses the same name as a plugin bean still replaces the plugin's bean. However, because plugin beans now register earlier and application beans register in a separate, later step, an application that explicitly sets `spring.main.allow-bean-definition-overriding` to `false` will see a `BeanDefinitionOverrideException` for an application bean that shadows a plugin bean — in previous releases the two definitions were merged before a single registry write, so the shadowing was silent even with overriding disabled. + +**Most applications and plugins need no action.** Behavior only changes where a plugin bean and a conditional Boot bean competed for the same name or type — the plugin bean now wins, which is almost always the intended outcome. diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/OpenSessionInViewSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/OpenSessionInViewSpec.groovy new file mode 100644 index 00000000000..833493acd1f --- /dev/null +++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/OpenSessionInViewSpec.groovy @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 functionaltests + +import grails.testing.mixin.integration.Integration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import spock.lang.Specification + +/** + * Verifies the Hibernate open-session-in-view interceptor still registers in a web application. + * Its registration is guarded by a web-application check inside the Hibernate plugin's bean + * definitions; because plugin beans now register before Spring Boot auto-configuration, that + * check can no longer rely on the {@code dispatcherServlet} bean definition being present. + */ +@Integration +class OpenSessionInViewSpec extends Specification { + + @Autowired + ApplicationContext applicationContext + + void "the open session in view interceptor is registered in a web application"() { + expect: + applicationContext.containsBean('openSessionInViewInterceptor') + } +} diff --git a/grails-test-examples/app3/grails-app/init/app3/Application.groovy b/grails-test-examples/app3/grails-app/init/app3/Application.groovy index 2c8e46b552d..3a2f9f97fd1 100755 --- a/grails-test-examples/app3/grails-app/init/app3/Application.groovy +++ b/grails-test-examples/app3/grails-app/init/app3/Application.groovy @@ -21,9 +21,24 @@ package app3 import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } + + // Default for the probe the loadafter plugin registers in doWithSpring. Plugin beans now + // register ahead of auto-configuration, so this @ConditionalOnMissingBean default must defer + // to it — PluginBeansBeforeAutoConfigurationSpec asserts the plugin's value wins. + @Bean + @ConditionalOnMissingBean(name = 'earlyPluginProbe') + String earlyPluginProbe() { 'from-conditional-default' } + + // Default for the probe the loadafter plugin registers via beanRegistrar(). Registrar beans also + // register ahead of auto-configuration, so this @ConditionalOnMissingBean default must defer to it. + @Bean + @ConditionalOnMissingBean(name = 'registrarProbe') + String registrarProbe() { 'from-conditional-default' } } diff --git a/grails-test-examples/app3/src/integration-test/groovy/app3/PluginBeansBeforeAutoConfigurationSpec.groovy b/grails-test-examples/app3/src/integration-test/groovy/app3/PluginBeansBeforeAutoConfigurationSpec.groovy new file mode 100644 index 00000000000..cd16296501f --- /dev/null +++ b/grails-test-examples/app3/src/integration-test/groovy/app3/PluginBeansBeforeAutoConfigurationSpec.groovy @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 app3 + +import grails.testing.mixin.integration.Integration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import spock.lang.Specification + +/** + * End-to-end test of the retimed plugin lifecycle through real plugin discovery and a real + * application boot. The {@code loadafter} plugin (an app3 dependency) registers + * {@code earlyPluginProbe} in plain {@code doWithSpring}, which now runs ahead of Spring Boot + * auto-configuration; {@code Application} defines a + * {@code @ConditionalOnMissingBean(name='earlyPluginProbe')} default. The plugin's bean must win. + * + *

The {@code loadafter} plugin also registers {@code registrarProbe} through the new + * {@code beanRegistrar()} API (a closure coerced to a Spring {@code BeanRegistrar}), which the + * matching {@code @ConditionalOnMissingBean} default must likewise defer to. + * + *

The strict ordering proof (the conditional default is never even created) is covered at unit + * level by {@code EarlyPluginRegistrationOrderingSpec} in grails-core, including its control case. + */ +@Integration +class PluginBeansBeforeAutoConfigurationSpec extends Specification { + + @Autowired + ApplicationContext applicationContext + + void "a plugin doWithSpring bean wins over the app's @ConditionalOnMissingBean default"() { + expect: 'the plugin registered the probe ahead of auto-config, so the conditional default deferred to it' + applicationContext.getBean('earlyPluginProbe') == 'from-plugin-doWithSpring' + } + + void "a plugin beanRegistrar bean wins over the app's @ConditionalOnMissingBean default"() { + expect: 'the plugin registrar registered the probe ahead of auto-config, so the conditional default deferred to it' + applicationContext.getBean('registrarProbe') == 'from-plugin-beanRegistrar' + } +} diff --git a/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/OpenSessionInViewSpec.groovy b/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/OpenSessionInViewSpec.groovy new file mode 100644 index 00000000000..833493acd1f --- /dev/null +++ b/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/OpenSessionInViewSpec.groovy @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 functionaltests + +import grails.testing.mixin.integration.Integration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import spock.lang.Specification + +/** + * Verifies the Hibernate open-session-in-view interceptor still registers in a web application. + * Its registration is guarded by a web-application check inside the Hibernate plugin's bean + * definitions; because plugin beans now register before Spring Boot auto-configuration, that + * check can no longer rely on the {@code dispatcherServlet} bean definition being present. + */ +@Integration +class OpenSessionInViewSpec extends Specification { + + @Autowired + ApplicationContext applicationContext + + void "the open session in view interceptor is registered in a web application"() { + expect: + applicationContext.containsBean('openSessionInViewInterceptor') + } +} diff --git a/grails-test-examples/issue-10279/src/integration-test/groovy/issue10279/ActuatorEnvClosureSpec.groovy b/grails-test-examples/issue-10279/src/integration-test/groovy/issue10279/ActuatorEnvClosureSpec.groovy index 7ad8fa2dfd4..b8e6178c701 100644 --- a/grails-test-examples/issue-10279/src/integration-test/groovy/issue10279/ActuatorEnvClosureSpec.groovy +++ b/grails-test-examples/issue-10279/src/integration-test/groovy/issue10279/ActuatorEnvClosureSpec.groovy @@ -53,4 +53,12 @@ class ActuatorEnvClosureSpec extends Specification implements HttpClientSupport and: 'the string property defined in application.groovy is present in the response' response.assertContains('hello-from-groovy-config') } + + void 'actuator /health endpoint returns 200'() { + when: 'the actuator health endpoint is accessed' + def response = http('/actuator/health') + + then: 'it too serializes to JSON — the same converter path the /env regression exposed' + response.assertStatus(200) + } } diff --git a/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy b/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy index 8a1c853f436..62254225deb 100755 --- a/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy +++ b/grails-test-examples/plugins/loadafter/src/main/groovy/loadafter/LoadafterGrailsPlugin.groovy @@ -20,6 +20,9 @@ package loadafter import grails.plugins.* +import org.springframework.beans.factory.BeanRegistrar +import org.springframework.beans.factory.BeanRegistry +import org.springframework.core.env.Environment class LoadafterGrailsPlugin extends Plugin { @@ -33,4 +36,26 @@ Brief summary/description of the plugin. def loadAfter = ['springSecurityCore'] + // Exercises the retimed plugin lifecycle end-to-end (see app3 PluginBeansBeforeAutoConfigurationSpec): + // doWithSpring beans register ahead of auto-configuration through real plugin discovery, so the + // app's @ConditionalOnMissingBean default for this name must defer. + @Override + Closure doWithSpring() { + { -> + earlyPluginProbe(String, 'from-plugin-doWithSpring') + } + } + + // Exercises the new beanRegistrar() API end-to-end using the concise closure-coercion form. + // Registrar beans also register ahead of auto-configuration, so the app's @ConditionalOnMissingBean + // default for this name must defer — PluginBeansBeforeAutoConfigurationSpec asserts the plugin's value wins. + @Override + BeanRegistrar beanRegistrar() { + { BeanRegistry registry, Environment environment -> + registry.registerBean('registrarProbe', String) { spec -> + spec.supplier { context -> 'from-plugin-beanRegistrar' } + } + } as BeanRegistrar + } + }