Skip to content
Merged
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
14 changes: 14 additions & 0 deletions grails-core/src/main/groovy/grails/plugins/Plugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ abstract class Plugin implements GrailsApplicationLifeCycle, GrailsApplicationAw
@Override
Closure doWithSpring() { null }

/**
* Registers Spring beans directly against the supplied {@link BeanBuilder}. This is the statically-compilable
* alternative to {@link #doWithSpring()}: instead of returning a closure whose delegate the container wires up,
* an implementation registers beans against the builder passed as an argument. Subclasses should override.
*
* A plugin should override either this method or {@link #doWithSpring()}, but not both: the two forms are
* alternatives, and defining both causes the plugin to fail to load.
*
* @param beans The {@link BeanBuilder} to register beans against
*/
void doWithSpring(BeanBuilder beans) {
// no-op
}

/**
* Invoked in a phase where plugins can add dynamic methods. Subclasses should override
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -391,13 +392,23 @@ public void doWithRuntimeConfiguration(RuntimeSpringConfiguration springConfig)
b.setVariable("resolver", getResolver());

if (plugin instanceof Plugin) {
Closure c = ((Plugin) plugin).doWithSpring();
Plugin pluginObject = (Plugin) plugin;
// Legacy closure-returning hook: doWithSpring() returns a bean-defining closure
Closure c = pluginObject.doWithSpring();
// A plugin must define at most one Spring configuration hook. Both forms are explicit
// overrides, so defining both is an authoring error rather than a supported combination.
if (c != null && isDoWithSpringMethodOverridden(pluginObject)) {
throw new PluginException("Plugin [" + this + "] defines both the closure-returning doWithSpring() " +
"and the doWithSpring(BeanBuilder) method. Define only one Spring configuration hook.");
}
BeanBuilder bb = new BeanBuilder(getParentCtx(), springConfig, grailsApplication.getClassLoader());
bb.setBinding(b);
if (c != null) {
BeanBuilder bb = new BeanBuilder(getParentCtx(), springConfig, grailsApplication.getClassLoader());
bb.setBinding(b);
c.setDelegate(bb);
bb.invokeMethod("beans", new Object[]{c});
}
// Method-based hook: doWithSpring(BeanBuilder) registers beans directly against the builder
pluginObject.doWithSpring(bb);
Comment thread
jdaugherty marked this conversation as resolved.
} else {
if (!pluginBean.isReadableProperty(DO_WITH_SPRING)) {
return;
Expand All @@ -416,6 +427,24 @@ public void doWithRuntimeConfiguration(RuntimeSpringConfiguration springConfig)

}

/**
* Determines whether the plugin overrides the {@link Plugin#doWithSpring(BeanBuilder)} method, as opposed to
* inheriting the no-op base implementation. Used to detect a plugin that defines both the closure-returning and
* method-based Spring configuration hooks.
*
* @param pluginObject the plugin instance
* @return true if {@code doWithSpring(BeanBuilder)} is declared below {@link Plugin}
*/
private boolean isDoWithSpringMethodOverridden(Plugin pluginObject) {
try {
Method method = pluginObject.getClass().getMethod(DO_WITH_SPRING, BeanBuilder.class);
return method.getDeclaringClass() != Plugin.class;
}
catch (NoSuchMethodException e) {
return false;
}
}

@Override
public String getName() {
return pluginGrailsClass.getLogicalPropertyName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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.plugins

import grails.core.DefaultGrailsApplication
import grails.plugins.exceptions.PluginException
import grails.spring.BeanBuilder
import org.grails.plugins.DefaultGrailsPlugin
import org.grails.spring.DefaultRuntimeSpringConfiguration
import org.grails.spring.RuntimeSpringConfiguration
import org.springframework.beans.propertyeditors.ClassEditor
import spock.lang.Specification

/**
* Tests the method-based {@link Plugin#doWithSpring(BeanBuilder)} hook that complements the closure-returning
* {@link Plugin#doWithSpring()} hook.
*/
class PluginDoWithSpringMethodSpec extends Specification {

void "a Plugin subclass can register beans by overriding doWithSpring(BeanBuilder)"() {
given: "a plugin that registers a bean through the method-based hook"
def app = new DefaultGrailsApplication()
def plugin = new MethodHookPlugin(grailsApplication: app)
def bb = new BeanBuilder()

when: "the method-based hook is invoked with the builder"
plugin.doWithSpring(bb)
def ctx = bb.createApplicationContext()

then: "the bean registered against the builder is present"
ctx.containsBean('methodEditor')
}

void "the base Plugin doWithSpring(BeanBuilder) is a no-op"() {
given: "a plugin that overrides neither hook"
def app = new DefaultGrailsApplication()
def plugin = new NoOpPlugin(grailsApplication: app)
def bb = new BeanBuilder()

when: "the method-based hook is invoked"
plugin.doWithSpring(bb)
def ctx = bb.createApplicationContext()

then: "no beans are registered by the plugin and the closure hook still returns null"
plugin.doWithSpring() == null
ctx.beanDefinitionNames.length == 0
}

void "DefaultGrailsPlugin invokes the method-based doWithSpring(BeanBuilder) hook"() {
given: "a plugin descriptor that uses only the method-based hook"
def app = new DefaultGrailsApplication()
def plugin = new DefaultGrailsPlugin(MethodHookGrailsPlugin, app)
RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration()

when: "runtime configuration is performed"
plugin.doWithRuntimeConfiguration(springConfig)
def ctx = springConfig.applicationContext

then: "the bean registered via the method hook is present"
ctx.containsBean('methodEditor')
}

void "DefaultGrailsPlugin throws when a plugin defines both doWithSpring hooks"() {
given: "a plugin descriptor that overrides both hooks"
def app = new DefaultGrailsApplication()
def plugin = new DefaultGrailsPlugin(BothHooksGrailsPlugin, app)
RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration()

when: "runtime configuration is performed"
plugin.doWithRuntimeConfiguration(springConfig)

then: "a PluginException is thrown identifying the conflicting hooks"
PluginException e = thrown()
e.message.contains('doWithSpring()')
e.message.contains('doWithSpring(BeanBuilder)')
}

void "DefaultGrailsPlugin still supports the legacy closure-returning doWithSpring hook"() {
given: "a plugin descriptor that overrides only the closure-returning hook"
def app = new DefaultGrailsApplication()
def plugin = new DefaultGrailsPlugin(LegacyClosureGrailsPlugin, app)
RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration()

when: "runtime configuration is performed"
plugin.doWithRuntimeConfiguration(springConfig)
def ctx = springConfig.applicationContext

then: "the bean registered via the closure hook is present"
ctx.containsBean('classEditor')
}
}

class MethodHookPlugin extends Plugin {
@Override
void doWithSpring(BeanBuilder beans) {
beans.methodEditor(ClassEditor, grailsApplication.classLoader)
}
}

class NoOpPlugin extends Plugin {
}

class MethodHookGrailsPlugin extends Plugin {
def version = '1.0'

@Override
void doWithSpring(BeanBuilder beans) {
beans.methodEditor(ClassEditor, grailsApplication.classLoader)
}
}

class BothHooksGrailsPlugin extends Plugin {
def version = '1.0'

@Override
Closure doWithSpring() {
{ ->
classEditor(ClassEditor, application.classLoader)
}
}

@Override
void doWithSpring(BeanBuilder beans) {
beans.customEditor(ClassEditor, grailsApplication.classLoader)
}
}

class LegacyClosureGrailsPlugin extends Plugin {
def version = '1.0'

@Override
Closure doWithSpring() {
{ ->
classEditor(ClassEditor, application.classLoader)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ class I18nGrailsPlugin extends Plugin {

This plugin configures the Grails `messageSource` bean and a couple of other beans to manage Locale resolution and switching. It using the link:spring.html#theBeanBuilderDSLExplained[Spring Bean Builder] syntax to do so.

Alternatively, you can override the `doWithSpring(BeanBuilder)` method and register beans directly against the supplied `BeanBuilder` instead of returning a closure. This avoids the closure indirection and is the recommended form for statically compiled plugins:

[source,groovy]
----
import org.springframework.web.servlet.i18n.CookieLocaleResolver
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
import org.springframework.context.support.ReloadableResourceBundleMessageSource
import grails.plugins.*
import grails.spring.BeanBuilder

class I18nGrailsPlugin extends Plugin {

def version = "0.1"

@Override
void doWithSpring(BeanBuilder beans) {
beans.messageSource(ReloadableResourceBundleMessageSource) {
basename = "WEB-INF/grails-app/i18n/messages"
}
beans.localeChangeInterceptor(LocaleChangeInterceptor) {
paramName = "lang"
}
beans.localeResolver(CookieLocaleResolver)
}
}
----

A plugin may override either `doWithSpring()` or `doWithSpring(BeanBuilder)`, but not both. The closure-returning `doWithSpring()` remains fully supported for backwards compatibility. Defining both forms in the same plugin is an error: the plugin will fail to load with an exception, since the two forms are alternatives and defining both indicates an incomplete migration between them. Consolidate the bean definitions into a single form.


==== Customizing the Servlet Environment

Expand Down
Loading