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

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/main by this push:
     new e98123755f2 CAUSEWAY-3883: re-implements ProxyFactoryService
e98123755f2 is described below

commit e98123755f20463b3d8c5a2f4e2a3f7dbfef74ff
Author: Andi Huber <[email protected]>
AuthorDate: Wed Jun 25 17:16:36 2025 +0200

    CAUSEWAY-3883: re-implements ProxyFactoryService
---
 .../commons/internal/debug/_MemoryUsage.java       |  40 ++-----
 .../internal/proxy/CachableInvocationHandler.java  |  30 +++++
 .../internal/proxy/CachingProxyFactoryService.java |  79 +++++++++++++
 .../{_ProxyFactory.java => ProxyFactory.java}      |  12 +-
 ...actoryService.java => ProxyFactoryService.java} |  48 +++++---
 .../proxy/_ProxyFactoryServiceAbstract.java        |  68 -----------
 .../services/ProxyFactoryServiceByteBuddy.java     | 127 ++++++++++-----------
 .../classsubstitutor/ClassSubstitutorAbstract.java |   4 +-
 .../runtime/wrap/WrapperInvocationHandler.java     |  40 +++----
 .../causeway/core/runtime/wrap/WrappingObject.java |   2 +-
 .../wrapper/WrapperFactoryDefault.java             |  37 +++---
 .../handlers/DomainObjectInvocationHandler.java    |  31 ++---
 .../wrapper/handlers/ProxyGenerator.java           |  64 +++++------
 .../RuntimeServicesTestAbstract.java               |   3 +-
 .../wrapper/WrapperFactoryDefaultTest.java         |   4 +-
 .../ProxyCreatorTestUsingCodegenPlugin.java        |  70 +++---------
 .../WrapperFactoryMetaspaceMemoryLeakTest.java     |  62 ++++++----
 17 files changed, 373 insertions(+), 348 deletions(-)

diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java
index ab6958541a1..255a8794e9c 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java
@@ -18,11 +18,10 @@
  */
 package org.apache.causeway.commons.internal.debug;
 
-import lombok.SneakyThrows;
-
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryPoolMXBean;
-import java.util.concurrent.Callable;
+
+import org.apache.causeway.commons.functional.ThrowingRunnable;
 
 /**
  * <h1>- internal use only -</h1>
@@ -35,36 +34,25 @@
  * @since 3.4.0
  */
 public record _MemoryUsage(long usedInKibiBytes) {
-    
+
     // -- UTILITIES
-    
+
     static int indent = 0;
 
-    @SneakyThrows
-    public static <T> T measureMetaspace(final String desc, final Callable<T> 
runnable) {
-        var before = metaspace();
-        try {
-            indent++;
-            return runnable.call();
-        } finally {
-            var after = metaspace();
-            System.out.printf("%s%s : %s%n", spaces(indent), 
after.minus(before), desc);
-            indent--;
-        }
-    }
-    
-    public static void measureMetaspace(String desc, final Runnable runnable) {
+    public static _MemoryUsage measureMetaspace(final ThrowingRunnable 
runnable) {
         var before = metaspace();
+        var after = (_MemoryUsage) null;
         try {
             runnable.run();
+        } catch (Throwable e) {
         } finally {
-            var after = metaspace();
-            System.out.printf("%s : %s%n", after.minus(before), desc);
+            after = metaspace();
         }
+        return after.minus(before);
     }
-    
+
     // -- FACTORY
-    
+
     private static _MemoryUsage metaspace() {
         for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) 
{
             if (pool.getName().contains("Metaspace")) {
@@ -73,7 +61,7 @@ private static _MemoryUsage metaspace() {
         }
         throw new RuntimeException("Metaspace Usage not found");
     }
-    
+
     // -- NON CANONICAL CONSTRUCTOR
 
     private _MemoryUsage(java.lang.management.MemoryUsage usage) {
@@ -87,10 +75,6 @@ public String toString() {
 
     // -- HELPER
 
-    private static String spaces(int indent) {
-        return " ".repeat(indent * 2);
-    }
-
     private _MemoryUsage minus(_MemoryUsage before) {
         return new _MemoryUsage(this.usedInKibiBytes - before.usedInKibiBytes);
     }
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/CachableInvocationHandler.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/CachableInvocationHandler.java
new file mode 100644
index 00000000000..ad70b6e6154
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/CachableInvocationHandler.java
@@ -0,0 +1,30 @@
+/*
+ *  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
+ *
+ *        http://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.apache.causeway.commons.internal.proxy;
+
+import java.lang.reflect.InvocationHandler;
+
+public interface CachableInvocationHandler extends InvocationHandler {
+
+    /**
+     * key under which this {@link InvocationHandler} can be put into a cache
+     */
+    String key();
+
+}
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/CachingProxyFactoryService.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/CachingProxyFactoryService.java
new file mode 100644
index 00000000000..2d2141ae1fe
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/CachingProxyFactoryService.java
@@ -0,0 +1,79 @@
+/*
+ *  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
+ *
+ *        http://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.apache.causeway.commons.internal.proxy;
+
+import java.lang.reflect.InvocationHandler;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Partly implements {@link ProxyFactoryService} and adds caching.
+ *
+ * @since 3.4
+ */
+public abstract class CachingProxyFactoryService implements 
ProxyFactoryService {
+
+    private final Map<Class<?>, ProxyFactory<?>> proxyFactoryCache =
+            Collections.synchronizedMap(new WeakHashMap<>());
+    private final Map<String, Class<?>> proxyClassCache =
+            Collections.synchronizedMap(new WeakHashMap<>());
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public final <T> Class<? extends T> proxyClass(
+            InvocationHandler handler,
+            Class<T> base,
+            Class<?>[] interfaces,
+            @Nullable List<AdditionalField> additionalFields) {
+        var proxyClass = handler instanceof CachableInvocationHandler 
cachableInvocationHandler
+                ? 
proxyClassCache.computeIfAbsent(cachableInvocationHandler.key(), __->
+                    createProxyClass(handler, base, interfaces, 
additionalFields))
+                : createProxyClass(handler, base, interfaces, 
additionalFields);
+        return (Class<? extends T>) proxyClass;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public final <T> ProxyFactory<T> factory(
+            Class<T> proxyClass,
+            @Nullable Class<?>[] constructorArgTypes) {
+
+        var factory = proxyFactoryCache.computeIfAbsent(proxyClass, __->
+            createFactory(proxyClass, constructorArgTypes));
+
+        return (ProxyFactory<T>) factory;
+    }
+
+    // -- ABSTRACT METHODS
+
+    protected abstract <T> Class<? extends T> createProxyClass(
+            InvocationHandler handler,
+            Class<T> base,
+            Class<?>[] interfaces,
+            @Nullable List<AdditionalField> additionalFields);
+
+    protected abstract <T> ProxyFactory<T> createFactory(
+            Class<T> proxyClass,
+            @Nullable Class<?>[] constructorArgTypes);
+
+}
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactory.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/ProxyFactory.java
similarity index 85%
rename from 
commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactory.java
rename to 
commons/src/main/java/org/apache/causeway/commons/internal/proxy/ProxyFactory.java
index 7a1462540c4..3fa5d42694f 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactory.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/ProxyFactory.java
@@ -18,34 +18,28 @@
  */
 package org.apache.causeway.commons.internal.proxy;
 
-import java.lang.reflect.InvocationHandler;
-
 /**
  * Generates dynamic classes and corresponding instances by rebasing a given 
'base' class.
  *
  * @since 2.0
  * @param <T> type of proxy objects this factory creates
  */
-public interface _ProxyFactory<T> {
-
-    // -- INTERFACE
+public interface ProxyFactory<T> {
 
     /**
-     * @param handler
      * @param initialize whether to call a constructor on object instantiation
      * @return new proxy instance of type T
      * @throws IllegalArgumentException when using initialize=true and the 
number of
      * constructorArgTypes specified while building this factory is greater 
than 0.
      */
-    public T createInstance(InvocationHandler handler, boolean initialize);
+    public T createInstance(boolean initialize);
 
     /**
-     * @param handler
      * @param constructorArgs passed to the constructor with matching 
signature on object instantiation
      * @return new proxy instance of type T (always uses a constructor)
      * @throws IllegalArgumentException if constructorArgs is null or empty or 
does not
      * conform to the number of constructorArgTypes specified while building 
this factory.
      */
-    public T createInstance(InvocationHandler handler, Object[] 
constructorArgs);
+    public T createInstance(Object[] constructorArgs);
 
 }
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/ProxyFactoryService.java
similarity index 57%
rename from 
commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java
rename to 
commons/src/main/java/org/apache/causeway/commons/internal/proxy/ProxyFactoryService.java
index 4cc5fa8fb2e..1478f7b3e9d 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryService.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/ProxyFactoryService.java
@@ -18,28 +18,46 @@
  */
 package org.apache.causeway.commons.internal.proxy;
 
+import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Type;
 import java.util.List;
 
 import org.jspecify.annotations.Nullable;
 
+import org.apache.causeway.commons.internal.collections._Arrays;
+
 /**
- * Internal service, that generates {@link _ProxyFactory}(s). 
- * 
- * @since 2.0
+ * Internal service, that generates {@link ProxyFactory}(s).
+ *
+ * @since 3.4
  */
-public interface _ProxyFactoryService {
+public interface ProxyFactoryService {
 
-    <T> _ProxyFactory<T> factory(
+    <T> Class<? extends T> proxyClass(
+            InvocationHandler handler,
             Class<T> base,
-            @Nullable Class<?>[] interfaces,
-            @Nullable List<AdditionalField> additionalFields,
-            @Nullable Class<?>[] constructorArgTypes);
+            Class<?>[] interfaces,
+            @Nullable List<AdditionalField> additionalFields);
 
-    <T> _ProxyFactory<T> factory(
-            Class<T> toProxyClass, 
+    default <T> Class<? extends T> proxyClass(
+            InvocationHandler handler,
+            Class<T> classToBeProxied,
             @Nullable Class<?> additionalClass,
-            @Nullable List<AdditionalField> additionalFields);
+            @Nullable List<AdditionalField> additionalFields) {
+        final Class<?>[] interfaces = additionalClass!=null
+                ? _Arrays.combine(classToBeProxied.getInterfaces(), 
ProxyEnhanced.class, additionalClass)
+                : _Arrays.combine(classToBeProxied.getInterfaces(), 
ProxyEnhanced.class);
+        return proxyClass(handler, classToBeProxied, interfaces, 
additionalFields);
+    }
+
+    <T> ProxyFactory<T> factory(
+            Class<T> proxyClass,
+            @Nullable Class<?>[] constructorArgTypes);
+
+    default <T> ProxyFactory<T> factory(
+            Class<T> proxyClass) {
+        return factory(proxyClass, null);
+    }
 
     /**
      * Marker interface for entities/services that have been enhanced with
@@ -48,13 +66,15 @@ <T> _ProxyFactory<T> factory(
     interface ProxyEnhanced {
 
     }
-    
+
     /**
      * Defines a field, that can be added to a proxy class.
      */
     record AdditionalField(
-            String name, 
-            Type type, 
+            String name,
+            Type type,
             int modifiers) {
     }
+
+
 }
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryServiceAbstract.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryServiceAbstract.java
deleted file mode 100644
index 13b6a2937d2..00000000000
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/proxy/_ProxyFactoryServiceAbstract.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- *  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
- *
- *        http://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.apache.causeway.commons.internal.proxy;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.WeakHashMap;
-
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.collections._Arrays;
-
-/**
- * Partly implements {@link _ProxyFactoryService} and adds caching.
- * 
- * @since 2.0
- */
-public abstract class _ProxyFactoryServiceAbstract implements 
_ProxyFactoryService {
-
-    @NonNull
-    private final Map<Class<?>, _ProxyFactory<?>> proxyFactoryByClass =
-            Collections.synchronizedMap(new WeakHashMap<>());
-
-    @Override
-    public final <T> _ProxyFactory<T> factory(
-            final Class<T> classToBeProxied, 
-            final @Nullable Class<?> additionalClass,
-            final @Nullable List<AdditionalField> additionalFields) {
-        _ProxyFactory<T> proxyFactory = 
_Casts.uncheckedCast(proxyFactoryByClass.get(classToBeProxied));
-        if(proxyFactory == null) {
-            proxyFactory = createFactory(classToBeProxied, additionalClass, 
additionalFields);
-            proxyFactoryByClass.put(classToBeProxied, proxyFactory);
-        }
-        return proxyFactory;
-    }
-
-    private <T> _ProxyFactory<T> createFactory(
-            final Class<T> classToBeProxied,
-            final @Nullable Class<?> additionalClass, 
-            final @Nullable List<AdditionalField> additionalFields) {
-
-        final Class<?>[] interfaces = additionalClass!=null
-                ? _Arrays.combine(classToBeProxied.getInterfaces(), 
ProxyEnhanced.class, additionalClass)
-                : _Arrays.combine(classToBeProxied.getInterfaces(), 
ProxyEnhanced.class);
-        
-        return factory(classToBeProxied, interfaces, additionalFields, null);
-    }
-
-}
diff --git 
a/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java
 
b/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java
index 5c07d247384..bfd88d49115 100644
--- 
a/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java
+++ 
b/core/codegen-bytebuddy/src/main/java/org/apache/causeway/core/codegen/bytebuddy/services/ProxyFactoryServiceByteBuddy.java
@@ -21,7 +21,6 @@
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.util.List;
-import java.util.function.Function;
 
 import org.jspecify.annotations.Nullable;
 
@@ -32,8 +31,8 @@
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.context._Context;
-import org.apache.causeway.commons.internal.proxy._ProxyFactory;
-import org.apache.causeway.commons.internal.proxy._ProxyFactoryServiceAbstract;
+import org.apache.causeway.commons.internal.proxy.CachingProxyFactoryService;
+import org.apache.causeway.commons.internal.proxy.ProxyFactory;
 
 import net.bytebuddy.ByteBuddy;
 import net.bytebuddy.NamingStrategy;
@@ -42,84 +41,84 @@
 import net.bytebuddy.matcher.ElementMatchers;
 
 @Service
-public class ProxyFactoryServiceByteBuddy extends _ProxyFactoryServiceAbstract 
{
+public class ProxyFactoryServiceByteBuddy extends CachingProxyFactoryService {
 
     private final ClassLoadingStrategyAdvisor strategyAdvisor = new 
ClassLoadingStrategyAdvisor();
 
-    @Override
-    public <T> _ProxyFactory<T> factory(
-            final Class<T> base,
-            final Class<?>[] interfaces,
-            @Nullable List<AdditionalField> additionalFields,
-            final Class<?>[] constructorArgTypes) {
+    private record ProxyFactoryByteBuddy<T>(
+            Class<T> proxyClass,
+            Class<?>[] constructorArgTypes,
+            ObjenesisStd objenesis) implements ProxyFactory<T> {
 
-        var objenesis = new ObjenesisStd();
+        @Override public T createInstance(final boolean initialize) {
 
-        final Function<InvocationHandler, Class<? extends T>> 
proxyClassFactory = handler->{
-            var def = proxyDef(base, interfaces, additionalFields);
-            return def
-                    .intercept(InvocationHandlerAdapter.of(handler))
-                    .make()
-                    .load(_Context.getDefaultClassLoader(),
-                            strategyAdvisor.getSuitableStrategy(base))
-                    .getLoaded();
-        };
+            try {
 
-        return new _ProxyFactory<T>() {
+                if(initialize) {
+                    ensureSameSize(constructorArgTypes, null);
+                    return _Casts.uncheckedCast( createUsingConstructor(null) 
);
+                } else {
+                    return _Casts.uncheckedCast( createNotUsingConstructor() );
+                }
 
-            @Override
-            public T createInstance(final InvocationHandler handler, final 
boolean initialize) {
+            } catch (NoSuchMethodException | IllegalArgumentException | 
InstantiationException |
+                    IllegalAccessException | InvocationTargetException e) {
+                throw new RuntimeException(e);
+            }
 
-                try {
+        }
 
-                    if(initialize) {
-                        ensureSameSize(constructorArgTypes, null);
-                        return _Casts.uncheckedCast( 
createUsingConstructor(handler, null) );
-                    } else {
-                        return _Casts.uncheckedCast( 
createNotUsingConstructor(handler) );
-                    }
+        @Override public T createInstance(final Object[] constructorArgs) {
 
-                } catch (NoSuchMethodException | IllegalArgumentException | 
InstantiationException |
-                        IllegalAccessException | InvocationTargetException e) {
-                    throw new RuntimeException(e);
-                }
+            ensureNonEmtpy(constructorArgs);
+            ensureSameSize(constructorArgTypes, constructorArgs);
 
+            try {
+                return _Casts.uncheckedCast( 
createUsingConstructor(constructorArgs) );
+            } catch (NoSuchMethodException | InstantiationException | 
IllegalAccessException |
+                    IllegalArgumentException | InvocationTargetException | 
SecurityException  e) {
+                throw new RuntimeException(e);
             }
+        }
 
-            @Override
-            public T createInstance(final InvocationHandler handler, final 
Object[] constructorArgs) {
-
-                ensureNonEmtpy(constructorArgs);
-                ensureSameSize(constructorArgTypes, constructorArgs);
+        // -- HELPER (create w/o initialize)
 
-                try {
-                    return _Casts.uncheckedCast( 
createUsingConstructor(handler, constructorArgs) );
-                } catch (NoSuchMethodException | InstantiationException | 
IllegalAccessException |
-                        IllegalArgumentException | InvocationTargetException | 
SecurityException  e) {
-                    throw new RuntimeException(e);
-                }
-            }
+        private Object createNotUsingConstructor() {
+            final Object object = objenesis.newInstance(proxyClass);
+            return object;
+        }
 
-            // -- HELPER (create w/o initialize)
+        // -- HELPER (create with initialize)
 
-            private Object createNotUsingConstructor(final InvocationHandler 
invocationHandler) {
-                final Class<? extends T> proxyClass = 
proxyClassFactory.apply(invocationHandler);
-                final Object object = objenesis.newInstance(proxyClass);
-                return object;
-            }
+        private Object createUsingConstructor(final @Nullable Object[] 
constructorArgs)
+                throws InstantiationException, IllegalAccessException, 
IllegalArgumentException, InvocationTargetException, NoSuchMethodException, 
SecurityException {
+            return proxyClass
+                    .getConstructor(constructorArgTypes==null ? 
_Constants.emptyClasses : constructorArgTypes)
+                    .newInstance(constructorArgs==null ? 
_Constants.emptyObjects : constructorArgs);
+        }
 
-            // -- HELPER (create with initialize)
+    };
 
-            private Object createUsingConstructor(final InvocationHandler 
invocationHandler, final @Nullable Object[] constructorArgs)
-                    throws InstantiationException, IllegalAccessException, 
IllegalArgumentException, InvocationTargetException, NoSuchMethodException, 
SecurityException {
-                final Class<? extends T> proxyClass = 
proxyClassFactory.apply(invocationHandler);
-                return proxyClass
-                        .getConstructor(constructorArgTypes==null ? 
_Constants.emptyClasses : constructorArgTypes)
-                        .newInstance(constructorArgs==null ? 
_Constants.emptyObjects : constructorArgs);
-            }
+    @Override
+    public <T> Class<? extends T> createProxyClass(
+            final InvocationHandler handler,
+            final Class<T> base,
+            final Class<?>[] interfaces,
+            @Nullable List<AdditionalField> additionalFields) {
 
-        };
+        return proxyDef(base, interfaces, additionalFields)
+                .intercept(InvocationHandlerAdapter.of(handler))
+                .make()
+                .load(_Context.getDefaultClassLoader(),
+                        strategyAdvisor.getSuitableStrategy(base))
+                .getLoaded();
+    }
 
+    @Override
+    public <T> ProxyFactory<T> createFactory(
+            final Class<T> proxyClass,
+            final Class<?>[] constructorArgTypes) {
+        return new ProxyFactoryByteBuddy<T>(proxyClass, constructorArgTypes, 
new ObjenesisStd());
     }
 
     // -- HELPER
@@ -127,19 +126,19 @@ private Object createUsingConstructor(final 
InvocationHandler invocationHandler,
     /**
      * @implNote could not find a simple way to use the ByteBuddy builder
      *      to add zero, one or multiple additional fields via {@code 
defineField},
-     *      so we do those 3 cases conditionally all picking up on a shared 
+     *      so we do those 3 cases conditionally all picking up on a shared
      *      {@code prolog}
      */
     private static <T> ImplementationDefinition<T> proxyDef(
             final Class<T> base,
             final Class<?>[] interfaces,
             @Nullable List<AdditionalField> additionalFields) {
-        
+
         var prolog = new ByteBuddy()
                 .with(new NamingStrategy.SuffixingRandom("bb"))
                 .subclass(base)
                 .implement(interfaces);
-        
+
         int additionalFieldCount = _NullSafe.size(additionalFields);
         if(additionalFieldCount==0) {
             return prolog
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java
index db92c914163..0bb4c1e2b28 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/classsubstitutor/ClassSubstitutorAbstract.java
@@ -23,7 +23,7 @@
 
 import org.apache.causeway.commons.internal.collections._Sets;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.proxy._ProxyFactoryService;
+import org.apache.causeway.commons.internal.proxy.ProxyFactoryService;
 import org.apache.causeway.commons.internal.reflection._ClassCache;
 import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
 import org.apache.causeway.core.metamodel.commons.ClassUtil;
@@ -74,7 +74,7 @@ protected Class<?> getReplacement(final Class<?> cls) {
         if(superclass != null && superclass.isEnum()) {
             return superclass;
         }
-        if (ClassUtil.directlyImplements(cls, 
_ProxyFactoryService.ProxyEnhanced.class)) {
+        if (ClassUtil.directlyImplements(cls, 
ProxyFactoryService.ProxyEnhanced.class)) {
             // REVIEW: arguably this should now go back to the 
ClassSubstitorRegistry
             return getReplacement(cls.getSuperclass());
         }
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
index 1a087d6420e..fbe759c1843 100644
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
+++ 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrapperInvocationHandler.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.runtime.wrap;
 
-import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.Objects;
 
@@ -28,18 +27,19 @@
 import org.apache.causeway.applib.services.wrapper.control.ExecutionMode;
 import org.apache.causeway.commons.internal._Constants;
 import org.apache.causeway.commons.internal.base._Lazy;
+import org.apache.causeway.commons.internal.proxy.CachableInvocationHandler;
+
+public interface WrapperInvocationHandler extends CachableInvocationHandler {
 
-public interface WrapperInvocationHandler extends InvocationHandler {
-    
     ClassMetaData classMetaData();
-    
+
     Object invoke(WrapperInvocation wrapperInvocation) throws Throwable;
-    
+
     @Override
     default Object invoke(Object target, Method method, Object[] args) throws 
Throwable {
         return invoke(WrapperInvocation.of(target, method, args));
     }
-    
+
     public record ClassMetaData(
             /** underlying class that is to be proxied */
             Class<?> pojoClass,
@@ -47,31 +47,31 @@ public record ClassMetaData(
             Method equalsMethod,
             Method hashCodeMethod,
             Method toStringMethod,
-            
+
             /**
              * The <tt>title()</tt> method; may be <tt>null</tt>.
              */
             @Nullable Method titleMethod) {
-        
+
         /**
          * The <tt>__causeway_origin()</tt> method from {@link 
WrappingObject#__causeway_origin()}.
          */
-        static final _Lazy<Method> __causeway_originMethod = 
_Lazy.threadSafe(()-> 
+        static final _Lazy<Method> __causeway_originMethod = 
_Lazy.threadSafe(()->
                 
WrappingObject.class.getMethod(WrappingObject.ORIGIN_GETTER_NAME, 
_Constants.emptyClasses));
-        
+
         /**
          * The <tt>__causeway_save()</tt> method from {@link 
WrappingObject#__causeway_save()}.
          */
         static final _Lazy<Method> __causeway_saveMethod = 
_Lazy.threadSafe(()->
                 
WrappingObject.class.getMethod(WrappingObject.SAVE_METHOD_NAME, 
_Constants.emptyClasses));
-        
+
         public static ClassMetaData of(
                 final @NonNull Class<?> pojoClass) {
             try {
                 var equalsMethod = pojoClass.getMethod("equals", 
_Constants.classesOfObject);
                 var hashCodeMethod = pojoClass.getMethod("hashCode", 
_Constants.emptyClasses);
                 var toStringMethod = pojoClass.getMethod("toString", 
_Constants.emptyClasses);
-                
+
                 var titleMethod = (Method)null;
                 try {
                     titleMethod = pojoClass.getMethod("title", 
_Constants.emptyClasses);
@@ -80,17 +80,17 @@ public static ClassMetaData of(
                 }
                 return new WrapperInvocationHandler
                         .ClassMetaData(pojoClass, equalsMethod, 
hashCodeMethod, toStringMethod, titleMethod);
-                
+
             } catch (final NoSuchMethodException e) {
                 // ///CLOVER:OFF
                 throw new RuntimeException("An Object method could not be 
found: " + e.getMessage());
                 // ///CLOVER:ON
             }
         }
-        
+
         public boolean isObjectMethod(final Method method) {
-            return toStringMethod().equals(method) 
-                    || hashCodeMethod().equals(method) 
+            return toStringMethod().equals(method)
+                    || hashCodeMethod().equals(method)
                     || equalsMethod().equals(method);
         }
 
@@ -104,7 +104,7 @@ public boolean isSaveMethod(Method method) {
             return method.equals(__causeway_saveMethod.get());
         }
     }
-    
+
     public record WrapperInvocation(
         WrappingObject.@NonNull Origin origin,
         @NonNull Method method,
@@ -112,12 +112,12 @@ public record WrapperInvocation(
 
         static WrapperInvocation of(Object target, Method method, Object[] 
args) {
             Objects.requireNonNull(target);
-            var origin = target instanceof WrappingObject wrappingObject 
+            var origin = target instanceof WrappingObject wrappingObject
                     ? WrappingObject.getOrigin(wrappingObject)
                     : WrappingObject.Origin.fallback(target);
             return new WrapperInvocation(origin, method, args!=null ? args : 
_Constants.emptyObjects);
         }
-        
+
         public boolean shouldEnforceRules() {
             return 
!origin().syncControl().getExecutionModes().contains(ExecutionMode.SKIP_RULE_VALIDATION);
         }
@@ -126,5 +126,5 @@ public boolean shouldExecute() {
             return 
!origin().syncControl().getExecutionModes().contains(ExecutionMode.SKIP_EXECUTION);
         }
     }
-    
+
 }
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
index e4db23562c4..dec7ce86e0b 100644
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
+++ 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/wrap/WrappingObject.java
@@ -25,7 +25,7 @@
 
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
 import org.apache.causeway.applib.services.wrapper.control.SyncControl;
-import 
org.apache.causeway.commons.internal.proxy._ProxyFactoryService.AdditionalField;
+import 
org.apache.causeway.commons.internal.proxy.ProxyFactoryService.AdditionalField;
 import org.apache.causeway.commons.internal.reflection._Reflect;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
index df197befd95..84015204d17 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
@@ -18,6 +18,7 @@
  */
 package org.apache.causeway.core.runtimeservices.wrapper;
 
+import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -83,7 +84,7 @@
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.proxy._ProxyFactoryService;
+import org.apache.causeway.commons.internal.proxy.ProxyFactoryService;
 import org.apache.causeway.commons.internal.reflection._GenericResolver;
 import 
org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
 import 
org.apache.causeway.core.config.progmodel.ProgrammingModelConstants.MixinConstructor;
@@ -128,7 +129,7 @@ public class WrapperFactoryDefault
 
     @Inject private FactoryService factoryService;
     @Inject @Getter(onMethod_= {@Override}) MetaModelContext metaModelContext; 
// HasMetaModelContext
-    @Inject protected _ProxyFactoryService proxyFactoryService; // protected: 
in support of JUnit tests
+    @Inject protected ProxyFactoryService proxyFactoryService; // protected: 
in support of JUnit tests
     @Inject @Lazy private CommandDtoFactory commandDtoFactory;
 
     @Inject private Provider<InteractionService> interactionServiceProvider;
@@ -288,11 +289,7 @@ public <T,R> T asyncWrap(
                     + "use WrapperFactory.asyncWrapMixin(...) instead");
         }
 
-        var proxyFactory = proxyFactoryService
-                .<T>factory(_Casts.uncheckedCast(domainObject.getClass()), 
WrappingObject.class, WrappingObject.ADDITIONAL_FIELDS);
-
-        return proxyFactory.createInstance((proxy, method, args) -> {
-
+        final InvocationHandler handler = (proxy, method, args) -> {
             var resolvedMethod = _GenericResolver.resolveMethod(method, 
domainObject.getClass())
                     .orElseThrow(); // fail early on attempt to invoke method 
that is not part of the meta-model
 
@@ -311,7 +308,14 @@ public <T,R> T asyncWrap(
             }
 
             return submitAsync(memberAndTarget, args, asyncControl);
-        }, false);
+        };
+
+        @SuppressWarnings("unchecked")
+        var proxyClass = proxyFactoryService
+                .proxyClass(handler,
+                        (Class<T>)domainObject.getClass(), 
WrappingObject.class, WrappingObject.ADDITIONAL_FIELDS);
+        var proxyFactory = proxyFactoryService.factory(proxyClass);
+        return proxyFactory.createInstance(false);
     }
 
     @Override
@@ -328,12 +332,7 @@ public <T, R> T asyncWrapMixin(
         var mixinConstructor = 
MixinConstructor.PUBLIC_SINGLE_ARG_RECEIVING_MIXEE
                 .getConstructorElseFail(mixinClass, mixee.getClass());
 
-        var proxyFactory = proxyFactoryService
-                .factory(mixinClass, new Class[]{WrappingObject.class}, 
WrappingObject.ADDITIONAL_FIELDS,
-                        mixinConstructor.getParameterTypes());
-
-        return proxyFactory.createInstance((proxy, method, args) -> {
-
+        final InvocationHandler handler = (proxy, method, args) -> {
             var resolvedMethod = _GenericResolver.resolveMethod(method, 
mixinClass)
                     .orElseThrow(); // fail early on attempt to invoke method 
that is not part of the meta-model
 
@@ -353,7 +352,15 @@ public <T, R> T asyncWrapMixin(
             }
 
             return submitAsync(actionAndTarget, args, asyncControl);
-        }, new Object[]{ mixee });
+        };
+
+        var proxyClass = proxyFactoryService
+            .proxyClass(handler, mixinClass, new 
Class[]{WrappingObject.class}, WrappingObject.ADDITIONAL_FIELDS);
+
+        var proxyFactory = proxyFactoryService
+            .factory(proxyClass, mixinConstructor.getParameterTypes());
+
+        return proxyFactory.createInstance(new Object[]{ mixee });
     }
 
     private boolean isInheritedFromJavaLangObject(final Method method) {
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
index 3badeb16cf0..d9dc132084f 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/DomainObjectInvocationHandler.java
@@ -71,9 +71,11 @@
 final class DomainObjectInvocationHandler
 implements WrapperInvocationHandler {
 
-    @Getter(onMethod_ = {@Override}) @Accessors(fluent=true) 
+    @Getter(onMethod_ = {@Override}) @Accessors(fluent=true)
     private final WrapperInvocationHandler.ClassMetaData classMetaData;
-    
+    @Getter(onMethod_ = {@Override}) @Accessors(fluent=true)
+    private final String key;
+
     private final ProxyGenerator proxyGenerator;
     private final ObjectSpecification targetSpec;
 
@@ -83,15 +85,16 @@ final class DomainObjectInvocationHandler
         this.targetSpec = targetSpec;
         this.classMetaData = 
WrapperInvocationHandler.ClassMetaData.of(targetSpec.getCorrespondingClass());
         this.proxyGenerator = proxyGenerator;
+        this.key = targetSpec.getCorrespondingClass().getName();
     }
 
     @Override
     public Object invoke(WrapperInvocation wrapperInvocation) throws Throwable 
{
-    
+
         final Object target = wrapperInvocation.origin().pojo();
         final Method method = wrapperInvocation.method();
-        final ManagedObject managedMixee = (ManagedObject) 
wrapperInvocation.origin().managedMixee();
-        
+        final ManagedObject managedMixee = 
wrapperInvocation.origin().managedMixee();
+
         if (classMetaData().isObjectMethod(method)
                 || isEnhancedEntityMethod(method)) {
             return method.invoke(target, wrapperInvocation.args());
@@ -165,7 +168,7 @@ public Object invoke(WrapperInvocation wrapperInvocation) 
throws Throwable {
             }
 
             var objectAction = (ObjectAction) objectMember;
-            
+
             if(targetAdapter.objSpec().isMixin()) {
                 if (managedMixee == null) {
                     throw _Exceptions.illegalState(
@@ -236,7 +239,7 @@ private boolean isEnhancedEntityMethod(final Method method) 
{
     }
 
     private Object handleTitleMethod(
-            final WrapperInvocation wrapperInvocation, 
+            final WrapperInvocation wrapperInvocation,
             final ManagedObject targetAdapter) {
 
         var targetNoSpec = targetAdapter.objSpec();
@@ -248,8 +251,8 @@ private Object handleTitleMethod(
     }
 
     private Object handleSaveMethod(
-            final WrapperInvocation wrapperInvocation, 
-            final ManagedObject targetAdapter, 
+            final WrapperInvocation wrapperInvocation,
+            final ManagedObject targetAdapter,
             final ObjectSpecification targetNoSpec) {
 
         runValidationTask(wrapperInvocation, ()->{
@@ -288,8 +291,8 @@ private Object handleGetterMethodOnProperty(
             var currentReferencedObj = 
MmUnwrapUtils.single(currentReferencedAdapter);
 
             targetSpec.getWrapperFactory().notifyListeners(new 
PropertyAccessEvent(
-                    targetAdapter.getPojo(), 
-                    property.getFeatureIdentifier(), 
+                    targetAdapter.getPojo(),
+                    property.getFeatureIdentifier(),
                     currentReferencedObj));
             return currentReferencedObj;
 
@@ -348,12 +351,12 @@ private Object handleGetterMethodOnCollection(
 
             if (currentReferencedObj instanceof Collection) {
                 var collectionViewObject = wrapCollection(
-                        (Collection<?>) currentReferencedObj, 
+                        (Collection<?>) currentReferencedObj,
                         collection);
                 
targetSpec.getWrapperFactory().notifyListeners(collectionAccessEvent);
                 return collectionViewObject;
             } else if (currentReferencedObj instanceof Map) {
-                var mapViewObject = wrapMap( 
+                var mapViewObject = wrapMap(
                         (Map<?, ?>) currentReferencedObj,
                         collection);
                 
targetSpec.getWrapperFactory().notifyListeners(collectionAccessEvent);
@@ -474,7 +477,7 @@ private void checkUsability(
 
     private void notifyListenersAndVetoIfRequired(final InteractionResult 
interactionResult) {
         var interactionEvent = interactionResult.getInteractionEvent();
-        
+
         targetSpec.getWrapperFactory().notifyListeners(interactionEvent);
         if (interactionEvent.isVeto()) {
             throw toException(interactionEvent);
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
index 55e8421968b..85a1e9a4466 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyGenerator.java
@@ -27,7 +27,7 @@
 import org.apache.causeway.applib.services.wrapper.control.SyncControl;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.context._Context;
-import org.apache.causeway.commons.internal.proxy._ProxyFactoryService;
+import org.apache.causeway.commons.internal.proxy.ProxyFactoryService;
 import org.apache.causeway.commons.semantics.CollectionSemantics;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
@@ -35,25 +35,25 @@
 import org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
 
-public record ProxyGenerator(@NonNull _ProxyFactoryService 
proxyFactoryService) {
+public record ProxyGenerator(@NonNull ProxyFactoryService proxyFactoryService) 
{
 
+    @SuppressWarnings("unchecked")
     public <T> T objectProxy(
-        final T domainObject,
-        final ObjectSpecification domainObjectSpec,
-        final SyncControl syncControl) {
-
-        return instantiateProxy(handler(domainObjectSpec), new 
WrappingObject.Origin(domainObject, syncControl));
+            final T domainObject,
+            final ObjectSpecification domainObjectSpec,
+            final SyncControl syncControl) {
+        return (T) instantiateProxy(domainObjectSpec, new 
WrappingObject.Origin(domainObject, syncControl));
     }
 
+    @SuppressWarnings("unchecked")
     public <T> T mixinProxy(
             final T mixin,
             final ManagedObject managedMixee,
             final ObjectSpecification mixinSpec,
             final SyncControl syncControl) {
-    
-        return instantiateProxy(handler(mixinSpec), new 
WrappingObject.Origin(mixin, managedMixee, syncControl));
+        return (T) instantiateProxy(mixinSpec, new 
WrappingObject.Origin(mixin, managedMixee, syncControl));
     }
-    
+
     /**
      * Whether to execute or not will be picked up from the supplied parent
      * handler.
@@ -61,18 +61,18 @@ public <T> T mixinProxy(
     public <T, E> Collection<E> collectionProxy(
             final Collection<E> collectionToBeProxied,
             final OneToManyAssociation otma) {
-    
+
         var collectionInvocationHandler = PluralInvocationHandler
             .forCollection(collectionToBeProxied, otma);
-    
+
         var proxyBase = CollectionSemantics
             .valueOfElseFail(collectionToBeProxied.getClass())
             .getContainerType();
-    
-        return instantiatePluralProxy(_Casts.uncheckedCast(proxyBase), 
+
+        return instantiatePluralProxy(_Casts.uncheckedCast(proxyBase),
                 collectionInvocationHandler);
     }
-    
+
     /**
      * Whether to execute or not will be picked up from the supplied parent
      * handler.
@@ -80,37 +80,33 @@ public <T, E> Collection<E> collectionProxy(
     public <T, P, Q> Map<P, Q> mapProxy(
             final Map<P, Q> mapToBeProxied,
             final OneToManyAssociation otma) {
-    
+
         var proxyBase = Map.class;
-    
-        return instantiatePluralProxy(_Casts.uncheckedCast(proxyBase), 
+
+        return instantiatePluralProxy(_Casts.uncheckedCast(proxyBase),
                 PluralInvocationHandler.forMap(mapToBeProxied, otma));
     }
-    
-    // -- HELPER
 
-    <T> T instantiateProxy(final WrapperInvocationHandler handler, 
WrappingObject.Origin origin) {
-        return 
_Casts.uncheckedCast(instantiateProxy(handler.classMetaData().pojoClass(), 
handler, origin));
-    }
+    // -- HELPER
 
     /**
-     * Creates a proxy, using given {@code base} type as the proxy's base.
-     * @implNote introduced to circumvent access issues on cases,
-     *      where {@code handler.getDelegate().getClass()} is not visible
-     *      (eg. nested private type)
+     * Creates a proxy, using given {@code targetSpec} type as the proxy's 
base.
      */
-    private <T> T instantiateProxy(final Class<T> base, final 
WrapperInvocationHandler handler, WrappingObject.Origin origin) {
-        T proxy = proxyFactoryService
-                .factory(base, WrappingObject.class, 
WrappingObject.ADDITIONAL_FIELDS)
-                .createInstance(handler, false);
+    private Object instantiateProxy(final ObjectSpecification targetSpec, 
WrappingObject.Origin origin) {
+        var proxyClass = proxyFactoryService
+            .proxyClass(handler(targetSpec),
+                    targetSpec.getCorrespondingClass(), WrappingObject.class, 
WrappingObject.ADDITIONAL_FIELDS);
+        var proxy = proxyFactoryService
+                .factory(proxyClass)
+                .createInstance(false);
         return WrappingObject.withOrigin(proxy, origin);
     }
-    
+
     private <T, P> P instantiatePluralProxy(final Class<T> base, final 
PluralInvocationHandler<T, P> pluralInvocationHandler) {
         var proxyWithoutFields = Proxy.newProxyInstance(
                 _Context.getDefaultClassLoader(),
                 new Class<?>[] {base},
-                pluralInvocationHandler); 
+                pluralInvocationHandler);
         return _Casts.uncheckedCast(proxyWithoutFields);
     }
 
@@ -119,5 +115,5 @@ public WrapperInvocationHandler handler(ObjectSpecification 
targetSpec) {
                 targetSpec,
                 this);
     }
-    
+
 }
diff --git 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java
 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java
index d47f60378aa..c526db0d484 100644
--- 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java
+++ 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/RuntimeServicesTestAbstract.java
@@ -63,7 +63,8 @@ public abstract class RuntimeServicesTestAbstract
     @BeforeEach
     final void setUp() throws Exception {
         var mmcBuilder = MetaModelContext_forTesting.builder()
-                .memberExecutor(Mockito.mock(MemberExecutorService.class));
+                .memberExecutor(Mockito.mock(MemberExecutorService.class))
+                ;
 
         // install runtime services into MMC (extend as needed)
 
diff --git 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
index 9331895caca..3c78be6e899 100644
--- 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
+++ 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefaultTest.java
@@ -30,7 +30,7 @@
 
 import org.apache.causeway.applib.services.wrapper.control.ExecutionMode;
 import org.apache.causeway.applib.services.wrapper.control.SyncControl;
-import org.apache.causeway.commons.internal.proxy._ProxyFactoryService;
+import org.apache.causeway.commons.internal.proxy.ProxyFactoryService;
 import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting;
 import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
@@ -73,7 +73,7 @@ public void setUp() throws Exception {
             @Override
             public void init() {
                 this.metaModelContext = mmc;
-                this.proxyFactoryService = 
Mockito.mock(_ProxyFactoryService.class);
+                this.proxyFactoryService = 
Mockito.mock(ProxyFactoryService.class);
                 super.init();
             }
 
diff --git 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
index 4df6e38801f..ea62989b938 100644
--- 
a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
+++ 
b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/wrapper/handlers/ProxyCreatorTestUsingCodegenPlugin.java
@@ -18,35 +18,25 @@
  */
 package org.apache.causeway.core.runtimeservices.wrapper.handlers;
 
-import java.util.HashSet;
-import java.util.Set;
-
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.Nature;
 import org.apache.causeway.applib.services.wrapper.control.SyncControl;
 import 
org.apache.causeway.core.codegen.bytebuddy.services.ProxyFactoryServiceByteBuddy;
-import org.apache.causeway.core.runtime.wrap.WrapperInvocationHandler;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
+import org.apache.causeway.core.runtimeservices.RuntimeServicesTestAbstract;
 
-import lombok.Getter;
-import lombok.Setter;
-
-class ProxyCreatorTestUsingCodegenPlugin {
+class ProxyCreatorTestUsingCodegenPlugin extends RuntimeServicesTestAbstract {
 
-    private ProxyGenerator proxyGenerator;
-
-    @BeforeEach
-    void setUp() throws Exception {
-        proxyGenerator = new ProxyGenerator(new 
ProxyFactoryServiceByteBuddy());
-    }
+    private ProxyGenerator proxyGenerator = new ProxyGenerator(new 
ProxyFactoryServiceByteBuddy());
 
+    @DomainObject(nature = Nature.VIEW_MODEL)
     public static class Employee {
         private String name;
         public String getName() {
@@ -57,48 +47,22 @@ public void setName(final String name) {
         }
     }
 
-    private static class WrapperInvocationHandlerForTest implements 
WrapperInvocationHandler {
-        private final Employee delegate = new Employee();
-        private final Set<String> invoked = new HashSet<>();
-        private final WrapperInvocationHandler.ClassMetaData classMetaData = 
new WrapperInvocationHandler.ClassMetaData(
-                Employee.class, null, null, null, null);
-                
-        @Getter @Setter 
-        private boolean resolveObjectChangedEnabled = false;
-
-        public boolean wasInvoked(final String methodName) {
-            return invoked.contains(methodName);
-        }
-
-        @Override
-        public WrapperInvocationHandler.ClassMetaData classMetaData() {
-            return classMetaData;
-        }
-
-        @Override
-        public Object invoke(WrapperInvocation wrapperInvocation) throws 
Throwable {
-            invoked.add(wrapperInvocation.method().getName());
-            return "hi";
-        }
-        
-    }
-
     @Test
     void proxyShouldDelegateCalls() {
 
-        final WrapperInvocationHandlerForTest handler = new 
WrapperInvocationHandlerForTest();
-        final Employee proxyOfEmployee = 
proxyGenerator.instantiateProxy(handler, new 
WrappingObject.Origin(handler.delegate, SyncControl.control()));
-
-        assertNotNull(proxyOfEmployee);
-
-        assertNotEquals(Employee.class.getName(), 
proxyOfEmployee.getClass().getName());
-
-        assertFalse(handler.wasInvoked("getName"));
+        final Employee employee = new Employee();
+        var employeeSpec = 
getMetaModelContext().getSpecificationLoader().loadSpecification(Employee.class);
 
-        assertEquals("hi", proxyOfEmployee.getName());
+        var proxy = proxyGenerator.objectProxy(employee, employeeSpec, 
SyncControl.control());
 
-        assertTrue(handler.wasInvoked("getName"));
+        assertNotNull(proxy);
+        assertTrue(proxy instanceof WrappingObject);
+        assertNotEquals(Employee.class.getName(), proxy.getClass().getName());
+        assertNull(proxy.getName());
 
+        // requires interaction infrastructure ... (however, tested with 
regression tests separately)
+        //proxy.setName("hi");
+        //assertEquals("hi", proxy.getName());
     }
 
 }
diff --git 
a/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java
 
b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java
index 18dd21de935..7e8ae290f90 100644
--- 
a/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java
+++ 
b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java
@@ -21,8 +21,10 @@
 import jakarta.inject.Inject;
 
 import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.TestPropertySource;
@@ -38,6 +40,8 @@
 import org.apache.causeway.testdomain.jpa.entities.JpaProduct;
 import 
org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;
 
+import lombok.RequiredArgsConstructor;
+
 @SpringBootTest(
         classes = {
                 Configuration_usingJpa.class,
@@ -65,30 +69,42 @@ void uninstallFixture() {
         this.lock.release();
     }
 
-    @Test
-    void testWrapper_waitingOnDomainEvent() throws InterruptedException {
-        _MemoryUsage.measureMetaspace("exercise", ()->{
-// with caching
-//            exercise(1, 0);         // 2,221 KB
-//            exercise(1, 2000);      // 3,839 KB. // some leakage from 
collections
-//            exercise(20, 0);        // 2,112 KB
-//            exercise(20, 2000);     // 3,875 KB
-//            exercise(2000, 0);      // 3,263 KB. // ? increased some, is it 
significant; a lot less than without caching
-//            exercise(2000, 200);    // 4,294 KB.
-//            exercise(20000, 0);     // 3,243 KB  // no noticeable leakage 
compared to 2000; MUCH less than without caching
-
-// without caching
-//            exercise(1, 0);        //   2,244 KB
-//            exercise(1, 2000);     //.  3,669 KB // some leakage from 
collections
-//            exercise(20, 0);       //   2,440 KB
-//            exercise(20, 2000);    //.  4,286 KB
-            exercise(2000, 0);     //  14,580 KB // significant leakage from 20
-//            exercise(2000, 200);   //  20,423 KB
-//            exercise(20000, 0);    //.115,729 KB
-        });
+    //with caching
+    //  exercise(1, 0);         // 2,221 KB
+    //  exercise(1, 2000);      // 3,839 KB. // some leakage from collections
+    //  exercise(20, 0);        // 2,112 KB
+    //  exercise(20, 2000);     // 3,875 KB
+    //  exercise(2000, 0);      // 3,263 KB  // ? increased some, is it 
significant; a lot less than without caching
+    //  exercise(2000, 200);    // 4,294 KB
+    //  exercise(20000, 0);     // 3,243 KB  // no noticeable leakage compared 
to 2000; MUCH less than without caching
+    //without caching
+    //  exercise(1, 0);        //   2,244 KB
+    //  exercise(1, 2000);     //   3,669 KB // some leakage from collections
+    //  exercise(20, 0);       //   2,440 KB
+    //  exercise(20, 2000);    //   4,286 KB
+    //  exercise(2000, 0);     //  14,580 KB // significant leakage from 20
+    //  exercise(2000, 200);   //  20,423 KB
+    //  exercise(20000, 0);    //.115,729 KB
+    @RequiredArgsConstructor
+    enum Scenario {
+        TWO_K_TIMES_ONE(2000, 0, 4000),
+        TWO_K_TIMES_TEN(2000, 10, 4000),;
+        final int instances;
+        final int loops;
+        final int thresholdKibi;
+    }
+
+    @ParameterizedTest
+    @EnumSource(Scenario.class)
+    void testWrapper_waitingOnDomainEventScenario(final Scenario scenario) 
throws InterruptedException {
+        var usage = _MemoryUsage.measureMetaspace(()->
+            exercise(scenario.instances, scenario.loops));
+        Assertions.assertTrue(usage.usedInKibiBytes() < scenario.thresholdKibi,
+            ()->"%s exceeds expected %dKB threshold".formatted(usage, 
scenario.thresholdKibi));
+        System.out.printf("scenario %s usage %s%n", scenario.name(), usage);
     }
 
-    private void exercise(int instances, int loops) {
+    private void exercise(final int instances, final int loops) {
         for (int i = 0; i < instances; i++) {
             var jpaInventoryManager = 
wrapper.wrap(factoryService.viewModel(JpaInventoryManager.class));
             jpaInventoryManager.foo();

Reply via email to