Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -240,7 +240,9 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
def springConfig = new DefaultRuntimeSpringConfiguration()
def application = grailsApplication
Holders.setGrailsApplication(application)
if (!earlyPluginRegistrationRan) {
Holders.setGrailsApplication(application)
}

if (!earlyPluginRegistrationRan) {
// first register plugin beans; when the early phase ran they were
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.slf4j.LoggerFactory;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
Expand All @@ -41,6 +42,7 @@
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;

import grails.config.Settings;
import grails.core.DefaultGrailsApplication;
import grails.core.GrailsApplication;
import grails.core.GrailsApplicationClass;
Expand Down Expand Up @@ -123,6 +125,9 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
// The initializing flag is a system property, so a leak on failure poisons every subsequent
// context in the same JVM (test forks especially). Reset it if anything below throws; the
// success path leaves it set and resets on refresh via the listener added at the end.
GrailsApplication previousGrailsApplication = null;
GrailsApplication publishedGrailsApplication = null;
boolean publishedToLegacyHolder = false;
Environment.setInitializing(true);
try {
DefaultGrailsApplication grailsApplication = new DefaultGrailsApplication();
Expand All @@ -147,6 +152,21 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
}

RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration();
boolean legacyHoldersDuringDoWithSpring = applicationContext.getEnvironment()
.getProperty(Settings.LEGACY_HOLDERS_DURING_DO_WITH_SPRING, Boolean.class, false);
// This opt-in shim preserves Grails 7 compatibility for plugins that read Holders in
// doWithSpring. It is disabled by default because that global publication is unsafe
// during early plugin registration; plugins should migrate to injected dependencies.
if (legacyHoldersDuringDoWithSpring) {
LOG.warn("legacy doWithSpring compatibility shim is enabled. Migrate closures away from Holders access and remove '{}'",
Settings.LEGACY_HOLDERS_DURING_DO_WITH_SPRING);
previousGrailsApplication = Holders.replaceGrailsApplication(grailsApplication);
publishedGrailsApplication = grailsApplication;
publishedToLegacyHolder = true;
}
else {
previousGrailsApplication = Holders.findGrailsApplicationFallback();
}
pluginManager.doRuntimeConfiguration(springConfig);
springConfig.registerBeansWithRegistry(registry);
applyBeanRegistrars(pluginManager, registry);
Expand All @@ -155,16 +175,27 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, grailsApplication);
beanFactory.registerSingleton(GrailsPluginManager.BEAN_NAME, pluginManager);
beanFactory.registerSingleton(EARLY_REGISTRATION_COMPLETE_BEAN_NAME, Boolean.TRUE);
Holders.setGrailsApplication(grailsApplication);

// GrailsApplicationPostProcessor resets the initializing flag on refresh, but it is not
// present in every context that runs this phase — reset here as well so the flag does not
// leak once the context is up.
applicationContext.addApplicationListener(this);
if (!publishedToLegacyHolder) {
Holders.restoreGrailsApplication(previousGrailsApplication, grailsApplication);
}
}
catch (RuntimeException | Error e) {
catch (Throwable e) {
Environment.setInitializing(false);
throw e;
if (publishedToLegacyHolder) {
Holders.restoreGrailsApplicationAfterFailure(publishedGrailsApplication, previousGrailsApplication);
}
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error error) {
throw error;
}
throw new BeanInitializationException("Early Grails plugin registration failed", e);
}
}

Expand Down
9 changes: 9 additions & 0 deletions grails-core/src/main/groovy/grails/config/Settings.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ interface Settings {
*/
String PLUGIN_EXCLUDES = 'grails.plugin.excludes'

/**
* Whether legacy {@code doWithSpring} closures can access the application through
* {@code Holders}. Defaults to {@code false}; enable only while migrating those closures away
* from global {@code Holders} access.
*
* @since 8.0
*/
String LEGACY_HOLDERS_DURING_DO_WITH_SPRING = 'grails.legacy.holdersDuringDoWithSpring'

/**
* Whether to include the jsessionid in the rendered links
**/
Expand Down
178 changes: 172 additions & 6 deletions grails-core/src/main/groovy/grails/util/Holders.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@
*/
package grails.util;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -61,7 +67,10 @@ public class Holders {
createServletContextsHolder();
}

private static GrailsApplication applicationSingleton; // TODO remove
private static final AtomicReference<GrailsApplication> applicationSingleton = new AtomicReference<>(); // TODO remove
private static final Object applicationSingletonMonitor = new Object();
private static final ReferenceQueue<GrailsApplication> failedApplications = new ReferenceQueue<>();
private static final Map<WeakIdentityKey, GrailsApplication> failedApplicationPredecessors = new HashMap<>();

private Holders() {
// static only
Expand All @@ -80,7 +89,7 @@ public static void clear() {
servletContexts.set(null);
}
applicationDiscoveryStrategies.clear();
applicationSingleton = null;
clearGrailsApplicationState();
}

public static void setServletContext(final Object servletContext) {
Expand Down Expand Up @@ -129,7 +138,7 @@ public static GrailsApplication findApplication() {
return grailsApplication;
}
}
return applicationSingleton;
return findGrailsApplicationFallback();
}

public static GrailsApplication getGrailsApplication() {
Expand All @@ -138,8 +147,155 @@ public static GrailsApplication getGrailsApplication() {
return grailsApplication;
}

/**
* Returns the fallback Grails application without invoking discovery strategies.
*
* @return the fallback application, or {@code null}
*/
public static GrailsApplication findGrailsApplicationFallback() {
synchronized (applicationSingletonMonitor) {
drainFailedApplications();
return applicationSingleton.get();
}
}

/**
* Sets the fallback Grails application without invoking discovery strategies.
* A previously failed application is replaced by its first nonfailed predecessor.
*
* @param application the fallback application, or {@code null}
*/
public static void setGrailsApplication(GrailsApplication application) {
applicationSingleton = application;
synchronized (applicationSingletonMonitor) {
drainFailedApplications();
applicationSingleton.set(firstNonfailedApplication(application));
}
}

/**
* Replaces the fallback Grails application without invoking discovery strategies.
*
* <p>A previously failed requested application is replaced by its first nonfailed predecessor.
*
* @param application the new fallback application, or {@code null}
* @return the previous fallback application
*/
public static GrailsApplication replaceGrailsApplication(GrailsApplication application) {
synchronized (applicationSingletonMonitor) {
drainFailedApplications();
return applicationSingleton.getAndSet(firstNonfailedApplication(application));
}
}

/**
* Restores a fallback Grails application only if no other publisher has replaced it.
*
* <p>The ownership comparison retains identity semantics. A previously failed application
* requested for restoration is replaced by its first nonfailed predecessor.
*
* @param expected the fallback application owned by the caller
* @param application the fallback application to restore, or {@code null}
* @return {@code true} when the fallback was restored
*/
public static boolean restoreGrailsApplication(GrailsApplication expected, GrailsApplication application) {
synchronized (applicationSingletonMonitor) {
drainFailedApplications();
return applicationSingleton.compareAndSet(expected, firstNonfailedApplication(application));
}
}

/**
* Records a failed fallback publication and restores its first nonfailed predecessor if the
* caller still owns the fallback. The failure record is retained when another publisher is
* currently visible, allowing that publisher's later restore to skip the failed application.
* Discovery strategies are not invoked.
*
* <p>The failure key uses weak identity semantics. The predecessor remains strongly reachable
* only while the failed application is reachable, directly or through another live failure
* chain.
*
* @param expected the failed fallback application owned by the caller, never {@code null}
* @param previous the predecessor that was visible before publication, or {@code null}
* @return {@code true} when the failed fallback was still visible and was restored
*/
public static boolean restoreGrailsApplicationAfterFailure(GrailsApplication expected, GrailsApplication previous) {
synchronized (applicationSingletonMonitor) {
drainFailedApplications();
Assert.notNull(expected, "Expected failed GrailsApplication must not be null");
failedApplicationPredecessors.put(new WeakIdentityKey(expected, failedApplications), previous);
Set<GrailsApplication> visited = Collections.newSetFromMap(new IdentityHashMap<>());
visited.add(expected);
GrailsApplication application = firstNonfailedApplication(previous, visited);
return applicationSingleton.compareAndSet(expected, application);
}
}

private static GrailsApplication firstNonfailedApplication(GrailsApplication application) {
return firstNonfailedApplication(application, Collections.newSetFromMap(new IdentityHashMap<>()));
}

private static GrailsApplication firstNonfailedApplication(
GrailsApplication application, Set<GrailsApplication> visited) {
GrailsApplication candidate = application;
while (candidate != null) {
if (!visited.add(candidate)) {
// A publication chain cannot legitimately cycle. Terminate it both to avoid
// resurrection and to prevent strong predecessor values retaining the cycle.
failedApplicationPredecessors.put(new WeakIdentityKey(candidate, failedApplications), null);
return null;
}
WeakIdentityKey lookupKey = new WeakIdentityKey(candidate);
if (!failedApplicationPredecessors.containsKey(lookupKey)) {
return candidate;
}
candidate = failedApplicationPredecessors.get(lookupKey);
}
return null;
}

private static void drainFailedApplications() {
WeakIdentityKey failedApplication;
while ((failedApplication = (WeakIdentityKey) failedApplications.poll()) != null) {
failedApplicationPredecessors.remove(failedApplication);
}
}

/**
* Weak map key whose hash and equality use Grails application identity rather than
* application-defined equality. Cleared keys compare only to themselves so queue removal
* remains reliable without allowing unrelated cleared references to compare equal.
*/
private static final class WeakIdentityKey extends WeakReference<GrailsApplication> {

private final int identityHashCode;

private WeakIdentityKey(GrailsApplication application) {
super(application);
identityHashCode = System.identityHashCode(application);
}

private WeakIdentityKey(
GrailsApplication application, ReferenceQueue<GrailsApplication> referenceQueue) {
super(application, referenceQueue);
identityHashCode = System.identityHashCode(application);
}

@Override
public int hashCode() {
return identityHashCode;
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof WeakIdentityKey otherKey)) {
return false;
}
GrailsApplication application = get();
return application != null && application == otherKey.get();
}
}

public static void setConfig(Config config) {
Expand Down Expand Up @@ -202,13 +358,23 @@ public static GrailsPluginManager currentPluginManager() {

public static void reset() {
setPluginManager(null);
setGrailsApplication(null);
setServletContext(null);
clearGrailsApplicationState();
if (servletContexts != null) {
setServletContext(null);
}
setPluginManager(null);
setPluginManagerInCreation(false);
setConfig(null);
}

private static void clearGrailsApplicationState() {
synchronized (applicationSingletonMonitor) {
drainFailedApplications();
applicationSingleton.set(null);
failedApplicationPredecessors.clear();
}
}

private static <T> T get(Holder<T> holder, String type) {
return get(holder, type, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"name": "grails.bootstrap",
"description": "Core Properties"
},
{
"name": "grails.legacy",
"description": "Legacy Compatibility"
},
{
"name": "grails.spring",
"description": "Core Properties"
Expand Down Expand Up @@ -96,6 +100,12 @@
"description": "Whether to skip execution of BootStrap.groovy classes on startup.",
"defaultValue": false
},
{
"name": "grails.legacy.holdersDuringDoWithSpring",
"type": "java.lang.Boolean",
"description": "Whether legacy doWithSpring closures can access the promoted GrailsApplication through Holders during early plugin registration. Enable only while migrating legacy plugins away from global Holders access.",
"defaultValue": false
},
{
"name": "grails.spring.bean.packages",
"type": "java.util.List<java.lang.String>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,16 @@ class ConfigReportCommandSpec extends Specification {
profileEntry.description != null
profileEntry.description.length() > 0
metadataResult.groupDescriptions.get('grails') == 'Core Properties'

and: "legacy compatibility metadata retains its migration guidance"
ConfigReportCommand.ConfigPropertyMetadata legacyEntry = metadataResult.properties.find { ConfigReportCommand.ConfigPropertyMetadata property -> property.name == 'grails.legacy.holdersDuringDoWithSpring' }
legacyEntry.name == 'grails.legacy.holdersDuringDoWithSpring'
legacyEntry.type == 'java.lang.Boolean'
legacyEntry.defaultValue == false
legacyEntry.description != null
legacyEntry.description.length() > 0
legacyEntry.description.toLowerCase().contains('migrat')
metadataResult.groupDescriptions.get('grails.legacy') == 'Legacy Compatibility'
}

def "escapeAsciidoc handles null and empty strings"() {
Expand Down
Loading