You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/07/01 04:31:13 UTC

(grails-core) 01/02: Avoid duplicate cached codec metaclass registration

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

jamesfredley pushed a commit to branch fix/15374-codec-metaclass-registration-overhead
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 6e914ccac0a6b8866f71cdb7fd20e95f87fd65c1
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Tue Jun 30 23:53:25 2026 -0400

    Avoid duplicate cached codec metaclass registration
    
    Assisted-by: Hephaestus:gpt-5.5
---
 .../grails/encoder/CodecMetaClassSupport.groovy    | 120 ++++++++-
 .../encoder/CodecMetaClassSupportSpec.groovy       | 283 +++++++++++++++++++++
 .../commons/DefaultGrailsCodecClassTests.groovy    |  16 ++
 3 files changed, 412 insertions(+), 7 deletions(-)

diff --git a/grails-encoder/src/main/groovy/org/grails/encoder/CodecMetaClassSupport.groovy b/grails-encoder/src/main/groovy/org/grails/encoder/CodecMetaClassSupport.groovy
index f304048bc9..35e613d203 100644
--- a/grails-encoder/src/main/groovy/org/grails/encoder/CodecMetaClassSupport.groovy
+++ b/grails-encoder/src/main/groovy/org/grails/encoder/CodecMetaClassSupport.groovy
@@ -18,6 +18,10 @@
  */
 package org.grails.encoder
 
+import java.lang.ref.WeakReference
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicLong
+
 import groovy.transform.CompileStatic
 import org.codehaus.groovy.runtime.GStringImpl
 import org.codehaus.groovy.runtime.NullObject
@@ -38,6 +42,8 @@ class CodecMetaClassSupport {
     static final Object[] EMPTY_ARGS = []
     static final String ENCODE_AS_PREFIX = 'encodeAs'
     static final String DECODE_PREFIX = 'decode'
+    private static final Set<MetaMethodRegistrationKey> REGISTERED_META_METHODS = Collections.newSetFromMap(new ConcurrentHashMap<MetaMethodRegistrationKey, Boolean>())
+    private static final AtomicLong META_METHOD_REGISTRATION_COUNT = new AtomicLong()
 
     /**
      * Adds "encodeAs*" and "decode*" metamethods for given codecClass
@@ -101,14 +107,14 @@ class CodecMetaClassSupport {
             }
         }
 
-        addMetaMethod(targetMetaClasses, encodeMethodName, encoderClosure)
+        addMetaMethod(targetMetaClasses, encodeMethodName, encoderClosure, cacheLookup, codecFactory)
         if (codecFactory.encoder) {
-            addAliasMetaMethods(targetMetaClasses, codecFactory.encoder.codecIdentifier.codecAliases, encodeMethodNameClosure, encoderClosure)
+            addAliasMetaMethods(targetMetaClasses, codecFactory.encoder.codecIdentifier.codecAliases, encodeMethodNameClosure, encoderClosure, cacheLookup, codecFactory)
         }
 
-        addMetaMethod(targetMetaClasses, decodeMethodName, decoderClosure)
+        addMetaMethod(targetMetaClasses, decodeMethodName, decoderClosure, cacheLookup, codecFactory)
         if (codecFactory.decoder) {
-            addAliasMetaMethods(targetMetaClasses, codecFactory.decoder.codecIdentifier.codecAliases, decodeMethodNameClosure, decoderClosure)
+            addAliasMetaMethods(targetMetaClasses, codecFactory.decoder.codecIdentifier.codecAliases, decodeMethodNameClosure, decoderClosure, cacheLookup, codecFactory)
         }
     }
 
@@ -130,12 +136,18 @@ class CodecMetaClassSupport {
 
     @CompileStatic
     private addAliasMetaMethods(List<ExpandoMetaClass> targetMetaClasses, Set<String> aliases, Closure<String> methodNameClosure, Closure methodClosure) {
+        addAliasMetaMethods(targetMetaClasses, aliases, methodNameClosure, methodClosure, false, null)
+    }
+
+    @CompileStatic
+    private addAliasMetaMethods(List<ExpandoMetaClass> targetMetaClasses, Set<String> aliases, Closure<String> methodNameClosure, Closure methodClosure,
+            boolean cacheLookup, CodecFactory codecFactory) {
         aliases?.each { String aliasName ->
-            addMetaMethod(targetMetaClasses, methodNameClosure(aliasName), methodClosure)
+            addMetaMethod(targetMetaClasses, methodNameClosure(aliasName), methodClosure, cacheLookup, codecFactory)
         }
     }
 
-    private String resolveCodecName(CodecFactory codecFactory) {
+    private static String resolveCodecName(CodecFactory codecFactory) {
         codecFactory.encoder?.codecIdentifier?.codecName ?: codecFactory.decoder?.codecIdentifier?.codecName
     }
 
@@ -152,8 +164,102 @@ class CodecMetaClassSupport {
     }
 
     protected void addMetaMethod(List<ExpandoMetaClass> targetMetaClasses, String methodName, Closure closure) {
+        addMetaMethod(targetMetaClasses, methodName, closure, false, null)
+    }
+
+    protected void addMetaMethod(List<ExpandoMetaClass> targetMetaClasses, String methodName, Closure closure, boolean cacheLookup, CodecFactory codecFactory) {
         targetMetaClasses.each { ExpandoMetaClass emc ->
-            emc."${methodName}" << closure
+            if (!cacheLookup) {
+                emc."${methodName}" << closure
+                META_METHOD_REGISTRATION_COUNT.incrementAndGet()
+            }
+            else {
+                synchronized (emc) {
+                    if (shouldRegisterMetaMethod(emc, methodName, codecFactory)) {
+                        emc."${methodName}" << closure
+                        META_METHOD_REGISTRATION_COUNT.incrementAndGet()
+                    }
+                }
+            }
+        }
+    }
+
+    @CompileStatic
+    protected static long getMetaMethodRegistrationCount() {
+        META_METHOD_REGISTRATION_COUNT.get()
+    }
+
+    @CompileStatic
+    protected static int getMetaMethodRegistrationKeyCount() {
+        REGISTERED_META_METHODS.size()
+    }
+
+    @CompileStatic
+    protected static void clearMetaMethodRegistrationState() {
+        REGISTERED_META_METHODS.clear()
+        META_METHOD_REGISTRATION_COUNT.set(0L)
+    }
+
+    @CompileStatic
+    private static boolean shouldRegisterMetaMethod(ExpandoMetaClass emc, String methodName, CodecFactory codecFactory) {
+        removeStaleMetaMethodRegistrationKeys()
+        MetaMethodRegistrationKey key = registrationKey(emc, methodName, codecFactory)
+        REGISTERED_META_METHODS.add(key) || emc.getMetaMethod(methodName, EMPTY_ARGS) == null
+    }
+
+    @CompileStatic
+    private static void removeStaleMetaMethodRegistrationKeys() {
+        REGISTERED_META_METHODS.removeIf { MetaMethodRegistrationKey key -> key.stale }
+    }
+
+    @CompileStatic
+    private static MetaMethodRegistrationKey registrationKey(ExpandoMetaClass emc, String methodName, CodecFactory codecFactory) {
+        new MetaMethodRegistrationKey(emc.getTheClass().getName(), methodName, codecFactory)
+    }
+
+    @CompileStatic
+    private static class MetaMethodRegistrationKey {
+
+        private final String targetClassName
+        private final String methodName
+        private final WeakReference<CodecFactory> codecFactoryReference
+        private final int codecFactoryIdentityHashCode
+
+        MetaMethodRegistrationKey(String targetClassName, String methodName, CodecFactory codecFactory) {
+            this.targetClassName = targetClassName
+            this.methodName = methodName
+            this.codecFactoryReference = new WeakReference<CodecFactory>(codecFactory)
+            this.codecFactoryIdentityHashCode = System.identityHashCode(codecFactory)
+        }
+
+        @Override
+        boolean equals(Object other) {
+            if (this.is(other)) {
+                return true
+            }
+            if (!(other instanceof MetaMethodRegistrationKey)) {
+                return false
+            }
+
+            MetaMethodRegistrationKey otherKey = (MetaMethodRegistrationKey) other
+            CodecFactory codecFactory = codecFactoryReference.get()
+            CodecFactory otherCodecFactory = otherKey.codecFactoryReference.get()
+            codecFactory != null &&
+                    otherCodecFactory != null &&
+                    codecFactory.is(otherCodecFactory) &&
+                    targetClassName == otherKey.targetClassName &&
+                    methodName == otherKey.methodName
+        }
+
+        @Override
+        int hashCode() {
+            int result = targetClassName.hashCode()
+            result = 31 * result + methodName.hashCode()
+            31 * result + codecFactoryIdentityHashCode
+        }
+
+        boolean isStale() {
+            codecFactoryReference.get() == null
         }
     }
 }
diff --git a/grails-encoder/src/test/groovy/org/grails/encoder/CodecMetaClassSupportSpec.groovy b/grails-encoder/src/test/groovy/org/grails/encoder/CodecMetaClassSupportSpec.groovy
new file mode 100644
index 0000000000..66ca3d1dd4
--- /dev/null
+++ b/grails-encoder/src/test/groovy/org/grails/encoder/CodecMetaClassSupportSpec.groovy
@@ -0,0 +1,283 @@
+/*
+ *  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.encoder
+
+import java.lang.ref.WeakReference
+import java.util.concurrent.Callable
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+
+import org.codehaus.groovy.runtime.InvokerHelper
+
+import grails.util.GrailsMetaClassUtils
+import spock.lang.Specification
+
+class CodecMetaClassSupportSpec extends Specification {
+
+    void setup() {
+        CodecMetaClassSupport.clearMetaMethodRegistrationState()
+    }
+
+    void cleanup() {
+        GroovySystem.metaClassRegistry.removeMetaClass(CodecMetaClassSupportSpecTarget)
+        CodecMetaClassSupport.clearMetaMethodRegistrationState()
+    }
+
+    void 'cached codec registration is idempotent for the same codec factory'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            BenchmarkCodecFactory codecFactory = new BenchmarkCodecFactory()
+            List<ExpandoMetaClass> targetMetaClasses = [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)]
+
+        when:
+            long baselineRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            support.configureCodecMethods(codecFactory, true, targetMetaClasses)
+            long firstRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object firstResult = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBenchmark', null)
+            support.configureCodecMethods(codecFactory, true, targetMetaClasses)
+            long secondRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object secondResult = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBenchmark', null)
+
+        then:
+            firstResult == 'a&amp;b'
+            secondResult == 'a&amp;b'
+            firstRegistrationCount > baselineRegistrationCount
+            secondRegistrationCount == firstRegistrationCount
+    }
+
+    void 'cached codec registration is idempotent for decoder and aliases'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            BenchmarkCodecFactory codecFactory = new BenchmarkCodecFactory()
+            List<ExpandoMetaClass> targetMetaClasses = [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)]
+
+        when:
+            support.configureCodecMethods(codecFactory, true, targetMetaClasses)
+            long firstRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object encodedAlias = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBench', null)
+            Object decoded = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&amp;b'), 'decodeBenchmark', null)
+            Object decodedAlias = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&amp;b'), 'decodeBench', null)
+            support.configureCodecMethods(codecFactory, true, targetMetaClasses)
+            long secondRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+
+        then:
+            encodedAlias == 'a&amp;b'
+            decoded == 'a&b'
+            decodedAlias == 'a&b'
+            firstRegistrationCount == 4L
+            secondRegistrationCount == firstRegistrationCount
+    }
+
+    void 'cached codec registration is atomic for concurrent same factory registration'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            BenchmarkCodecFactory codecFactory = new BenchmarkCodecFactory()
+            List<ExpandoMetaClass> targetMetaClasses = [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)]
+            CountDownLatch start = new CountDownLatch(1)
+            ExecutorService executor = Executors.newFixedThreadPool(8)
+            List<Future<Object>> futures = (1..8).collect {
+                executor.submit({
+                    start.await()
+                    support.configureCodecMethods(codecFactory, true, targetMetaClasses)
+                    null
+                } as Callable<Object>)
+            }
+
+        when:
+            start.countDown()
+            futures*.get(10L, TimeUnit.SECONDS)
+
+        then:
+            CodecMetaClassSupport.metaMethodRegistrationCount == 4L
+
+        cleanup:
+            executor.shutdownNow()
+    }
+
+    void 'cached codec registration re-adds methods after metaclass replacement'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            BenchmarkCodecFactory codecFactory = new BenchmarkCodecFactory()
+
+        when:
+            long baselineRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            support.configureCodecMethods(codecFactory, true, [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)])
+            long firstRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object firstResult = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBenchmark', null)
+            GroovySystem.metaClassRegistry.removeMetaClass(CodecMetaClassSupportSpecTarget)
+            support.configureCodecMethods(codecFactory, true, [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)])
+            long secondRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object secondResult = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBenchmark', null)
+
+        then:
+            firstResult == 'a&amp;b'
+            secondResult == 'a&amp;b'
+            firstRegistrationCount > baselineRegistrationCount
+            secondRegistrationCount > firstRegistrationCount
+    }
+
+    void 'cached codec registration keeps distinct codec factories isolated'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            List<ExpandoMetaClass> targetMetaClasses = [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)]
+
+        when:
+            long baselineRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            support.configureCodecMethods(new BenchmarkCodecFactory(), true, targetMetaClasses)
+            long firstRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object firstResult = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBenchmark', null)
+            support.configureCodecMethods(new BenchmarkCodecFactory(), true, targetMetaClasses)
+            long secondRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            Object secondResult = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'encodeAsBenchmark', null)
+
+        then:
+            firstResult == 'a&amp;b'
+            secondResult == 'a&amp;b'
+            firstRegistrationCount > baselineRegistrationCount
+            secondRegistrationCount > firstRegistrationCount
+    }
+
+    void 'cached codec registration prunes stale factory registration keys'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            List<ExpandoMetaClass> targetMetaClasses = [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)]
+
+        when:
+            support.configureCodecMethods(new BenchmarkCodecFactory(), true, targetMetaClasses)
+            clearRegisteredCodecFactoryReferences()
+            support.configureCodecMethods(new BenchmarkCodecFactory(), true, targetMetaClasses)
+
+        then:
+            metaMethodRegistrationKeyCount() == 4
+            CodecMetaClassSupport.metaMethodRegistrationCount == 8L
+    }
+
+    void 'non-cached codec registration keeps development reload behavior'() {
+        given:
+            CodecMetaClassSupport support = new CodecMetaClassSupport()
+            BenchmarkCodecFactory codecFactory = new BenchmarkCodecFactory()
+            List<ExpandoMetaClass> targetMetaClasses = [GrailsMetaClassUtils.getExpandoMetaClass(CodecMetaClassSupportSpecTarget)]
+
+        when:
+            long baselineRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            support.configureCodecMethods(codecFactory, false, targetMetaClasses)
+            long firstRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+            support.configureCodecMethods(codecFactory, false, targetMetaClasses)
+            long secondRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+
+        then:
+            firstRegistrationCount > baselineRegistrationCount
+            secondRegistrationCount > firstRegistrationCount
+    }
+
+    private static Set registeredMetaMethodKeys() {
+        def field = CodecMetaClassSupport.getDeclaredField('REGISTERED_META_METHODS')
+        field.accessible = true
+        (Set) field.get(null)
+    }
+
+    private static int metaMethodRegistrationKeyCount() {
+        CodecMetaClassSupport.metaMethodRegistrationKeyCount
+    }
+
+    private static void clearRegisteredCodecFactoryReferences() {
+        registeredMetaMethodKeys().each { Object key ->
+            def field = key.getClass().getDeclaredField('codecFactoryReference')
+            field.accessible = true
+            ((WeakReference) field.get(key)).clear()
+        }
+    }
+
+    private static class CodecMetaClassSupportSpecTarget {
+
+        private final String value
+
+        CodecMetaClassSupportSpecTarget(String value) {
+            this.value = value
+        }
+
+        @Override
+        String toString() {
+            value
+        }
+    }
+
+    private static class BenchmarkCodecFactory implements CodecFactory {
+
+        private final BenchmarkEncoder encoder = new BenchmarkEncoder()
+        private final BenchmarkDecoder decoder = new BenchmarkDecoder()
+
+        @Override
+        Encoder getEncoder() {
+            encoder
+        }
+
+        @Override
+        Decoder getDecoder() {
+            decoder
+        }
+    }
+
+    private static class BenchmarkEncoder implements Encoder {
+
+        private final CodecIdentifier codecIdentifier = new DefaultCodecIdentifier('Benchmark', 'Bench')
+
+        @Override
+        CodecIdentifier getCodecIdentifier() {
+            codecIdentifier
+        }
+
+        @Override
+        Object encode(Object o) {
+            o?.toString()?.replace('&', '&amp;')
+        }
+
+        @Override
+        void markEncoded(CharSequence string) {
+        }
+
+        @Override
+        boolean isSafe() {
+            true
+        }
+
+        @Override
+        boolean isApplyToSafelyEncoded() {
+            false
+        }
+    }
+
+    private static class BenchmarkDecoder implements Decoder {
+
+        private final CodecIdentifier codecIdentifier = new DefaultCodecIdentifier('Benchmark', 'Bench')
+
+        @Override
+        CodecIdentifier getCodecIdentifier() {
+            codecIdentifier
+        }
+
+        @Override
+        Object decode(Object o) {
+            o?.toString()?.replace('&amp;', '&')
+        }
+    }
+}
diff --git a/grails-test-suite-uber/src/test/groovy/org/grails/commons/DefaultGrailsCodecClassTests.groovy b/grails-test-suite-uber/src/test/groovy/org/grails/commons/DefaultGrailsCodecClassTests.groovy
index eea6cba1f6..9fed69dd5e 100644
--- a/grails-test-suite-uber/src/test/groovy/org/grails/commons/DefaultGrailsCodecClassTests.groovy
+++ b/grails-test-suite-uber/src/test/groovy/org/grails/commons/DefaultGrailsCodecClassTests.groovy
@@ -18,6 +18,8 @@
  */
 package org.grails.commons
 
+import org.codehaus.groovy.runtime.GStringImpl
+import org.codehaus.groovy.runtime.InvokerHelper
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
@@ -37,6 +39,9 @@ class DefaultGrailsCodecClassTests {
 
     @AfterEach
     protected void tearDown() {
+        [String, GStringImpl, StringBuffer, StringBuilder, Object].each { Class targetClass ->
+            GroovySystem.metaClassRegistry.removeMetaClass(targetClass)
+        }
         ExpandoMetaClass.disableGlobally()
     }
 
@@ -55,6 +60,17 @@ class DefaultGrailsCodecClassTests {
         assertEquals "encoded", codecClass.encoder.encode("stuff")
         assertEquals "decoded", codecClass.decoder.decode("stuff")
     }
+
+    @Test
+    void testConfigureCodecMethodsRegistersDynamicMethodsThroughPublicCodecClass() {
+        def codecClass = new DefaultGrailsCodecClass(CodecWithClosuresCodec)
+
+        codecClass.configureCodecMethods()
+        codecClass.configureCodecMethods()
+
+        assertEquals 'encoded', InvokerHelper.invokeMethod('stuff', 'encodeAsCodecWithClosures', null)
+        assertEquals 'decoded', InvokerHelper.invokeMethod('stuff', 'decodeCodecWithClosures', null)
+    }
 }
 
 class CodecWithClosuresCodec {