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 78c0d84a3b5bf75b9e66fcb09f78d38fba7dacc4 Author: David Estes <[email protected]> AuthorDate: Wed Feb 25 22:43:28 2026 -0500 Optimize tag invocation dispatch with concurrent-safe method caching - add thread-safe ClassValue cache for invokable public tag methods by name - remove per-invocation getMethods scans in hasInvokableTagMethod/invokeTagMethod - optimize TagLibrary.propertyMissing by caching method fallback closures in non-dev mode - use resolved namespace for default-namespace fallback closures Co-Authored-By: Oz <[email protected]> --- .../groovy/org/grails/taglib/TagMethodInvoker.java | 34 +++++++++++++++------- .../main/groovy/grails/artefact/TagLibrary.groovy | 11 ++++--- 2 files changed, 29 insertions(+), 16 deletions(-) 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 index cf19e2fb4b..4644417709 100644 --- 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 @@ -26,6 +26,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,6 +35,22 @@ import groovy.lang.GroovyObject; import groovy.lang.MissingMethodException; public final class TagMethodInvoker { + private static final ClassValue<Map<String, List<Method>>> INVOKABLE_METHODS_BY_NAME = new ClassValue<>() { + @Override + protected Map<String, List<Method>> computeValue(Class<?> type) { + Map<String, List<Method>> methodsByName = new HashMap<>(); + for (Method method : type.getMethods()) { + if (isTagMethodCandidate(method)) { + methodsByName.computeIfAbsent(method.getName(), ignored -> new ArrayList<>()).add(method); + } + } + Map<String, List<Method>> immutableMethodsByName = new HashMap<>(methodsByName.size()); + for (Map.Entry<String, List<Method>> entry : methodsByName.entrySet()) { + immutableMethodsByName.put(entry.getKey(), Collections.unmodifiableList(entry.getValue())); + } + return Collections.unmodifiableMap(immutableMethodsByName); + } + }; private TagMethodInvoker() { } @@ -70,19 +87,16 @@ public final class TagMethodInvoker { } 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; + List<Method> methods = INVOKABLE_METHODS_BY_NAME.get(tagLib.getClass()).get(tagName); + return methods != null && !methods.isEmpty(); } 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; - } + List<Method> methods = INVOKABLE_METHODS_BY_NAME.get(tagLib.getClass()).get(tagName); + if (methods == null) { + throw new MissingMethodException(tagName, tagLib.getClass(), new Object[] { attrs, body }); + } + for (Method method : methods) { Object[] args = toMethodArguments(method, attrs, body); if (args != null) { try { 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 74a02b72c3..ffe068ff0d 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 @@ -149,13 +149,13 @@ trait TagLibrary implements WebAttributes, ServletAttributes, TagLibraryInvoker } TagLibraryLookup gspTagLibraryLookup = getTagLibraryLookup() if (gspTagLibraryLookup != null) { - boolean methodTagFallback = false Object result = gspTagLibraryLookup.lookupNamespaceDispatcher(name) if (result == null) { - String namespace = getTaglibNamespace() - GroovyObject tagLibrary = gspTagLibraryLookup.lookupTagLibrary(namespace, name) + String resolvedNamespace = getTaglibNamespace() + GroovyObject tagLibrary = gspTagLibraryLookup.lookupTagLibrary(resolvedNamespace, name) if (tagLibrary == null) { + resolvedNamespace = TagOutput.DEFAULT_NAMESPACE tagLibrary = gspTagLibraryLookup.lookupTagLibrary(TagOutput.DEFAULT_NAMESPACE, name) } @@ -164,8 +164,7 @@ trait TagLibrary implements WebAttributes, ServletAttributes, TagLibraryInvoker if (tagProperty instanceof Closure) { result = ((Closure<?>) tagProperty).clone() } else if (TagMethodInvoker.hasInvokableTagMethod(tagLibrary, name)) { - methodTagFallback = true - final String currentNamespace = namespace + final String currentNamespace = resolvedNamespace 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() @@ -179,7 +178,7 @@ trait TagLibrary implements WebAttributes, ServletAttributes, TagLibraryInvoker } } } - if (result != null && !Environment.isDevelopmentMode() && !methodTagFallback) { + if (result != null && !Environment.isDevelopmentMode()) { MetaClass mc = GrailsMetaClassUtils.getExpandoMetaClass(getClass()) // Register the property for the already-existing singleton instance of the taglib
