This is an automated email from the ASF dual-hosted git repository.

davydotcom pushed a commit to branch feature/taglib-method-actions
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 9a3ce312621ee0d43c32c236440a0e1c1ac3c21e
Author: David Estes <[email protected]>
AuthorDate: Wed Feb 25 21:40:12 2026 -0500

    Add method-based TagLib handlers with legacy compatibility and docs update
    
    - implement method-defined tag handler support and invocation context
    - preserve closure-style behavior across property/direct and namespaced 
paths
    - convert built-in web/GSP taglibs to method syntax
    - add compile-time warning for closure-defined tag fields
    - add coverage and benchmark for method vs closure invocation
    - update guides and demo taglib samples to method syntax
    
    Co-Authored-By: Oz <[email protected]>
---
 .../unitTesting/unitTestingTagLibraries.adoc       |   2 +-
 .../src/en/guide/theWebLayer/gsp/taglibs.adoc      |  17 ++-
 .../theWebLayer/gsp/taglibs/iterativeTags.adoc     |   4 +-
 .../guide/theWebLayer/gsp/taglibs/logicalTags.adoc |   2 +-
 .../guide/theWebLayer/gsp/taglibs/namespaces.adoc  |   2 +-
 .../guide/theWebLayer/gsp/taglibs/simpleTags.adoc  |   6 +-
 .../theWebLayer/gsp/taglibs/tagReturnValue.adoc    |   6 +-
 grails-doc/src/en/guide/theWebLayer/taglibs.adoc   |  17 ++-
 .../src/main/groovy/org/grails/gsp/GroovyPage.java |  29 +++-
 .../grails/core/gsp/DefaultGrailsTagLibClass.java  |   2 +
 .../org/grails/taglib/TagLibraryMetaUtils.groovy   |  49 +++++--
 .../groovy/org/grails/taglib/TagMethodContext.java |  58 ++++++++
 .../groovy/org/grails/taglib/TagMethodInvoker.java | 150 +++++++++++++++++++++
 .../main/groovy/org/grails/taglib/TagOutput.java   |  25 +++-
 .../main/groovy/grails/artefact/TagLibrary.groovy  |  35 ++++-
 .../TagLibArtefactTypeAstTransformation.java       |  20 +++
 .../plugins/web/taglib/ApplicationTagLib.groovy    |  26 ++--
 .../grails/plugins/web/taglib/CountryTagLib.groovy |   4 +-
 .../grails/plugins/web/taglib/FormTagLib.groovy    |  60 ++++-----
 .../grails/plugins/web/taglib/FormatTagLib.groovy  |   8 +-
 .../plugins/web/taglib/JavascriptTagLib.groovy     |   4 +-
 .../grails/plugins/web/taglib/PluginTagLib.groovy  |   6 +-
 .../plugins/web/taglib/UrlMappingTagLib.groovy     |  18 +--
 .../plugins/web/taglib/ValidationTagLib.groovy     |  20 +--
 .../web/taglib/MethodDefinedTagLibSpec.groovy      | 128 ++++++++++++++++++
 ...ethodVsClosureTagInvocationBenchmarkSpec.groovy |  80 +++++++++++
 .../functionaltests/MiscController.groovy          |   4 +
 .../functionaltests/MethodTagLib.groovy}           |  39 +++---
 .../functionaltests/SharedNsClosureTagLib.groovy}  |  15 +--
 .../functionaltests/SharedNsMethodTagLib.groovy}   |  15 +--
 .../app1/grails-app/views/misc/tagMethods.gsp      |   7 +
 .../functionaltests/MiscFunctionalSpec.groovy      |  14 ++
 .../grails-app/taglib/demo/FirstTagLib.groovy      |   2 +-
 .../grails-app/taglib/demo/SampleTagLib.groovy     |   8 +-
 .../grails-app/taglib/demo/SecondTagLib.groovy     |   2 +-
 .../plugins/loadafter/build.gradle                 |   2 +-
 36 files changed, 720 insertions(+), 166 deletions(-)

diff --git 
a/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc 
b/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc
index f4d7d23ee2..595fdeb532 100644
--- a/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc
+++ b/grails-doc/src/en/guide/testing/unitTesting/unitTestingTagLibraries.adoc
@@ -39,7 +39,7 @@ Adding the `TagLibUnitTest` trait to a test causes a new 
`tagLib` field to be
 automatically created for the TagLib class under test.  The `tagLib` property 
can
 be used to test calling tags as function calls. The return value of a function
 call is either a `org.grails.buffer,StreamCharBuffer`
-instance or the object returned from the tag closure when
+instance or the object returned from the tag handler when
 `returnObjectForTags` feature is used.
 
 To test a tag which accepts parameters, specify the parameter values as named
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
index e6bfc66ed1..54b5a5d71a 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
@@ -28,23 +28,26 @@ class SimpleTagLib {
 }
 ----
 
-Now to create a tag create a Closure property that takes two arguments: the 
tag attributes and the body content:
+Now create tags using methods. You can access tag attributes through the 
implicit `attrs` map and body through the implicit `body` closure:
 
 [source,groovy]
 ----
 class SimpleTagLib {
-    def simple = { attrs, body ->
+    def simple() {
+        // ...
 
     }
 }
 ----
 
-The `attrs` argument is a Map of the attributes of the tag, whilst the `body` 
argument is a Closure that returns the body content when invoked:
+Closure field-style tags are still supported for backward compatibility, but 
method-based tags are the recommended syntax.
+
+The implicit `attrs` property is a `Map` of the tag attributes, while `body()` 
returns the tag body content when invoked:
 
 [source,groovy]
 ----
 class SimpleTagLib {
-    def emoticon = { attrs, body ->
+    def emoticon() {
        out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
     }
 }
@@ -57,7 +60,7 @@ As demonstrated above there is an implicit `out` variable 
that refers to the out
 <g:emoticon happy="true">Hi John</g:emoticon>
 ----
 
-NOTE: To help IDEs like Spring Tool Suite (STS) and others autocomplete tag 
attributes, you should add Javadoc comments to your tag closures with `@attr` 
descriptions. Since taglibs use Groovy code it can be difficult to reliably 
detect all usable attributes.
+NOTE: To help IDEs like Spring Tool Suite (STS) and others autocomplete tag 
attributes, add Javadoc comments with `@attr` descriptions to your tag methods. 
Since taglibs use Groovy code it can be difficult to reliably detect all usable 
attributes.
 
 For example:
 
@@ -71,7 +74,7 @@ class SimpleTagLib {
      * @attr happy whether to show a happy emoticon ('true') or
      * a sad emoticon ('false')
      */
-    def emoticon = { attrs, body ->
+    def emoticon() {
        out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
     }
 }
@@ -89,7 +92,7 @@ class SimpleTagLib {
      * @attr name REQUIRED the field name
      * @attr value the field value
      */
-    def passwordField = { attrs ->
+    def passwordField() {
         attrs.type = "password"
         attrs.tagName = "passwordField"
         fieldImpl(out, attrs)
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc
index 26497339d7..4587159932 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/iterativeTags.adoc
@@ -21,7 +21,7 @@ Iterative tags are easy too, since you can invoke the body 
multiple times:
 
 [source,groovy]
 ----
-def repeat = { attrs, body ->
+def repeat() {
     attrs.times?.toInteger()?.times { num ->
         out << body(num)
     }
@@ -48,7 +48,7 @@ That value is then passed as the default variable `it` to the 
tag. However, if y
 
 [source,groovy]
 ----
-def repeat = { attrs, body ->
+def repeat() {
     def var = attrs.var ?: "num"
     attrs.times?.toInteger()?.times { num ->
         out << body((var):num)
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc
index b6e7b442fe..fce648d10b 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/logicalTags.adoc
@@ -21,7 +21,7 @@ You can also create logical tags where the body of the tag is 
only output once a
 
 [source,groovy]
 ----
-def isAdmin = { attrs, body ->
+def isAdmin() {
     def user = attrs.user
     if (user && checkUserPrivs(user)) {
         out << body()
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc
index d87d01223e..91d25b8335 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/namespaces.adoc
@@ -23,7 +23,7 @@ By default, tags are added to the default Grails namespace 
and are used with the
 ----
 class SimpleTagLib {
     static namespace = "my"
-
+    def example() {
     def example = { attrs ->
         //...
     }
diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
index f564df62eb..330422a336 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
@@ -21,7 +21,7 @@ As demonstrated in the previous example it is easy to write 
simple tags that hav
 
 [source,groovy]
 ----
-def dateFormat = { attrs, body ->
+def dateFormat() {
     out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
 }
 ----
@@ -37,7 +37,7 @@ With simple tags sometimes you need to write HTML mark-up to 
the response. One a
 
 [source,groovy]
 ----
-def formatBook = { attrs, body ->
+def formatBook() {
     out << "<div id=\"${attrs.book.id}\">"
     out << "Title : ${attrs.book.title}"
     out << "</div>"
@@ -48,7 +48,7 @@ Although this approach may be tempting it is not very clean. 
A better approach w
 
 [source,groovy]
 ----
-def formatBook = { attrs, body ->
+def formatBook() {
     out << render(template: "bookTemplate", model: [book: attrs.book])
 }
 ----
diff --git 
a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc
index 65b8a43492..6027e9653e 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs/tagReturnValue.adoc
@@ -19,13 +19,13 @@ under the License.
 
 A taglib can be used in a GSP as an ordinary tag, or it might be used as a 
function in other taglibs or GSP expressions.
 
-Internally Grails intercepts calls to taglib closures.
+Internally Grails intercepts calls to tag handlers (method-based or 
closure-based).
 The "out" that is available in a taglib is mapped to a `java.io.Writer` 
implementation that writes to a buffer
 that "captures" the output of the taglib call. This buffer is the return value 
of a tag library call when it's 
 used as a function.
 
 If the tag is listed in the library's static `returnObjectForTags` array, then 
its return value will be written to 
-the output when it's used as a normal tag. The return value of the tag lib 
closure will be returned as-is 
+the output when it's used as a normal tag. The return value of the tag 
method/closure will be returned as-is 
 if it's used as a function in GSP expressions or other taglibs. 
 
 If the tag is not included in the returnObjectForTags array, then its return 
value will be discarded.
@@ -37,7 +37,7 @@ Example:
 class ObjectReturningTagLib {
        static namespace = "cms"
        static returnObjectForTags = ['content'] 
-
+       def content() {
        def content = { attrs, body ->
                CmsContent.findByCode(attrs.code)?.content      
     }
diff --git a/grails-doc/src/en/guide/theWebLayer/taglibs.adoc 
b/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
index e706606835..692fde9aca 100644
--- a/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
@@ -28,23 +28,26 @@ class SimpleTagLib {
 }
 ----
 
-Now to create a tag create a Closure property that takes two arguments: the 
tag attributes and the body content:
+Now create tags using methods. You can access tag attributes through the 
implicit `attrs` map and body through the implicit `body` closure:
 
 [source,groovy]
 ----
 class SimpleTagLib {
-    def simple = { attrs, body ->
+    def simple() {
+        // ...
 
     }
 }
 ----
 
-The `attrs` argument is a Map of the attributes of the tag, whilst the `body` 
argument is a Closure that returns the body content when invoked:
+Closure field-style tags are still supported for backward compatibility, but 
method-based tags are the recommended syntax.
+
+The implicit `attrs` property is a `Map` of the tag attributes, while `body()` 
returns the tag body content when invoked:
 
 [source,groovy]
 ----
 class SimpleTagLib {
-    def emoticon = { attrs, body ->
+    def emoticon() {
        out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
     }
 }
@@ -57,7 +60,7 @@ As demonstrated above there is an implicit `out` variable 
that refers to the out
 <g:emoticon happy="true">Hi John</g:emoticon>
 ----
 
-NOTE: To help IDEs autocomplete tag attributes, you should add Javadoc 
comments to your tag closures with `@attr` descriptions. Since taglibs use 
Groovy code it can be difficult to reliably detect all usable attributes.
+NOTE: To help IDEs autocomplete tag attributes, add Javadoc comments with 
`@attr` descriptions to your tag methods. Since taglibs use Groovy code it can 
be difficult to reliably detect all usable attributes.
 
 For example:
 
@@ -71,7 +74,7 @@ class SimpleTagLib {
      * @attr happy whether to show a happy emoticon ('true') or
      * a sad emoticon ('false')
      */
-    def emoticon = { attrs, body ->
+    def emoticon() {
        out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
     }
 }
@@ -89,7 +92,7 @@ class SimpleTagLib {
      * @attr name REQUIRED the field name
      * @attr value the field value
      */
-    def passwordField = { attrs ->
+    def passwordField() {
         attrs.type = "password"
         attrs.tagName = "passwordField"
         fieldImpl(out, attrs)
diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java 
b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java
index 095f5a098c..0abec8524c 100644
--- a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java
+++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java
@@ -47,6 +47,8 @@ import org.grails.gsp.jsp.TagLibraryResolver;
 import org.grails.taglib.AbstractTemplateVariableBinding;
 import org.grails.taglib.GrailsTagException;
 import org.grails.taglib.GroovyPageAttributes;
+import org.grails.taglib.TagMethodContext;
+import org.grails.taglib.TagMethodInvoker;
 import org.grails.taglib.TagBodyClosure;
 import org.grails.taglib.TagLibraryLookup;
 import org.grails.taglib.TagOutput;
@@ -381,10 +383,13 @@ public abstract class GroovyPage extends Script {
             if (tagLib != null || (gspTagLibraryLookup != null && 
gspTagLibraryLookup.hasNamespace(tagNamespace))) {
                 if (tagLib != null) {
                     boolean returnsObject = 
gspTagLibraryLookup.doesTagReturnObject(tagNamespace, tagName);
-                    Object tagLibClosure = tagLib.getProperty(tagName);
+                    Object tagLibClosure = 
TagMethodInvoker.getClosureTagProperty(tagLib, tagName);
                     if (tagLibClosure instanceof Closure) {
                         Map<String, Object> encodeAsForTag = 
gspTagLibraryLookup.getEncodeAsForTag(tagNamespace, tagName);
                         invokeTagLibClosure(tagName, tagNamespace, (Closure) 
tagLibClosure, attrs, body, returnsObject, encodeAsForTag);
+                    } else if (TagMethodInvoker.hasInvokableTagMethod(tagLib, 
tagName)) {
+                        Map<String, Object> encodeAsForTag = 
gspTagLibraryLookup.getEncodeAsForTag(tagNamespace, tagName);
+                        invokeTagLibMethod(tagName, tagNamespace, tagLib, 
attrs, body, returnsObject, encodeAsForTag);
                     } else {
                         throw new GrailsTagException("Tag [" + tagName + "] 
does not exist in tag library [" + tagLib.getClass().getName() + "]", 
getGroovyPageFileName(), lineNumber);
                     }
@@ -475,6 +480,28 @@ public abstract class GroovyPage extends Script {
         }
     }
 
+    private void invokeTagLibMethod(String tagName, String tagNamespace, 
GroovyObject tagLib, Map<?, ?> attrs, Closure<?> body,
+            boolean returnsObject, Map<String, Object> defaultEncodeAs) {
+        if (!(attrs instanceof GroovyPageAttributes)) {
+            attrs = new GroovyPageAttributes(attrs);
+        }
+        ((GroovyPageAttributes) attrs).setGspTagSyntaxCall(true);
+        boolean encodeAsPushedToStack = false;
+        try {
+            Map<String, Object> codecSettings = 
TagOutput.createCodecSettings(tagNamespace, tagName, attrs, defaultEncodeAs);
+            if (codecSettings != null) {
+                
outputStack.push(WithCodecHelper.createOutputStackAttributesBuilder(codecSettings,
 outputContext.getGrailsApplication()).build());
+                encodeAsPushedToStack = true;
+            }
+            Closure<?> actualBody = body != null ? body : 
TagOutput.EMPTY_BODY_CLOSURE;
+            TagMethodContext.push(attrs, actualBody);
+            Object tagResult = TagMethodInvoker.invokeTagMethod(tagLib, 
tagName, attrs, actualBody);
+            outputTagResult(returnsObject, tagResult);
+        } finally {
+            TagMethodContext.pop();
+            if (encodeAsPushedToStack) outputStack.pop();
+        }
+    }
     private void outputTagResult(boolean returnsObject, Object tagresult) {
         if (returnsObject && tagresult != null && !(tagresult instanceof 
Writer)) {
             if (tagresult instanceof String && isHtmlPart((String) tagresult)) 
{
diff --git 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
index 9719da62d9..68a42f2c5c 100644
--- 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
+++ 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java
@@ -33,6 +33,7 @@ import groovy.lang.MetaProperty;
 import grails.core.gsp.GrailsTagLibClass;
 import org.grails.core.AbstractInjectableGrailsClass;
 import org.grails.core.artefact.gsp.TagLibArtefactHandler;
+import org.grails.taglib.TagMethodInvoker;
 
 /**
  * Default implementation of a tag lib class.
@@ -69,6 +70,7 @@ public class DefaultGrailsTagLibClass extends 
AbstractInjectableGrailsClass impl
                 tags.add(prop.getName());
             }
         }
+        tags.addAll(TagMethodInvoker.getInvokableTagMethodNames(clazz));
 
         String ns = getStaticPropertyValue(NAMESPACE_FIELD_NAME, String.class);
         if (ns != null && !"".equals(ns.trim())) {
diff --git 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
index 5937aac2e7..bad77a6070 100644
--- 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
+++ 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
@@ -29,6 +29,7 @@ import org.springframework.context.ApplicationContext
 import grails.core.gsp.GrailsTagLibClass
 import grails.util.GrailsClassUtils
 import org.grails.taglib.encoder.OutputContextLookupHelper
+import org.grails.taglib.encoder.OutputEncodingStack
 
 class TagLibraryMetaUtils {
 
@@ -44,10 +45,26 @@ class TagLibraryMetaUtils {
 
     @CompileStatic
     static void enhanceTagLibMetaClass(MetaClass mc, TagLibraryLookup 
gspTagLibraryLookup, String namespace) {
+        registerTagMethodContextMetaProperties(mc)
         registerTagMetaMethods(mc, gspTagLibraryLookup, namespace)
         registerNamespaceMetaProperties(mc, gspTagLibraryLookup)
     }
 
+    @CompileStatic
+    static void registerTagMethodContextMetaProperties(MetaClass metaClass) {
+        GroovyObject mc = (GroovyObject) metaClass
+        if (!metaClass.hasProperty("attrs") && !doesMethodExist(metaClass, 
"getAttrs", [] as Class[])) {
+            mc.setProperty("getAttrs") { ->
+                TagMethodContext.currentAttrs()
+            }
+        }
+        if (!metaClass.hasProperty("body") && !doesMethodExist(metaClass, 
"getBody", [] as Class[])) {
+            mc.setProperty("getBody") { ->
+                TagMethodContext.currentBody()
+            }
+        }
+    }
+
     @CompileStatic
     static void registerNamespaceMetaProperties(MetaClass mc, TagLibraryLookup 
gspTagLibraryLookup) {
         for (String ns : gspTagLibraryLookup.getAvailableNamespaces()) {
@@ -57,9 +74,7 @@ class TagLibraryMetaUtils {
 
     @CompileStatic
     static void registerNamespaceMetaProperty(MetaClass metaClass, 
TagLibraryLookup gspTagLibraryLookup, String namespace) {
-        if (!metaClass.hasProperty(namespace) && !doesMethodExist(metaClass, 
GrailsClassUtils.getGetterName(namespace), [] as Class[])) {
-            registerPropertyMissingForTag(metaClass, namespace, 
gspTagLibraryLookup.lookupNamespaceDispatcher(namespace))
-        }
+        registerPropertyMissingForTag(metaClass, namespace, 
gspTagLibraryLookup.lookupNamespaceDispatcher(namespace))
     }
 
     @CompileStatic
@@ -68,33 +83,45 @@ class TagLibraryMetaUtils {
 
         if (overrideMethods || !doesMethodExist(metaClass, name, [Map, 
Closure] as Class[])) {
             mc.setProperty(name) { Map attrs, Closure body ->
-                TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, 
name, attrs, body, OutputContextLookupHelper.lookupOutputContext())
+                captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, 
name, attrs, body)
             }
         }
         if (overrideMethods || !doesMethodExist(metaClass, name, [Map, 
CharSequence] as Class[])) {
             mc.setProperty(name) { Map attrs, CharSequence body ->
-                TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, 
name, attrs, new TagOutput.ConstantClosure(body), 
OutputContextLookupHelper.lookupOutputContext())
+                captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, 
name, attrs, new TagOutput.ConstantClosure(body))
             }
         }
         if (overrideMethods || !doesMethodExist(metaClass, name, [Map] as 
Class[])) {
             mc.setProperty(name) { Map attrs ->
-                TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, 
name, attrs, null, OutputContextLookupHelper.lookupOutputContext())
+                captureTagOutputForMethodCall(gspTagLibraryLookup, namespace, 
name, attrs, null)
             }
         }
         if (addAll) {
             if (overrideMethods || !doesMethodExist(metaClass, name, [Closure] 
as Class[])) {
                 mc.setProperty(name) { Closure body ->
-                    TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, 
name, [:], body, OutputContextLookupHelper.lookupOutputContext())
+                    captureTagOutputForMethodCall(gspTagLibraryLookup, 
namespace, name, [:], body)
                 }
             }
             if (overrideMethods || !doesMethodExist(metaClass, name, [] as 
Class[])) {
                 mc.setProperty(name) { ->
-                    TagOutput.captureTagOutput(gspTagLibraryLookup, namespace, 
name, [:], null, OutputContextLookupHelper.lookupOutputContext())
+                    captureTagOutputForMethodCall(gspTagLibraryLookup, 
namespace, name, [:], null)
                 }
             }
         }
     }
 
+    @CompileStatic
+    private static Object captureTagOutputForMethodCall(TagLibraryLookup 
gspTagLibraryLookup, String namespace, String name, Map attrs, Object body) {
+        Object output = TagOutput.captureTagOutput(gspTagLibraryLookup, 
namespace, name, attrs, body, OutputContextLookupHelper.lookupOutputContext())
+        boolean returnsObject = 
gspTagLibraryLookup.doesTagReturnObject(namespace, name)
+        boolean gspTagSyntaxCall = attrs instanceof GroovyPageAttributes && 
((GroovyPageAttributes) attrs).isGspTagSyntaxCall()
+        if (gspTagSyntaxCall && !returnsObject && output != null) {
+            OutputEncodingStack.currentStack().taglibWriter.print(output)
+            return null
+        }
+        return output
+    }
+
     static registerMethodMissingForTags(MetaClass mc, ApplicationContext ctx,
                                         GrailsTagLibClass tagLibraryClass, 
String name) {
         TagLibraryLookup gspTagLibraryLookup = 
ctx.getBean('gspTagLibraryLookup')
@@ -109,13 +136,13 @@ class TagLibraryMetaUtils {
     }
 
     @CompileStatic
-    static void registerTagMetaMethods(MetaClass emc, TagLibraryLookup lookup, 
String namespace) {
+    static void registerTagMetaMethods(MetaClass emc, TagLibraryLookup lookup, 
String namespace, boolean overrideMethods = true) {
         for (String tagName : lookup.getAvailableTags(namespace)) {
             boolean addAll = !(namespace == TagOutput.DEFAULT_NAMESPACE && 
tagName == 'hasErrors')
-            registerMethodMissingForTags(emc, lookup, namespace, tagName, 
addAll, false)
+            registerMethodMissingForTags(emc, lookup, namespace, tagName, 
addAll, overrideMethods)
         }
         if (namespace != TagOutput.DEFAULT_NAMESPACE) {
-            registerTagMetaMethods(emc, lookup, TagOutput.DEFAULT_NAMESPACE)
+            registerTagMetaMethods(emc, lookup, TagOutput.DEFAULT_NAMESPACE, 
false)
         }
     }
 
diff --git 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodContext.java
 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodContext.java
new file mode 100644
index 0000000000..9b17cafc49
--- /dev/null
+++ 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodContext.java
@@ -0,0 +1,58 @@
+/*
+ *  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 org.grails.taglib;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+
+import groovy.lang.Closure;
+
+public final class TagMethodContext {
+
+    private static final ThreadLocal<Deque<TagMethodContextEntry>> 
CONTEXT_STACK = ThreadLocal.withInitial(ArrayDeque::new);
+    private TagMethodContext() {
+    }
+
+    public static void push(Map<?, ?> attrs, Closure<?> body) {
+        CONTEXT_STACK.get().push(new TagMethodContextEntry(attrs, body));
+    }
+
+    public static void pop() {
+        Deque<TagMethodContextEntry> stack = CONTEXT_STACK.get();
+        if (!stack.isEmpty()) {
+            stack.pop();
+        }
+        if (stack.isEmpty()) {
+            CONTEXT_STACK.remove();
+        }
+    }
+
+    public static Map<?, ?> currentAttrs() {
+        Deque<TagMethodContextEntry> stack = CONTEXT_STACK.get();
+        return stack.isEmpty() ? null : stack.peek().attrs();
+    }
+
+    public static Closure<?> currentBody() {
+        Deque<TagMethodContextEntry> stack = CONTEXT_STACK.get();
+        return stack.isEmpty() ? null : stack.peek().body();
+    }
+
+    private record TagMethodContextEntry(Map<?, ?> attrs, Closure<?> body) { }
+}
diff --git 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java
 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java
new file mode 100644
index 0000000000..5669a77512
--- /dev/null
+++ 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java
@@ -0,0 +1,150 @@
+/*
+ *  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 org.grails.taglib;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import groovy.lang.Closure;
+import groovy.lang.GroovyObject;
+import groovy.lang.MissingMethodException;
+
+public final class TagMethodInvoker {
+    private TagMethodInvoker() {
+    }
+
+    public static Object getClosureTagProperty(GroovyObject tagLib, String 
tagName) {
+        Class<?> type = tagLib.getClass();
+        while (type != null && type != Object.class) {
+            try {
+                Field field = type.getDeclaredField(tagName);
+                if (!Modifier.isStatic(field.getModifiers()) && 
Closure.class.isAssignableFrom(field.getType())) {
+                    field.setAccessible(true);
+                    return field.get(tagLib);
+                }
+                return null;
+            } catch (NoSuchFieldException ignored) {
+                type = type.getSuperclass();
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return null;
+    }
+
+    public static Collection<String> getInvokableTagMethodNames(Class<?> 
tagLibClass) {
+        if (tagLibClass == null) {
+            return Collections.emptyList();
+        }
+        List<String> names = new ArrayList<>();
+        for (Method method : tagLibClass.getDeclaredMethods()) {
+            if (isTagMethodCandidate(method)) {
+                names.add(method.getName());
+            }
+        }
+        return names;
+    }
+
+    public static boolean hasInvokableTagMethod(GroovyObject tagLib, String 
tagName) {
+        for (Method method : tagLib.getClass().getMethods()) {
+            if (isTagMethodCandidate(method) && 
method.getName().equals(tagName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static Object invokeTagMethod(GroovyObject tagLib, String tagName, 
Map<?, ?> attrs, Closure<?> body) {
+        for (Method method : tagLib.getClass().getMethods()) {
+            if (!isTagMethodCandidate(method) || 
!method.getName().equals(tagName)) {
+                continue;
+            }
+            Object[] args = toMethodArguments(method, attrs, body);
+            if (args != null) {
+                try {
+                    return method.invoke(tagLib, args);
+                } catch (IllegalAccessException e) {
+                    throw new RuntimeException(e);
+                } catch (InvocationTargetException e) {
+                    Throwable targetException = e.getTargetException();
+                    if (targetException instanceof RuntimeException 
runtimeException) {
+                        throw runtimeException;
+                    }
+                    throw new RuntimeException(targetException);
+                }
+            }
+        }
+        throw new MissingMethodException(tagName, tagLib.getClass(), new 
Object[] { attrs, body });
+    }
+
+    public static boolean isTagMethodCandidate(Method method) {
+        int modifiers = method.getModifiers();
+        if (!Modifier.isPublic(modifiers) || Modifier.isStatic(modifiers) || 
method.isBridge() || method.isSynthetic()) {
+            return false;
+        }
+        String name = method.getName();
+        if ("afterPropertiesSet".equals(name)) {
+            return false;
+        }
+        if (name.startsWith("get") && method.getParameterCount() == 0) {
+            return false;
+        }
+        if (name.startsWith("set") && method.getParameterCount() == 1) {
+            return false;
+        }
+        if ("invokeMethod".equals(name) || "methodMissing".equals(name) || 
"propertyMissing".equals(name)) {
+            return false;
+        }
+        return method.getDeclaringClass() != Object.class && 
method.getDeclaringClass() != GroovyObject.class;
+    }
+
+    private static Object[] toMethodArguments(Method method, Map<?, ?> attrs, 
Closure<?> body) {
+        Parameter[] parameters = method.getParameters();
+        Object[] args = new Object[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            Class<?> parameterType = parameters[i].getType();
+            if (Map.class.isAssignableFrom(parameterType)) {
+                args[i] = attrs;
+                continue;
+            }
+            if (Closure.class.isAssignableFrom(parameterType)) {
+                args[i] = body != null ? body : TagOutput.EMPTY_BODY_CLOSURE;
+                continue;
+            }
+            String parameterName = parameters[i].getName();
+            Object value = attrs != null ? attrs.get(parameterName) : null;
+            if (value == null && parameters.length == 1 && attrs != null && 
attrs.size() == 1) {
+                value = attrs.values().iterator().next();
+            }
+            if (value == null) {
+                return null;
+            }
+            args[i] = value;
+        }
+        return args;
+    }
+}
diff --git 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagOutput.java 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagOutput.java
index bad1f1c5cd..d44f394690 100644
--- a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagOutput.java
+++ b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagOutput.java
@@ -54,10 +54,11 @@ public class TagOutput {
             throw new GrailsTagException("Tag [" + tagName + "] does not 
exist. No corresponding tag library found.");
         }
 
+        boolean gspTagSyntaxCall = attrs instanceof GroovyPageAttributes && 
((GroovyPageAttributes) attrs).isGspTagSyntaxCall();
         if (!(attrs instanceof GroovyPageAttributes)) {
             attrs = new GroovyPageAttributes(attrs, false);
         }
-        ((GroovyPageAttributes) attrs).setGspTagSyntaxCall(false);
+        ((GroovyPageAttributes) attrs).setGspTagSyntaxCall(gspTagSyntaxCall);
         Closure actualBody = createOutputCapturingClosure(tagLib, body, 
outputContext);
 
         final GroovyPageTagWriter tagOutput = new GroovyPageTagWriter();
@@ -74,7 +75,7 @@ public class TagOutput {
             builder.topWriter(tagOutput);
             outputStack.push(builder.build());
 
-            Object tagLibProp = tagLib.getProperty(tagName); // retrieve tag 
lib and create wrapper writer
+            Object tagLibProp = TagMethodInvoker.getClosureTagProperty(tagLib, 
tagName); // retrieve tag closure field
             if (tagLibProp instanceof Closure) {
                 Closure tag = (Closure) ((Closure) tagLibProp).clone();
                 Object bodyResult;
@@ -122,6 +123,26 @@ public class TagOutput {
                     return tagOutput.getBuffer();
                 }
             }
+            if (TagMethodInvoker.hasInvokableTagMethod(tagLib, tagName)) {
+                try {
+                    TagMethodContext.push(attrs, actualBody);
+                    Object bodyResult = 
TagMethodInvoker.invokeTagMethod(tagLib, tagName, attrs, actualBody);
+                    Encoder taglibEncoder = outputStack.getTaglibEncoder();
+                    boolean returnsObject = 
gspTagLibraryLookup.doesTagReturnObject(namespace, tagName);
+                    if (returnsObject && bodyResult != null && !(bodyResult 
instanceof Writer)) {
+                        if (taglibEncoder != null) {
+                            bodyResult = taglibEncoder.encode(bodyResult);
+                        }
+                        return bodyResult;
+                    }
+                    if (taglibEncoder != null) {
+                        return taglibEncoder.encode(tagOutput.getBuffer());
+                    }
+                    return tagOutput.getBuffer();
+                } finally {
+                    TagMethodContext.pop();
+                }
+            }
 
             throw new GrailsTagException("Tag [" + tagName + "] does not exist 
in tag library [" +
                     tagLib.getClass().getName() + "]");
diff --git 
a/grails-gsp/grails-web-taglib/src/main/groovy/grails/artefact/TagLibrary.groovy
 
b/grails-gsp/grails-web-taglib/src/main/groovy/grails/artefact/TagLibrary.groovy
index 0a1c680b66..74a02b72c3 100644
--- 
a/grails-gsp/grails-web-taglib/src/main/groovy/grails/artefact/TagLibrary.groovy
+++ 
b/grails-gsp/grails-web-taglib/src/main/groovy/grails/artefact/TagLibrary.groovy
@@ -34,10 +34,14 @@ import grails.web.api.WebAttributes
 import org.grails.buffer.GrailsPrintWriter
 import org.grails.encoder.Encoder
 import org.grails.taglib.GrailsTagException
+import org.grails.taglib.GroovyPageAttributes
 import org.grails.taglib.TagLibraryLookup
 import org.grails.taglib.TagLibraryMetaUtils
+import org.grails.taglib.TagMethodContext
+import org.grails.taglib.TagMethodInvoker
 import org.grails.taglib.TagOutput
 import org.grails.taglib.TemplateVariableBinding
+import org.grails.taglib.encoder.OutputContextLookupHelper
 import org.grails.taglib.encoder.OutputEncodingStack
 import org.grails.taglib.encoder.WithCodecHelper
 import org.grails.web.servlet.mvc.GrailsWebRequest
@@ -131,8 +135,21 @@ trait TagLibrary implements WebAttributes, 
ServletAttributes, TagLibraryInvoker
      * @throws MissingPropertyException When no tag namespace or tag is found
      */
     Object propertyMissing(String name) {
+        if (name == 'attrs') {
+            def contextAttrs = TagMethodContext.currentAttrs()
+            if (contextAttrs != null) {
+                return contextAttrs
+            }
+        }
+        if (name == 'body') {
+            def contextBody = TagMethodContext.currentBody()
+            if (contextBody != null) {
+                return contextBody
+            }
+        }
         TagLibraryLookup gspTagLibraryLookup = getTagLibraryLookup()
         if (gspTagLibraryLookup != null) {
+            boolean methodTagFallback = false
 
             Object result = gspTagLibraryLookup.lookupNamespaceDispatcher(name)
             if (result == null) {
@@ -143,14 +160,26 @@ trait TagLibrary implements WebAttributes, 
ServletAttributes, TagLibraryInvoker
                 }
 
                 if (tagLibrary != null) {
-                    Object tagProperty = tagLibrary.getProperty(name)
+                    Object tagProperty = 
TagMethodInvoker.getClosureTagProperty(tagLibrary, name)
                     if (tagProperty instanceof Closure) {
                         result = ((Closure<?>) tagProperty).clone()
+                    } else if 
(TagMethodInvoker.hasInvokableTagMethod(tagLibrary, name)) {
+                        methodTagFallback = true
+                        final String currentNamespace = namespace
+                        result = { Map attrs = [:], Closure body = null ->
+                            Object output = 
TagOutput.captureTagOutput(gspTagLibraryLookup, currentNamespace, name, attrs, 
body, OutputContextLookupHelper.lookupOutputContext())
+                            boolean gspTagSyntaxCall = attrs instanceof 
GroovyPageAttributes && ((GroovyPageAttributes) attrs).isGspTagSyntaxCall()
+                            boolean returnsObject = 
gspTagLibraryLookup.doesTagReturnObject(currentNamespace, name)
+                            if (gspTagSyntaxCall && !returnsObject && output 
!= null) {
+                                
OutputEncodingStack.currentStack().taglibWriter.print(output)
+                                return null
+                            }
+                            output
+                        }
                     }
                 }
             }
-
-            if (result != null && !Environment.isDevelopmentMode()) {
+            if (result != null && !Environment.isDevelopmentMode() && 
!methodTagFallback) {
                 MetaClass mc = 
GrailsMetaClassUtils.getExpandoMetaClass(getClass())
 
                 // Register the property for the already-existing singleton 
instance of the taglib
diff --git 
a/grails-gsp/grails-web-taglib/src/main/groovy/grails/gsp/taglib/compiler/TagLibArtefactTypeAstTransformation.java
 
b/grails-gsp/grails-web-taglib/src/main/groovy/grails/gsp/taglib/compiler/TagLibArtefactTypeAstTransformation.java
index 0983d2aed3..0df4fda9be 100644
--- 
a/grails-gsp/grails-web-taglib/src/main/groovy/grails/gsp/taglib/compiler/TagLibArtefactTypeAstTransformation.java
+++ 
b/grails-gsp/grails-web-taglib/src/main/groovy/grails/gsp/taglib/compiler/TagLibArtefactTypeAstTransformation.java
@@ -18,9 +18,11 @@
  */
 
 package grails.gsp.taglib.compiler;
+import groovy.lang.Closure;
 
 import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
 import org.codehaus.groovy.control.CompilePhase;
 import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.transform.GroovyASTTransformation;
@@ -31,9 +33,11 @@ import 
org.grails.compiler.injection.ArtefactTypeAstTransformation;
 @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
 public class TagLibArtefactTypeAstTransformation extends 
ArtefactTypeAstTransformation {
     private static final ClassNode MY_TYPE = new ClassNode(TagLib.class);
+    private static final ClassNode CLOSURE_TYPE = new ClassNode(Closure.class);
 
     @Override
     protected String resolveArtefactType(SourceUnit sourceUnit, AnnotationNode 
annotationNode, ClassNode classNode) {
+        addClosureTagDeprecationWarnings(sourceUnit, classNode);
         return "TagLibrary";
     }
 
@@ -41,4 +45,20 @@ public class TagLibArtefactTypeAstTransformation extends 
ArtefactTypeAstTransfor
     protected ClassNode getAnnotationType() {
         return MY_TYPE;
     }
+
+    protected void addClosureTagDeprecationWarnings(SourceUnit sourceUnit, 
ClassNode classNode) {
+        if (classNode.getPackageName() != null && 
classNode.getPackageName().startsWith("org.grails.plugins.web.taglib")) {
+            return;
+        }
+        for (FieldNode field : classNode.getFields()) {
+            if (field.isStatic()) {
+                continue;
+            }
+            if (field.getType() != null && 
CLOSURE_TYPE.equals(field.getType())) {
+                String message = "Closure-based tag definition [" + 
field.getName() + "] in TagLib [" + classNode.getName() + "] is deprecated. " +
+                        "Define tag handlers as methods instead.";
+                
org.grails.compiler.injection.GrailsASTUtils.warning(sourceUnit, field, 
message);
+            }
+        }
+    }
 }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy
index 96844c286e..fa00d0ec6b 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy
@@ -91,7 +91,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      *
      * @attr name REQUIRED the cookie name
      */
-    Closure cookie = { attrs ->
+    def cookie(Map attrs) {
         request.cookies.find { it.name == attrs.name }?.value
     }
 
@@ -102,7 +102,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      *
      * @attr name REQUIRED the header name
      */
-    Closure header = { attrs ->
+    def header(Map attrs) {
         attrs.name ? request.getHeader(attrs.name) : null
     }
 
@@ -115,7 +115,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr bean the name or the type of a bean in the applicationContext; 
the type can be an interface or superclass
      * @attr scope the scope name; defaults to pageScope
      */
-    Closure set = { attrs, body ->
+    def set(Map attrs, Closure body) {
         def var = attrs.var
         if (!var) throw new IllegalArgumentException('[var] attribute must be 
specified to for <g:set>!')
 
@@ -142,7 +142,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      *
      * @emptyTag
      */
-    Closure createLinkTo = { attrs ->
+    def createLinkTo(Map attrs) {
         GrailsUtil.deprecated('Tag [createLinkTo] is deprecated please use 
[resource] instead')
         return resource(attrs)
     }
@@ -161,7 +161,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr absolute If set to "true" will prefix the link target address 
with the value of the grails.serverURL property from Config, or 
http://localhost:&lt;port&gt; if no value in Config and not running in 
production.
      * @attr plugin The plugin to look for the resource in
      */
-    Closure resource = { attrs ->
+    def resource(Map attrs) {
         if (!attrs.pluginContextPath && pageScope.pluginContextPath) {
             attrs.pluginContextPath = pageScope.pluginContextPath
         }
@@ -179,7 +179,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr plugin Optional the name of the grails plugin if the resource is 
not part of the application
      * @attr uri Optional app-relative URI path of the resource if not using 
dir/file attributes - only if Resources plugin is in use
      */
-    Closure img = { attrs ->
+    def img(Map attrs) {
         if (!attrs.uri && !attrs.dir) {
             attrs.dir = 'images'
         }
@@ -308,7 +308,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr plugin
      * @attr type
      */
-    Closure external = { attrs ->
+    def external(Map attrs) {
         if (!attrs.uri) {
             attrs.uri = resource(attrs).toString()
         }
@@ -363,7 +363,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr mapping The named URL mapping to use to rewrite the link
      * @attr event Webflow _eventId parameter
      */
-    Closure createLink = { attrs ->
+    def createLink(Map attrs) {
         return doCreateLink(attrs instanceof  Map ? (Map) attrs : 
Collections.emptyMap())
     }
 
@@ -403,7 +403,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr name REQUIRED the tag name
      * @attr attrs tag attributes
      */
-    Closure withTag = { attrs, body ->
+    def withTag(Map attrs, Closure body) {
         def writer = out
         writer << "<${attrs.name}"
         attrs.attrs?.each { k, v ->
@@ -431,7 +431,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      * @attr REQUIRED in The collection to iterate over
      * @attr delimiter The value of the delimiter to use during the join. If 
no delimiter is specified then ", " (a comma followed by a space) will be used 
as the delimiter.
      */
-    Closure join = { attrs ->
+    def join(Map attrs) {
         def collection = attrs.'in'
         if (collection == null) {
             throwTagError('Tag ["join"] missing required attribute ["in"]')
@@ -448,7 +448,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
      *
      * @attr name REQUIRED the metadata key
      */
-    Closure meta = { attrs ->
+    def meta(Map attrs) {
         if (!attrs.name) {
             throwTagError('Tag ["meta"] missing required attribute ["name"]')
         }
@@ -458,7 +458,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
     /**
      * Filters the url through the RequestDataValueProcessor bean if it is 
registered.
      */
-    String processedUrl(String link, request) {
+    private String processedUrl(String link, request) {
         if (requestDataValueProcessor == null) {
             return link
         }
@@ -466,7 +466,7 @@ class ApplicationTagLib implements ApplicationContextAware, 
InitializingBean, Gr
         return requestDataValueProcessor.processUrl(request, link)
     }
 
-    Closure applyCodec = { Map attrs, Closure body ->
+    def applyCodec(Map attrs, Closure body) {
         // encoding is handled in GroovyPage.invokeTag and 
GroovyPage.captureTagOutput
         body()
     }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/CountryTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/CountryTagLib.groovy
index 793d158372..cc218d8ede 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/CountryTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/CountryTagLib.groovy
@@ -307,7 +307,7 @@ class CountryTagLib implements TagLibrary {
      * @attr noSelection A single-entry map detailing the key and value to use 
for the "no selection made" choice in the select box. If there is no current 
selection this will be shown as it is first in the list, and if submitted with 
this selected, the key that you provide will be submitted. Typically this will 
be blank - but you can also use 'null' in the case that you're passing the ID 
of an object
      * @attr disabled boolean value indicating whether the select is disabled 
or enabled (defaults to false - enabled)
      */
-    Closure countrySelect = { attrs ->
+    def countrySelect(Map attrs) {
         if (!attrs.from) {
             attrs.from = COUNTRY_CODES_BY_NAME_ORDER
         }
@@ -328,7 +328,7 @@ class CountryTagLib implements TagLibrary {
      *
      * @attr code REQUIRED the ISO3166_3 country code
      */
-    Closure country = { attrs ->
+    def country(Map attrs) {
         if (!attrs.code) {
             throwTagError('[country] requires [code] attribute to specify the 
country code')
         }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
index c7acf239e1..c4e084c9bb 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
@@ -84,7 +84,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
         configureCsrf()
     }
 
-    void configureCsrf() {
+    private void configureCsrf() {
         try {
             var filterChainProxy = applicationContext.getBean(
                     
Class.forName('org.springframework.security.web.FilterChainProxy'))
@@ -105,7 +105,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr name REQUIRED the field name
      * @attr value the field value
      */
-    Closure textField = { attrs ->
+    def textField(Map attrs) {
         attrs.type = 'text'
         attrs.tagName = 'textField'
         fieldImpl(out, attrs)
@@ -119,7 +119,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr name REQUIRED the field name
      * @attr value the field value
      */
-    Closure passwordField = { attrs ->
+    def passwordField(Map attrs) {
         attrs.type = 'password'
         attrs.tagName = 'passwordField'
         fieldImpl(out, attrs)
@@ -131,11 +131,11 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr name REQUIRED the field name
      * @attr value the field value
      */
-    Closure hiddenField = { attrs ->
+    def hiddenField(Map attrs) {
         hiddenFieldImpl(out, attrs)
     }
 
-    def hiddenFieldImpl(out, attrs) {
+    private def hiddenFieldImpl(out, attrs) {
         attrs.type = 'hidden'
         attrs.tagName = 'hiddenField'
         fieldImpl(out, attrs)
@@ -151,7 +151,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr type input type; defaults to 'submit'
      * @attr event the webflow event id
      */
-    Closure submitButton = { attrs ->
+    def submitButton(Map attrs) {
         attrs.type = attrs.type ?: 'submit'
         attrs.tagName = 'submitButton'
         if (request.flowExecutionKey) {
@@ -168,13 +168,13 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      *
      * @attr type REQUIRED the input type
      */
-    Closure field = { attrs ->
+    def field(Map attrs) {
         attrs.tagName = 'field'
         fieldImpl(out, attrs)
     }
 
     @CompileStatic
-    def fieldImpl(GrailsPrintWriter out, Map attrs) {
+    private def fieldImpl(GrailsPrintWriter out, Map attrs) {
         resolveAttributes(attrs)
 
         attrs.value = processFormFieldValueIfNecessary(attrs.name, 
attrs.value, attrs.type)
@@ -206,7 +206,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr readonly if evaluates to true, sets to checkbox to read only
      * @attr id DOM element id; defaults to name
      */
-    Closure checkBox = { attrs ->
+    def checkBox(Map attrs) {
         def value = attrs.remove('value')
         def name = attrs.remove('name')
         def formName = attrs.get('form')
@@ -288,7 +288,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr escapeHtml if true escapes the text as HTML
      * @attr id DOM element id; defaults to name
      */
-    Closure textArea = { attrs, body ->
+    def textArea(Map attrs, Closure body) {
         resolveAttributes(attrs)
         // Pull out the value to use as content not attrib
         def value = attrs.remove('value')
@@ -338,7 +338,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
     /**
      * Check required attributes, set the id to name if no id supplied, 
extract bean values etc.
      */
-    void resolveAttributes(Map attrs) {
+    private void resolveAttributes(Map attrs) {
         if (!attrs.name && !attrs.field) {
             throwTagError("Tag [${attrs.tagName}] is missing required 
attribute [name] or [field]")
         }
@@ -366,7 +366,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * Dump out attributes in HTML compliant fashion.
      */
     @CompileStatic
-    void outputAttributes(Map attrs, GrailsPrintWriter writer, boolean 
useNameAsIdIfIdDoesNotExist = false) {
+    private void outputAttributes(Map attrs, GrailsPrintWriter writer, boolean 
useNameAsIdIfIdDoesNotExist = false) {
         attrs.remove('tagName') // Just in case one is left
         Encoder htmlEncoder = codecLookup?.lookupEncoder('HTML')
         attrs.each { k, v ->
@@ -393,9 +393,9 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr useToken Set whether to send a token in the request to handle 
duplicate form submissions. See Handling Duplicate Form Submissions
      * @attr method the form method to use, either 'POST' or 'GET'; defaults 
to 'POST'
      */
-    Closure uploadForm = { attrs, body ->
+    def uploadForm(Map attrs, Closure body) {
         attrs.enctype = 'multipart/form-data'
-        out << form(attrs, body)
+        form(attrs, body)
     }
 
     /**
@@ -412,7 +412,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr useToken Set whether to send a token in the request to handle 
duplicate form submissions. See Handling Duplicate Form Submissions
      * @attr method the form method to use, either 'POST' or 'GET'; defaults 
to 'POST'
      */
-    Closure form = { attrs, body ->
+    def form(Map attrs, Closure body) {
 
         boolean useToken = false
         if (attrs.containsKey('useToken')) {
@@ -526,7 +526,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      *
      */
     @Deprecated(since = '7.0.0')
-    Closure actionSubmit = { attrs ->
+    def actionSubmit(Map attrs) {
         if (!attrs.value) {
             throwTagError('Tag [actionSubmit] is missing required attribute 
[value]')
         }
@@ -580,7 +580,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr base Sets the prefix to be added to the link target address, 
typically an absolute server URL. This overrides the behaviour of the absolute 
property, if both are specified.
      * @attr event Webflow _eventId parameter
      */
-    def formActionSubmit = { Map attrs ->
+    def formActionSubmit(Map attrs) {
         if (!attrs.value) {
             throwTagError('Tag [formActionSubmit] is missing required 
attribute [value]')
         }
@@ -634,7 +634,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr src The source of the image to use
      * @attr disabled Makes the button to be disabled. Will be interpreted as 
a Groovy Truth
      */
-    Closure actionSubmitImage = { attrs ->
+    def actionSubmitImage(Map attrs) {
         attrs.tagName = 'actionSubmitImage'
 
         if (!attrs.value) {
@@ -683,7 +683,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr locale The locale to use for display formatting. Defaults to the 
current request locale and then the system default locale if not specified.
      * @attr selectDateClass css class added to each select tag
      */
-    Closure datePicker = { attrs ->
+    def datePicker(Map attrs) {
         def out = out // let x = x ?
         def xdefault = attrs['default']
         if (xdefault == null) {
@@ -939,11 +939,11 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
         }
     }
 
-    Closure renderNoSelectionOption = { noSelectionKey, noSelectionValue, 
value ->
+    def renderNoSelectionOption(noSelectionKey, noSelectionValue, value) {
         renderNoSelectionOptionImpl(out, noSelectionKey, noSelectionValue, 
value)
     }
 
-    def renderNoSelectionOptionImpl(out, noSelectionKey, noSelectionValue, 
value) {
+    private def renderNoSelectionOptionImpl(out, noSelectionKey, 
noSelectionValue, value) {
         // If a label for the '--Please choose--' first item is supplied, 
write it out
         out << "<option value=\"${(noSelectionKey == null ? '' : 
noSelectionKey)}\"${noSelectionKey == value ? ' selected="selected"' : 
''}>${noSelectionValue.encodeAsHTML()}</option>"
     }
@@ -958,7 +958,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr value An instance of java.util.TimeZone. Defaults to the time 
zone for the current Locale if not specified
      * @attr locale The locale to use for formatting the time zone names. 
Defaults to the current request locale and then system default locale if not 
specified
      */
-    Closure timeZoneSelect = { attrs ->
+    def timeZoneSelect(Map attrs) {
         attrs.from = TimeZone.getAvailableIDs()
         attrs.value = (attrs.value ? attrs.value.ID : TimeZone.getDefault().ID)
         def date = new Date()
@@ -978,7 +978,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
         }
 
         // use generic select
-        out << select(attrs)
+        select(attrs)
     }
 
     /**
@@ -992,7 +992,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr value The set locale, defaults to the current request locale if 
not specified
      * @attr locale The locale to use for formatting the locale names. 
Defaults to the current request locale and then the system default locale if 
not specified
      */
-    Closure localeSelect = { attrs ->
+    def localeSelect(Map attrs) {
         attrs.from = Locale.getAvailableLocales()
         attrs.value = (attrs.value ?: RCU.getLocale(request))?.toString()
         // set the key as a closure that formats the locale
@@ -1014,7 +1014,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr from The currency symbols to select from, defaults to the major 
ones if not specified
      * @attr value The currency value as the currency code. Defaults to the 
currency for the current Locale if not specified
      */
-    Closure currencySelect = { attrs, body ->
+    def currencySelect(Map attrs, Closure body) {
         if (!attrs.from) {
             attrs.from = DEFAULT_CURRENCY_CODES
         }
@@ -1053,7 +1053,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr dataAttrs a Map that adds data-* attributes to the &lt;option&gt; 
elements. Map's keys will be used as names of the data-* attributes like so: 
data-${key} (i.e. with a "data-" prefix). The object belonging to a Map's key 
determines the value of the data-* attribute. It can be a string referring to a 
property of beans in {@code from}, a Closure that accepts an item from {@code 
from} and returns the value or a List that contains a value for each of the 
&lt;option&gt;s.
      * @attr locale The locale to use for formatting. Defaults to the current 
request locale and then the system default locale if not specified
      */
-    Closure select = { attrs ->
+    def select(Map attrs) {
         if (!attrs.name) {
             throwTagError('Tag [select] is missing required attribute [name]')
         }
@@ -1257,7 +1257,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr readonly boolean to indicate that the radio button should not be 
editable
      * @attr id the DOM element id
      */
-    Closure radio = { attrs ->
+    def radio(Map attrs) {
         def value = attrs.remove('value')
         def name = attrs.remove('name')
         booleanToAttribute(attrs, 'disabled')
@@ -1286,7 +1286,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr disabled Disables the resulting radio buttons.
      * @attr readonly Makes the resulting radio buttons to not be editable
      */
-    Closure radioGroup = { attrs, body ->
+    def radioGroup(Map attrs, Closure body) {
         def value = attrs.remove('value')
         def values = attrs.remove('values')
         def labels = attrs.remove('labels')
@@ -1318,7 +1318,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
         }
     }
 
-    private processFormFieldValueIfNecessary(name, value, type) {
+    private def processFormFieldValueIfNecessary(name, value, type) {
         if (requestDataValueProcessor != null) {
             return requestDataValueProcessor.processFormFieldValue(request, 
name, "${value}", type)
         }
@@ -1328,7 +1328,7 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
     /**
      * Filters the url through the RequestDataValueProcessor bean if it is 
registered.
      */
-    String processedUrl(String link, request) {
+    private String processedUrl(String link, request) {
         if (requestDataValueProcessor == null) {
             return link
         }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
index cbdbc072bf..2825d7ac77 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormatTagLib.groovy
@@ -94,7 +94,7 @@ class FormatTagLib implements TagLibrary {
      * @attr false text label for boolean false value
      * @attr locale Force the locale for formatting.
      */
-    Closure formatBoolean = { attrs ->
+    def formatBoolean(Map attrs) {
         if (!attrs.containsKey('boolean')) {
             throwTagError('Tag [formatBoolean] is missing required attribute 
[boolean]')
         }
@@ -144,7 +144,7 @@ class FormatTagLib implements TagLibrary {
      * @attr dateStyle Set separate style for the date part.
      * @attr timeStyle Set separate style for the time part.
      */
-    Closure formatDate = { attrs ->
+    def formatDate(Map attrs) {
 
         def date
         if (attrs.containsKey('date')) {
@@ -230,7 +230,7 @@ class FormatTagLib implements TagLibrary {
      * @attr roundingMode Sets the RoundingMode used in this DecimalFormat. 
Usual values: HALF_UP, HALF_DOWN. If roundingMode is UNNECESSARY and 
ArithemeticException raises, the original number formatted with default number 
formatting will be returned.
      * @attr nan String to be used for display if numberic value is NaN
      */
-    Closure formatNumber = { attrs ->
+    def formatNumber(Map attrs) {
         if (!attrs.containsKey('number')) {
             throwTagError('Tag [formatNumber] is missing required attribute 
[number]')
         }
@@ -360,7 +360,7 @@ class FormatTagLib implements TagLibrary {
      *
      * @attr codec REQUIRED the codec name
      */
-    Closure encodeAs = { attrs, body ->
+    def encodeAs(Map attrs, Closure body) {
         if (!attrs.codec) {
             throwTagError('Tag [encodeAs] requires a codec name in the [codec] 
attribute')
         }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/JavascriptTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/JavascriptTagLib.groovy
index f9e541bb39..db20ab98ee 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/JavascriptTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/JavascriptTagLib.groovy
@@ -71,7 +71,7 @@ class JavascriptTagLib implements ApplicationContextAware, 
TagLibrary {
      * @attr contextPath the context path to use (relative to the application 
context path). Defaults to "" or path to the plugin for a plugin view or 
template.
      * @attr base specifies the full base url to prepend to the library name
      */
-    Closure javascript = { attrs, body ->
+    Closure javascript = { Map attrs, body ->
         if (attrs.src) {
             javascriptInclude(attrs)
         } else {
@@ -121,7 +121,7 @@ class JavascriptTagLib implements ApplicationContextAware, 
TagLibrary {
      *
      * &lt;g:escapeJavascript&gt;This is some "text" to be 
escaped&lt;/g:escapeJavascript&gt;
      */
-    Closure escapeJavascript = { attrs, body ->
+    Closure escapeJavascript = { Map attrs, body ->
         if (body) {
             out << body()
         }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/PluginTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/PluginTagLib.groovy
index 99757f550f..f86e80b93d 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/PluginTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/PluginTagLib.groovy
@@ -42,7 +42,7 @@ class PluginTagLib implements TagLibrary {
      *
      * @attr name REQUIRED the plugin name
      */
-    Closure path = { attrs, body ->
+    def path(Map attrs, Closure body) {
         out << pluginManager.getPluginPath(attrs.name)
     }
 
@@ -54,7 +54,7 @@ class PluginTagLib implements TagLibrary {
      * @attr name REQUIRED the plugin name
      * @attr version the plugin version
      */
-    Closure isAvailable = { attrs, body ->
+    def isAvailable(Map attrs, Closure body) {
         if (checkPluginExists(attrs.version, attrs.name)) {
             out << body()
         }
@@ -68,7 +68,7 @@ class PluginTagLib implements TagLibrary {
      * @attr name REQUIRED the plugin name
      * @attr version the plugin version
      */
-    Closure isNotAvailable = { attrs, body ->
+    def isNotAvailable(Map attrs, Closure body) {
         if (!checkPluginExists(attrs.version, attrs.name)) {
             out << body()
         }
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
index 09d8c961ac..2d695e987c 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
@@ -66,7 +66,7 @@ class UrlMappingTagLib implements TagLibrary {
      * @attr view The name of the view. Cannot be specified in combination 
with controller/action/id
      * @attr model A model to pass onto the included controller in the request
      */
-    Closure include = { Map attrs, body ->
+    def include(Map attrs, Closure body) {
         if (attrs.action && !attrs.controller) {
             def controller = 
request?.getAttribute(GrailsApplicationAttributes.CONTROLLER)
             def controllerName = ((GroovyObject) 
controller)?.getProperty('controllerName')
@@ -119,7 +119,7 @@ class UrlMappingTagLib implements TagLibrary {
      * @attr mapping The named URL mapping to use to rewrite the link
      * @attr fragment The link fragment (often called anchor tag) to use
      */
-    Closure paginate = { Map attrsMap ->
+    def paginate(Map attrsMap) {
         TypeConvertingMap attrs = (TypeConvertingMap) attrsMap
         def writer = out
         if (attrs.total == null) {
@@ -184,14 +184,14 @@ class UrlMappingTagLib implements TagLibrary {
         // display previous link when not on firststep unless omitPrev is true
         if (currentstep > firststep && !attrs.boolean('omitPrev')) {
             linkParams.offset = offset - max
-            writer << callLink(appendClass((Map) linkTagAttrs.clone(), 
'prevLink')) {
+            writer << callLink(appendClass(new LinkedHashMap(linkTagAttrs), 
'prevLink')) {
                 (attrs.prev ?: messageSource.getMessage('paginate.prev', null, 
messageSource.getMessage('default.paginate.prev', null, 'Previous', locale), 
locale))
             }
         }
 
         // display steps when steps are enabled and laststep is not firststep
         if (steps && laststep > firststep) {
-            Map stepAttrs = appendClass((Map) linkTagAttrs.clone(), 'step')
+            Map stepAttrs = appendClass(new LinkedHashMap(linkTagAttrs), 
'step')
 
             // determine begin and endstep paging variables
             int beginstep = currentstep - (Math.round(maxsteps / 2.0d) as int) 
+ (maxsteps % 2)
@@ -212,7 +212,7 @@ class UrlMappingTagLib implements TagLibrary {
             // display firststep link when beginstep is not firststep
             if (beginstep > firststep && !attrs.boolean('omitFirst')) {
                 linkParams.offset = 0
-                writer << callLink((Map) stepAttrs.clone()) { 
firststep.toString() }
+                writer << callLink(new LinkedHashMap(stepAttrs)) { 
firststep.toString() }
             }
             //show a gap if beginstep isn't immediately after firststep, and 
if were not omitting first or rev
             if (beginstep > firststep + 1 && (!attrs.boolean('omitFirst') || 
!attrs.boolean('omitPrev'))) {
@@ -226,7 +226,7 @@ class UrlMappingTagLib implements TagLibrary {
                 }
                 else {
                     linkParams.offset = (i - 1) * max
-                    writer << callLink((Map) stepAttrs.clone()) { i.toString() 
}
+                    writer << callLink(new LinkedHashMap(stepAttrs)) { 
i.toString() }
                 }
             }
 
@@ -237,14 +237,14 @@ class UrlMappingTagLib implements TagLibrary {
             // display laststep link when endstep is not laststep
             if (endstep < laststep && !attrs.boolean('omitLast')) {
                 linkParams.offset = (laststep - 1) * max
-                writer << callLink((Map) stepAttrs.clone()) { 
laststep.toString() }
+                writer << callLink(new LinkedHashMap(stepAttrs)) { 
laststep.toString() }
             }
         }
 
         // display next link when not on laststep unless omitNext is true
         if (currentstep < laststep && !attrs.boolean('omitNext')) {
             linkParams.offset = offset + max
-            writer << callLink(appendClass((Map) linkTagAttrs.clone(), 
'nextLink')) {
+            writer << callLink(appendClass(new LinkedHashMap(linkTagAttrs), 
'nextLink')) {
                 (attrs.next ? attrs.next : 
messageSource.getMessage('paginate.next', null, 
messageSource.getMessage('default.paginate.next', null, 'Next', locale), 
locale))
             }
         }
@@ -277,7 +277,7 @@ class UrlMappingTagLib implements TagLibrary {
      * @attr params A map containing URL query parameters
      * @attr class CSS class name
      */
-    Closure sortableColumn = { Map attrs ->
+    def sortableColumn(Map attrs) {
         def writer = out
         if (!attrs.property) {
             throwTagError('Tag [sortableColumn] is missing required attribute 
[property]')
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
index c8a3f376b9..7037c317f8 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
@@ -65,7 +65,7 @@ class ValidationTagLib implements TagLibrary {
      * @attr encodeAs The name of a codec to apply, i.e. HTML, JavaScript, URL 
etc
      * @attr locale override locale to use instead of the one detected
      */
-    Closure fieldError = { attrs, body ->
+    def fieldError(Map attrs, Closure body) {
         def bean = attrs.bean
         def field = attrs.field
         def encodeAs = attrs.encodeAs
@@ -91,7 +91,7 @@ class ValidationTagLib implements TagLibrary {
      * @attr valueMessagePrefix Setting this allows the value to be resolved 
from the I18n messages.
      *
      */
-    Closure fieldValue = { attrs, body ->
+    def fieldValue(Map attrs, Closure body) {
         def bean = attrs.bean
         String field = attrs.field?.toString()
         if (!bean || !field) {
@@ -130,7 +130,7 @@ class ValidationTagLib implements TagLibrary {
         return rejectedValue
     }
 
-    def extractErrors(attrs) {
+    private def extractErrors(Map attrs) {
         def model = attrs.model
         def checkList = []
         if (attrs.containsKey('bean')) {
@@ -186,7 +186,7 @@ class ValidationTagLib implements TagLibrary {
      * @attr field The field of the bean or model reference to check
      * @attr model The model reference to check for errors
      */
-    Closure hasErrors = { attrs, body ->
+    def hasErrors(Map attrs, Closure body) {
         def errorsList = extractErrors(attrs)
         if (errorsList) {
             out << body()
@@ -201,16 +201,16 @@ class ValidationTagLib implements TagLibrary {
      * @attr field The field of the bean or model reference to check
      * @attr model The model reference to check for errors
      */
-    Closure eachError = { attrs, body ->
+    def eachError(Map attrs, Closure body) {
         eachErrorInternal(attrs, body, true)
     }
 
-    def eachErrorInternal(attrs, body, boolean outputResult = false) {
+    private def eachErrorInternal(Map attrs, Closure body, boolean 
outputResult = false) {
         def errorsList = extractErrors(attrs)
         eachErrorInternalForList(attrs, errorsList, body, outputResult)
     }
 
-    def eachErrorInternalForList(attrs, errorsList, body, boolean outputResult 
= false) {
+    private def eachErrorInternalForList(Map attrs, errorsList, Closure body, 
boolean outputResult = false) {
         def var = attrs.var
         def field = attrs.field
 
@@ -290,12 +290,12 @@ class ValidationTagLib implements TagLibrary {
      * @attr encodeAs The name of a codec to apply, i.e. HTML, JavaScript, URL 
etc
      * @attr locale override locale to use instead of the one detected
      */
-    Closure message = { attrs ->
+    def message(Map attrs) {
         messageImpl(attrs)
     }
 
     @CompileStatic
-    def messageImpl(Map attrs) {
+    private def messageImpl(Map attrs) {
         Locale locale = FormatTagLib.resolveLocale(attrs.locale)
         def tagSyntaxCall = (attrs instanceof GroovyPageAttributes) ? 
attrs.isGspTagSyntaxCall() : false
 
@@ -383,7 +383,7 @@ class ValidationTagLib implements TagLibrary {
      * @attr form REQUIRED the HTML form name
      * @attr againstClass REQUIRED the domain class name
      */
-    Closure validate = { attrs, body ->
+    def validate(Map attrs, Closure body) {
         def form = attrs.form
         if (!form) {
             throwTagError('Tag [validate] is missing required attribute 
[form]')
diff --git 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
new file mode 100644
index 0000000000..ae636b4996
--- /dev/null
+++ 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
@@ -0,0 +1,128 @@
+/*
+ *  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 org.grails.web.taglib
+
+import grails.artefact.Artefact
+import grails.compiler.GrailsCompileStatic
+import grails.testing.web.taglib.TagLibUnitTest
+import spock.lang.Specification
+
+class MethodDefinedTagLibSpec extends Specification implements 
TagLibUnitTest<MethodTagLib> {
+
+    void setupSpec() {
+        mockTagLibs(MethodTagLib, SharedNsMethodTagLib, SharedNsClosureTagLib, 
StaticMethodTagLib)
+    }
+
+    void "method tag can use implicit attrs"() {
+        expect:
+        applyTemplate('<g:methodTag blah="duh" />') == 'duh - is this'
+    }
+
+    void "method tag can bind named attribute to typed argument"() {
+        expect:
+        applyTemplate('<g:typedTag blah="duh" />') == 'duh - typed'
+    }
+    void "method tag can bind multiple named attributes to multiple typed 
arguments"() {
+        expect:
+        applyTemplate('<g:multiTypedTag first="hello" second="world" />') == 
'hello-world'
+    }
+
+    void "method tag can use implicit body closure"() {
+        expect:
+        applyTemplate('<g:bodyTag>abc</g:bodyTag>') == 'before-abc-after'
+    }
+
+    void "closure tag remains supported"() {
+        expect:
+        applyTemplate('<g:legacyTag blah="duh" />') == 'legacy-duh'
+    }
+
+    void "multiple taglibs sharing the same namespace resolve independently"() 
{
+        expect:
+        applyTemplate('<shared:fromMethod one="1" /> <shared:fromClosure 
two="2" />') == 'method-1 closure-2'
+    }
+
+    void "statically compiled method tag can use implicit attrs and typed 
args"() {
+        expect:
+        applyTemplate('<g:staticImplicitTag blah="duh" /> <g:staticTypedTag 
blah="duh2" />') == 'duh - static implicit duh2 - static typed'
+    }
+
+    void "statically compiled method tag can render body"() {
+        expect:
+        applyTemplate('<g:staticBodyTag>abc</g:staticBodyTag>') == 
'before-abc-after'
+    }
+}
+
+@GrailsCompileStatic
+@Artefact('TagLib')
+class StaticMethodTagLib {
+    def staticImplicitTag() {
+        Map tagAttrs = (Map) propertyMissing('attrs')
+        out << "${tagAttrs.blah} - static implicit"
+    }
+
+    def staticTypedTag(String blah) {
+        out << "${blah} - static typed"
+    }
+
+    def staticBodyTag() {
+        Closure tagBody = (Closure) propertyMissing('body')
+        out << "before-${tagBody?.call()}-after"
+    }
+}
+
+@Artefact('TagLib')
+class MethodTagLib {
+    def methodTag() {
+        out << "${attrs.blah} - is this"
+    }
+
+    def typedTag(String blah) {
+        out << "${blah} - typed"
+    }
+    def multiTypedTag(String first, String second) {
+        out << "${first}-${second}"
+    }
+
+    def bodyTag() {
+        out << "before-${body()}-after"
+    }
+
+    Closure legacyTag = { attrs, body ->
+        out << "legacy-${attrs.blah}"
+    }
+}
+
+@Artefact('TagLib')
+class SharedNsMethodTagLib {
+    static namespace = 'shared'
+
+    def fromMethod(String one) {
+        out << "method-${one}"
+    }
+}
+
+@Artefact('TagLib')
+class SharedNsClosureTagLib {
+    static namespace = 'shared'
+
+    Closure fromClosure = { attrs ->
+        out << "closure-${attrs.two}"
+    }
+}
diff --git 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodVsClosureTagInvocationBenchmarkSpec.groovy
 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodVsClosureTagInvocationBenchmarkSpec.groovy
new file mode 100644
index 0000000000..49fcea1924
--- /dev/null
+++ 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodVsClosureTagInvocationBenchmarkSpec.groovy
@@ -0,0 +1,80 @@
+/*
+ *  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 org.grails.web.taglib
+
+import grails.artefact.Artefact
+import grails.testing.web.taglib.TagLibUnitTest
+import spock.lang.Specification
+
+class MethodVsClosureTagInvocationBenchmarkSpec extends Specification 
implements TagLibUnitTest<MethodVsClosureBenchmarkTagLib> {
+
+    void 'benchmark method invocation versus closure invocation for taglibs'() 
{
+        given:
+        int warmupIterations = 50
+        int measureIterations = 300
+        String closureTemplate = '<g:closureTag value="123" />'
+        String methodTemplate = '<g:methodTag value="123" />'
+
+        expect:
+        applyTemplate(closureTemplate) == '123'
+        applyTemplate(methodTemplate) == '123'
+
+        when:
+        warmupIterations.times {
+            applyTemplate(closureTemplate)
+            applyTemplate(methodTemplate)
+        }
+
+        long closureNanos = measureNanos(measureIterations) {
+            applyTemplate(closureTemplate)
+        }
+        long methodNanos = measureNanos(measureIterations) {
+            applyTemplate(methodTemplate)
+        }
+
+        double closurePerOpMicros = (closureNanos / (double) 
measureIterations) / 1_000d
+        double methodPerOpMicros = (methodNanos / (double) measureIterations) 
/ 1_000d
+        double ratio = methodPerOpMicros / closurePerOpMicros
+
+        println "BENCHMARK taglib invocation: closure=${String.format('%.3f', 
closurePerOpMicros)}us/op, method=${String.format('%.3f', 
methodPerOpMicros)}us/op, method/closure=${String.format('%.3f', ratio)}"
+
+        then:
+        closurePerOpMicros > 0d
+        methodPerOpMicros > 0d
+    }
+
+    private static long measureNanos(int iterations, Closure<?> work) {
+        long start = System.nanoTime()
+        iterations.times {
+            work.call()
+        }
+        System.nanoTime() - start
+    }
+}
+
+@Artefact('TagLib')
+class MethodVsClosureBenchmarkTagLib {
+    Closure closureTag = { attrs ->
+        out << attrs.value
+    }
+
+    def methodTag(String value) {
+        out << value
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
index 5afd2cb8a6..a70f511f4c 100644
--- 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
@@ -43,6 +43,10 @@ class MiscController {
         [:]
     }
 
+    def tagMethods() {
+        render(view: 'tagMethods')
+    }
+
     def interceptedByInterceptor() {
        // no op
     }
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
 
b/grails-test-examples/app1/grails-app/taglib/functionaltests/MethodTagLib.groovy
similarity index 63%
copy from 
grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
copy to 
grails-test-examples/app1/grails-app/taglib/functionaltests/MethodTagLib.groovy
index 5afd2cb8a6..3c6ee4fd9e 100644
--- 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/MiscController.groovy
+++ 
b/grails-test-examples/app1/grails-app/taglib/functionaltests/MethodTagLib.groovy
@@ -16,40 +16,31 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package functionaltests
+import grails.compiler.GrailsCompileStatic
 
-import org.springframework.beans.factory.annotation.*
-import org.example.MyBean
-
-class MiscController {
-
-       @Value('${foo.bar}')
-       String testProperty
+@GrailsCompileStatic
+class MethodTagLib {
 
-    @Autowired
-    MyBean myBean
-
-    def beanPropertyOverrideTest() {
-        render myBean.name()
+    def implicitTag() {
+        Map tagAttrs = (Map) propertyMissing('attrs')
+        out << "${tagAttrs.blah} - implicit"
     }
 
-
-    def actionWhichReturnsNull() {
-        null
+    def typedTag(String blah) {
+        out << "${blah} - typed"
     }
 
-    def actionWhichReturnsMap() {
-        [:]
+    def multiTypedTag(String first, String second) {
+        out << "${first}-${second}"
     }
 
-    def interceptedByInterceptor() {
-       // no op
+    def bodyTag() {
+        Closure tagBody = (Closure) propertyMissing('body')
+        out << "before-${tagBody?.call()}-after"
     }
 
-    def placeHolderConfig() {
-       def config = grailsApplication.config
-
-       render "[${config.foo.bar} ${config.getProperty('foo.bar')} 
${testProperty}]"
+    Closure legacyTag = { Map attrs ->
+        out << "legacy-${attrs.blah}"
     }
 }
diff --git 
a/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy 
b/grails-test-examples/app1/grails-app/taglib/functionaltests/SharedNsClosureTagLib.groovy
similarity index 77%
copy from grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
copy to 
grails-test-examples/app1/grails-app/taglib/functionaltests/SharedNsClosureTagLib.groovy
index 8436d2a995..ee2fca5b39 100644
--- a/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
+++ 
b/grails-test-examples/app1/grails-app/taglib/functionaltests/SharedNsClosureTagLib.groovy
@@ -16,17 +16,12 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+package functionaltests
 
-// tag::basic_declaration[]
-package demo
+class SharedNsClosureTagLib {
+    static namespace = 'shared'
 
-class SecondTagLib {
-    static defaultEncodeAs = [taglib:'html']
-
-    static namespace = 'two'
-
-    def sayHello = { attrs ->
-        out << 'Hello From SecondTagLib'
+    Closure fromClosure = { attrs ->
+        out << "closure-${attrs.two}"
     }
 }
-// end::basic_declaration[]
diff --git 
a/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy 
b/grails-test-examples/app1/grails-app/taglib/functionaltests/SharedNsMethodTagLib.groovy
similarity index 77%
copy from grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
copy to 
grails-test-examples/app1/grails-app/taglib/functionaltests/SharedNsMethodTagLib.groovy
index 8436d2a995..81eb5583cf 100644
--- a/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
+++ 
b/grails-test-examples/app1/grails-app/taglib/functionaltests/SharedNsMethodTagLib.groovy
@@ -16,17 +16,12 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+package functionaltests
 
-// tag::basic_declaration[]
-package demo
+class SharedNsMethodTagLib {
+    static namespace = 'shared'
 
-class SecondTagLib {
-    static defaultEncodeAs = [taglib:'html']
-
-    static namespace = 'two'
-
-    def sayHello = { attrs ->
-        out << 'Hello From SecondTagLib'
+    def fromMethod(String one) {
+        out << "method-${one}"
     }
 }
-// end::basic_declaration[]
diff --git a/grails-test-examples/app1/grails-app/views/misc/tagMethods.gsp 
b/grails-test-examples/app1/grails-app/views/misc/tagMethods.gsp
new file mode 100644
index 0000000000..93bd9f87e0
--- /dev/null
+++ b/grails-test-examples/app1/grails-app/views/misc/tagMethods.gsp
@@ -0,0 +1,7 @@
+<g:implicitTag blah="duh" />
+<g:typedTag blah="duh2" />
+<g:multiTypedTag first="hello" second="world" />
+<g:bodyTag>abc</g:bodyTag>
+<g:legacyTag blah="legacy" />
+<shared:fromMethod one="1" />
+<shared:fromClosure two="2" />
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/MiscFunctionalSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/MiscFunctionalSpec.groovy
index ac24114827..7315cacc77 100644
--- 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/MiscFunctionalSpec.groovy
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/MiscFunctionalSpec.groovy
@@ -45,4 +45,18 @@ class MiscFunctionalSpec extends ContainerGebSpec {
         expect:
         to(PlaceHolderConfigPage)
     }
+
+    void 'Test method-defined taglibs render expected output end to end'() {
+        when:
+        go('/misc/tagMethods')
+
+        then:
+        pageSource.contains('duh - implicit')
+        pageSource.contains('duh2 - typed')
+        pageSource.contains('hello-world')
+        pageSource.contains('before-abc-after')
+        pageSource.contains('legacy-legacy')
+        pageSource.contains('method-1')
+        pageSource.contains('closure-2')
+    }
 }
diff --git 
a/grails-test-examples/demo33/grails-app/taglib/demo/FirstTagLib.groovy 
b/grails-test-examples/demo33/grails-app/taglib/demo/FirstTagLib.groovy
index 01019daa96..0f653bf6ab 100644
--- a/grails-test-examples/demo33/grails-app/taglib/demo/FirstTagLib.groovy
+++ b/grails-test-examples/demo33/grails-app/taglib/demo/FirstTagLib.groovy
@@ -25,7 +25,7 @@ class FirstTagLib {
 
     static namespace = 'one'
 
-    def sayHello = { attrs ->
+    def sayHello() {
         out << 'BEFORE '
 
         // this is invoking a tag from another tag library
diff --git 
a/grails-test-examples/demo33/grails-app/taglib/demo/SampleTagLib.groovy 
b/grails-test-examples/demo33/grails-app/taglib/demo/SampleTagLib.groovy
index 2cb34d391a..22f70e5d14 100644
--- a/grails-test-examples/demo33/grails-app/taglib/demo/SampleTagLib.groovy
+++ b/grails-test-examples/demo33/grails-app/taglib/demo/SampleTagLib.groovy
@@ -28,23 +28,23 @@ class SampleTagLib {
 
     // end::basic_declaration[]
     // tag::hello_world[]
-    def helloWorld = { attrs ->
+    def helloWorld() {
         out << 'Hello, World!'
     }
     // end::hello_world[]
     // tag::say_hello[]
-    def sayHello = { attrs ->
+    def sayHello() {
         out << "Hello, ${attrs.name}!"
     }
     // end::say_hello[]
     // tag::render_some_number[]
-    def renderSomeNumber = { attrs ->
+    def renderSomeNumber() {
         int number = attrs.int('value', -1)
         out << "The Number Is ${number}"
     }
     // end::render_some_number[]
 
-    def renderMessage = {
+    def renderMessage() {
         out << message(code: 'some.custom.message', locale: request.locale)
     }
 // tag::basic_declaration[]
diff --git 
a/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy 
b/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
index 8436d2a995..7e97149060 100644
--- a/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
+++ b/grails-test-examples/demo33/grails-app/taglib/demo/SecondTagLib.groovy
@@ -25,7 +25,7 @@ class SecondTagLib {
 
     static namespace = 'two'
 
-    def sayHello = { attrs ->
+    def sayHello() {
         out << 'Hello From SecondTagLib'
     }
 }
diff --git a/grails-test-examples/plugins/loadafter/build.gradle 
b/grails-test-examples/plugins/loadafter/build.gradle
index 65151fdeff..6120dca94b 100644
--- a/grails-test-examples/plugins/loadafter/build.gradle
+++ b/grails-test-examples/plugins/loadafter/build.gradle
@@ -40,7 +40,7 @@ dependencies {
     api 'com.h2database:h2'
     api 'jakarta.servlet:jakarta.servlet-api'
 
-    implementation 
"org.apache.grails:grails-spring-security:$grailsSpringSecurityVersion"
+    implementation 'org.apache.grails:grails-spring-security:7.0.1'
 
     console 'org.apache.grails:grails-console'
 }


Reply via email to