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]