diff --git a/grails-core/src/main/groovy/grails/plugins/Plugin.groovy b/grails-core/src/main/groovy/grails/plugins/Plugin.groovy index b3ca44ddca1..21335ba6c88 100644 --- a/grails-core/src/main/groovy/grails/plugins/Plugin.groovy +++ b/grails-core/src/main/groovy/grails/plugins/Plugin.groovy @@ -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 */ diff --git a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java index 8064850d832..b6839c07823 100644 --- a/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java +++ b/grails-core/src/main/groovy/org/grails/plugins/DefaultGrailsPlugin.java @@ -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; @@ -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); } else { if (!pluginBean.isReadableProperty(DO_WITH_SPRING)) { return; @@ -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(); diff --git a/grails-core/src/test/groovy/grails/plugins/PluginDoWithSpringMethodSpec.groovy b/grails-core/src/test/groovy/grails/plugins/PluginDoWithSpringMethodSpec.groovy new file mode 100644 index 00000000000..c97ec0cb9a9 --- /dev/null +++ b/grails-core/src/test/groovy/grails/plugins/PluginDoWithSpringMethodSpec.groovy @@ -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) + } + } +} diff --git a/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc b/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc index f7b6593ea5d..bd3b8a7932d 100644 --- a/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc +++ b/grails-doc/src/en/guide/plugins/hookingIntoRuntimeConfiguration.adoc @@ -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