Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a853dda
Retime plugin doWithSpring to run before Spring Boot auto-configuration
codeconsole Jul 7, 2026
de3dd94
Add BeanRegistrar plugin hook and deprecate the doWithSpring bean DSL
codeconsole Jul 7, 2026
12ffe36
Verify application bean overriding and add end-to-end early plugin ph…
codeconsole Jul 7, 2026
eb79849
Resolve application classes for early artefact discovery via classes()
codeconsole Jul 7, 2026
0a84ee5
Detect web applications without relying on the dispatcherServlet bean
codeconsole Jul 7, 2026
e16f609
Merge branch '8.0.x' into feat/plugin-beans-before-autoconfig
codeconsole Jul 7, 2026
4a30a7d
Merge branch '8.0.x' into feat/plugin-beans-before-autoconfig
jdaugherty Jul 7, 2026
dd5ac78
Remove the duplicate annotation handler mapping and adapter beans
codeconsole Jul 7, 2026
a1a6a20
Merge remote-tracking branch 'upstream/8.0.x' into feat/plugin-beans-…
codeconsole Jul 8, 2026
c082a42
Merge branch '8.0.x' into feat/plugin-beans-before-autoconfig
codeconsole Jul 8, 2026
78b225e
Address review feedback on the plugin lifecycle change
codeconsole Jul 8, 2026
e737052
Add test coverage for review-audit gaps
codeconsole Jul 9, 2026
d054328
test(grails-data-mongodb): add unit-testable Mongo OSIV coverage
borinquenkid Jul 9, 2026
1af592c
Replace the mongo OSIV integration test with a unit test of the share…
codeconsole Jul 9, 2026
193eb45
Merge remote-tracking branch 'origin/feat/plugin-beans-before-autocon…
codeconsole Jul 9, 2026
c1cb748
Merge branch '8.0.x' into feat/plugin-beans-before-autoconfig
codeconsole Jul 9, 2026
1dea040
Merge remote-tracking branch 'upstream/8.0.x' into feat/plugin-beans-…
codeconsole Jul 9, 2026
c825ffe
Merge remote-tracking branch 'origin/feat/plugin-beans-before-autocon…
codeconsole Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
35 changes: 35 additions & 0 deletions grails-core/src/main/groovy/grails/boot/GrailsApp.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Class<?>> 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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class> scanApplicationClasses(Class<?> applicationClass) {
Package applicationPackage = applicationClass.package
Collection<String> packageNames = applicationPackage != null ? [applicationPackage.name] : new ArrayList<String>()
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<Class> scanApplicationClasses(Class<?> applicationClass, Collection<String> packageNames) {
Collection<Class> 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<Class> loadTransformedClasses(ClassLoader classLoader) {
Collection<Class> classes = []
for (String className in AbstractGrailsArtefactTransformer.transformedClassNames) {
try {
classes << classLoader.loadClass(className)
} catch (ClassNotFoundException ignored) {
}
}
return classes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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<String> registeredClassNames = grailsApplication.allArtefacts*.name as Set<String>
for (cls in classes) {
if (!registeredClassNames.contains(cls.name)) {
grailsApplication.addArtefact(cls)
}
}
}
Comment thread
codeconsole marked this conversation as resolved.

protected void customizeGrailsApplication(GrailsApplication grailsApplication) {
Expand Down Expand Up @@ -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)
Comment thread
codeconsole marked this conversation as resolved.
}

if (loadExternalBeans) {
// now allow overriding via application
Expand Down Expand Up @@ -233,18 +284,64 @@ 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
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -80,25 +79,13 @@ class GrailsAutoConfiguration implements GrailsApplicationClass, ApplicationCont
* @return The classes that constitute the Grails application
*/
Collection<Class> classes() {
Collection<Class> 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<Class> classes = new HashSet()
classes.addAll(new ClassPathScanner().scan(new PathMatchingResourcePatternResolver(applicationContext), packageNames()))
classes.addAll(ApplicationArtefactScanner.loadTransformedClasses(getClass().classLoader))
return classes
}

Expand Down
Loading
Loading