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&b'
+ secondResult == 'a&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&b'), 'decodeBenchmark', null)
+ Object decodedAlias = InvokerHelper.invokeMethod(new CodecMetaClassSupportSpecTarget('a&b'), 'decodeBench', null)
+ support.configureCodecMethods(codecFactory, true, targetMetaClasses)
+ long secondRegistrationCount = CodecMetaClassSupport.metaMethodRegistrationCount
+
+ then:
+ encodedAlias == 'a&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&b'
+ secondResult == 'a&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&b'
+ secondResult == 'a&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('&', '&')
+ }
+
+ @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('&', '&')
+ }
+ }
+}
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 {