jdaugherty commented on code in PR #15934:
URL: https://github.com/apache/grails-core/pull/15934#discussion_r3546350181


##########
grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy:
##########
@@ -194,8 +240,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)

Review Comment:
   When the early phase did **not** run (no promoted `PluginDiscovery` — 
unit-test slices, contexts not booted through `GrailsApp`/the bootstrap 
registry), this fallback drains only `doWithSpring` via 
`doRuntimeConfiguration`. Plugin `beanRegistrar()`s are never applied on this 
path — the only registrar applied here is the application's own 
(`lifeCycle.beanRegistrar()` below). A plugin that migrates to the new 
recommended API would silently lose its beans in any context that takes this 
fallback, which is a nasty asymmetry with the deprecated DSL that still works 
everywhere. Suggest also iterating `pluginManager.allPlugins` here and applying 
each plugin's `getBeanRegistrar()` (same filtering as 
`GrailsEarlyPluginRegistrationPostProcessor.applyBeanRegistrars`), so plugin 
registrars behave identically on both paths.



##########
grails-core/src/main/groovy/grails/boot/config/ApplicationClassScanner.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 classes that constitute 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 ApplicationClassScanner {

Review Comment:
   This scanner implies it finds the Application class but we already have the 
FindmainClass for that so we should rename this to reflect its purpose.



##########
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

Review Comment:
   Nit: still no trailing newline at EOF (this diff touched the last line — 
easy moment to fix it).



##########
grails-core/src/main/groovy/grails/plugins/Plugin.groovy:
##########
@@ -105,10 +106,29 @@ 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 bean 
builder DSL continues
+     * to work, but {@link #beanRegistrar()} is the modern, Spring-native 
replacement.
      */
+    @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.
+     *
+     * <p>The returned registrar is applied before Spring Boot 
auto-configuration is processed, so
+     * beans registered here take precedence over Boot's {@code 
@ConditionalOnMissingBean} defaults.</p>
+     *
+     * @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 }

Review Comment:
   Why not initialize the bean registry and pass it as a method argument?  It's 
effectively a functional interface already and I'd assume people would just 
have the closure be that implementation.



##########
grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc:
##########
@@ -23,7 +23,52 @@ 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]:

Review Comment:
   I think we need to mention this in the what's new as well. 



##########
grails-core/src/main/groovy/grails/boot/config/GrailsEarlyPluginRegistrationPostProcessor.java:
##########
@@ -0,0 +1,278 @@
+/*
+ *  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.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 
<em>before</em> 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.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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<ContextRefreshedEvent> {
+
+    /**
+     * 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;
+        }
+
+        Environment.setInitializing(true);
+
+        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);
+
+        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);

Review Comment:
   Minor: the marker (`Boolean`) and the `Class<?>[]` stash registered by 
`GrailsApp` remain manual singletons for the life of the context, so they show 
up as autowire-by-type candidates (`getBeansOfType(Boolean)` etc.). The marker 
has to survive until `GrailsApplicationPostProcessor` checks it, but the 
source-classes stash could be `destroySingleton`-ed once resolved to avoid 
leaking it into the context.



##########
grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc:
##########
@@ -23,7 +23,52 @@ 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]:
+
+[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() {
+        new I18nBeanRegistrar()

Review Comment:
   Since only 1 registrar can be returned, wha'ts the benefit here?  It seems 
like it's a lot more boiler plate.  if we built our own registrar and passed 
in, people could call just call registerBean, yes?  



##########
grails-core/src/main/groovy/grails/core/DefaultGrailsApplication.java:
##########
@@ -219,6 +219,18 @@ public GrailsApplicationClass getApplicationClass() {
         return applicationClass;
     }
 
+    /**
+     * Sets the application class. Used to adopt the application class when 
this instance was
+     * constructed before the {@link GrailsApplicationClass} was available, 
e.g. during early
+     * plugin registration ahead of Spring Boot auto-configuration.
+     *
+     * @param applicationClass The application class
+     * @since 8.0
+     */
+    public void setApplicationClass(GrailsApplicationClass applicationClass) {

Review Comment:
   What prevents someone calling this after initialize?  I think this has to be 
guarded so it can only be set at certain points in the life cycle.



##########
grails-core/src/main/groovy/grails/boot/config/GrailsEarlyPluginRegistrationPostProcessor.java:
##########
@@ -0,0 +1,278 @@
+/*
+ *  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.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 
<em>before</em> 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.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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<ContextRefreshedEvent> {
+
+    /**
+     * 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;
+        }
+
+        Environment.setInitializing(true);
+
+        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);
+
+        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 (a system
+        // property) does not leak once the context is up.
+        applicationContext.addApplicationListener(this);
+    }
+
+    /**
+     * 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<Class> scanApplicationSource(Class<?> source) {
+        if (GrailsAutoConfiguration.class.isAssignableFrom(source)) {
+            try {
+                GrailsAutoConfiguration application = 
(GrailsAutoConfiguration) source.getDeclaredConstructor().newInstance();
+                application.setApplicationContext(applicationContext);
+                return application.classes();
+            } catch (Throwable e) {

Review Comment:
   Two notes on this method:
   
   1. It instantiates the user's `Application` class a second time (once here 
for the scan, once later as the Spring bean). Constructor side effects — static 
init aside — now execute twice per boot, and `setApplicationContext` is invoked 
on a throwaway instance. Probably acceptable, but worth calling out explicitly 
in the upgrade notes since application-class constructors doing work is not 
unheard of.
   2. `catch (Throwable)` also swallows `OutOfMemoryError`/`StackOverflowError` 
into the fallback scan. `catch (Exception | LinkageError e)` would cover the 
realistic failure modes (missing no-arg ctor, class-init failures) without 
masking fatal errors.



##########
grails-core/src/main/groovy/grails/boot/GrailsApp.groovy:
##########
@@ -142,6 +143,22 @@ 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)
+        Class<?>[] sourceClasses = getAllSources().findAll { it instanceof 
Class } as Class<?>[]

Review Comment:
   `getAllSources()` can contain `String` class names 
(`SpringApplication.setSources` / `spring.main.sources`), which are dropped 
here. If the application class is supplied as a String while any other source 
is a `Class`, the stash is non-empty, so `resolveApplicationSourceClasses` 
never falls back to the registry scan — and the application class silently 
misses early artefact discovery (its controllers/services get no plugin beans). 
Suggest resolving String sources to classes here as well, or making the 
fallback in the post-processor also engage when the stash contains no 
`GrailsApplicationClass`.



##########
grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy:
##########
@@ -122,11 +148,31 @@ 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() {
+        for (cls in classes) {
+            if (!grailsApplication.isArtefact(cls)) {
+                grailsApplication.addArtefact(cls)
+            }
+        }
     }

Review Comment:
   Minor perf: `isArtefact(cls)` does a linear name-comparison scan over 
`allArtefactClasses`, so this loop is O(classes x registeredArtefacts) on every 
boot that took the early path — for a large app that's a quadratic pass over 
the full class list. Building a `Set` of registered class names once before the 
loop keeps it linear:
   
   ```suggestion
       private void registerRemainingApplicationClasses() {
           Set<String> registeredClassNames = 
grailsApplication.allArtefacts*.name as Set<String>
           for (cls in classes) {
               if (!registeredClassNames.contains(cls.name)) {
                   grailsApplication.addArtefact(cls)
               }
           }
       }
   ```



##########
grails-core/src/main/groovy/grails/boot/config/GrailsEarlyPluginRegistrationPostProcessor.java:
##########
@@ -0,0 +1,278 @@
+/*
+ *  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.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 
<em>before</em> 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.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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<ContextRefreshedEvent> {
+
+    /**
+     * 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;
+        }
+
+        Environment.setInitializing(true);

Review Comment:
   If anything in this phase throws (`loadPlugins()`, a plugin's 
`doWithSpring`, a registrar), or the refresh fails later for any reason, 
`Environment.setInitializing(true)` leaks — the reset listener is only added at 
the very end of this method and only fires on a *successful* refresh. Since the 
flag is a system property, a single failed context poisons every subsequent 
context in the same JVM (test forks especially). The same pattern pre-exists in 
`GrailsApplicationPostProcessor`, but this phase runs in more contexts and does 
much more work under the flag. Suggest wrapping the body in try/catch that 
calls `Environment.setInitializing(false)` before rethrowing (the success path 
still resets via the refresh listener).



##########
grails-core/src/main/groovy/grails/plugins/Plugin.groovy:
##########
@@ -105,10 +106,29 @@ 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 bean 
builder DSL continues

Review Comment:
   I would be stronger in the language here - Spring has said the bean dsl code 
will remain, but it won't be supported.  If there's an issue, they basically 
won't fix it.  So it's strongly urged to migrate.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to