From a145543003e1156fc4634d6266068fdf19b8360a Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sat, 3 Oct 2026 11:08:44 +0200 Subject: [PATCH 01/17] Do not create an InterceptionDecorationContext stack in client proxies when none exists Every business method invocation on a client proxy calls InterceptionDecorationContext.startIfNotEmpty(). When the caller is not intercepted and no RequestScopedCache is active (e.g. Weld SE, or any invocation outside of an HTTP/EJB request), getStack() allocated a new Stack (+ ArrayDeque), set it into the thread-local and then immediately removed it again in removeIfEmpty(). The net effect is nothing, but ThreadLocal.set()/remove() plus the allocation dominate the cost of a client proxy invocation. Return null straight away if there is no stack on the current thread. This is semantically identical: an absent stack is an empty stack and the old code never left anything behind in that case (with an active RequestScopedCache the empty stack would have been cached; it is now created lazily by the first startIfNotOnTop()/getStack() instead). --- .../weld/bean/proxy/InterceptionDecorationContext.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java index 88d3e2829e0..eb79055428a 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java @@ -187,7 +187,13 @@ public static void endInterceptorContext() { * returned value. */ public static Stack startIfNotEmpty() { - Stack stack = getStack(); + Stack stack = interceptionContexts.get(); + if (stack == null) { + // Fast path: there is no interception context on this thread (the caller is not intercepted). + // Do not create (and immediately remove) an empty Stack - allocating it and setting/removing the + // thread-local on every client proxy invocation is expensive and has no observable effect. + return null; + } if (!stack.elements.isEmpty()) { stack.push(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE); return stack; From 37705ee992fb1bdca260923e70be6deb883db7ba Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sat, 3 Oct 2026 11:11:59 +0200 Subject: [PATCH 02/17] Avoid ThreadLocal.remove() churn of the InterceptionDecorationContext stack Outside of an active RequestScopedCache each intercepted invocation (and each non-intercepted method of an intercepted subclass) creates a Stack, sets it into the thread-local and removes it again once the stack gets empty. ThreadLocal.remove() clears the weak reference of the map entry (native Reference.clear0) and expunges the entry, so the next set() allocates a new WeakReference entry again. The profile of an intercepted @ApplicationScoped bean shows remove() + expunge + entry allocation as the most expensive part of the invocation. Set the thread-local value to null instead: the entry is reused, and a null value does not retain any Weld class, so there is no class loader leak. Also allocate the ArrayDeque with a small initial capacity as the stack is usually very shallow. --- .../bean/proxy/InterceptionDecorationContext.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java index eb79055428a..743ba4ae913 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java @@ -42,6 +42,7 @@ public class InterceptionDecorationContext { private static ThreadLocal interceptionContexts = new ThreadLocal(); public static class Stack implements RequestScopedItem { + private static final int INITIAL_CAPACITY = 4; private boolean removeWhenEmpty; private final Deque elements; private final ThreadLocal interceptionContexts; @@ -49,7 +50,8 @@ public static class Stack implements RequestScopedItem { private Stack(ThreadLocal interceptionContexts) { this.interceptionContexts = interceptionContexts; - this.elements = new ArrayDeque(); + // the stack is usually very shallow, do not allocate the default 16 slots + this.elements = new ArrayDeque(INITIAL_CAPACITY); /* * Setting / removing of a thread-local is much more expensive compared to get. Therefore, * if RequestScopedCache is active we register the thread-local for removal at the end of the @@ -121,7 +123,15 @@ public void invalidate() { private void removeIfEmpty() { if (removeWhenEmpty && elements.isEmpty()) { - interceptionContexts.remove(); + /* + * Clear the value instead of calling ThreadLocal.remove(). remove() clears the weak reference + * of the ThreadLocalMap entry (a native call) and expunges the entry, so that the next set() + * has to allocate a new entry again. For an intercepted invocation outside of a request this + * happens on every call and is the most expensive part of it. A null value still does not + * retain any Weld class (the entry key is a weak reference to a java.lang.ThreadLocal), so + * there is no class loader leak. + */ + interceptionContexts.set(null); valid = false; } } From 8d92a23adbae5fbf6eb8ac7cbe7e9e1acfee7ee0 Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sat, 3 Oct 2026 11:12:36 +0200 Subject: [PATCH 03/17] Avoid the container registry lookup on every client proxy invocation ContextBeanInstance.getInstance() called Container.isSet(contextId) on each invocation to detect usage of a client proxy after shutdown. This is a ConcurrentHashMap lookup in RegistrySingletonProvider and shows up in the profile of @ApplicationScoped invocations. ContextBeanInstance now keeps the Container it was created for. Container gets a volatile cleanedUp flag set at the beginning of cleanup(); while it is false the container is guaranteed to be registered, so the registry lookup is only performed after the container was cleaned up. Behaviour is unchanged (including the case where a new container was registered under the same id afterwards). --- impl/src/main/java/org/jboss/weld/Container.java | 14 ++++++++++++++ .../jboss/weld/bean/proxy/ContextBeanInstance.java | 8 ++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/Container.java b/impl/src/main/java/org/jboss/weld/Container.java index c9507849462..03b9f84b3b4 100644 --- a/impl/src/main/java/org/jboss/weld/Container.java +++ b/impl/src/main/java/org/jboss/weld/Container.java @@ -117,6 +117,9 @@ public static void initialize(String contextId, BeanManagerImpl deploymentManage private ContainerState state = ContainerState.STOPPED; + // set at the very beginning of cleanup(); a container that is not cleaned up is still registered + private volatile boolean cleanedUp; + public Container(String contextId, BeanManagerImpl deploymentManager, ServiceRegistry deploymentServices) { this(contextId, deploymentManager, deploymentServices, null); } @@ -141,6 +144,7 @@ public Container(String contextId, BeanManagerImpl deploymentManager, ServiceReg * managers, and all deployment services */ public void cleanup() { + cleanedUp = true; managers.clear(); for (BeanManagerImpl beanManager : beanDeploymentArchives.values()) { beanManager.cleanup(); @@ -202,6 +206,16 @@ public void putBeanDeployments(BeanDeploymentArchiveMapping bdaMapping) { } } + /** + * Cheap check suitable for hot paths: as long as this method returns {@code false}, this container has not been cleaned + * up and is therefore still registered, i.e. {@link #isSet(String)} returns {@code true} for its context id. + * + * @return {@code true} if {@link #cleanup()} has been called on this container + */ + public boolean isCleanedUp() { + return cleanedUp; + } + public ContainerState getState() { return state; } diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java b/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java index eca7240de43..304d962bb4b 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java @@ -58,6 +58,7 @@ public class ContextBeanInstance extends AbstractBeanInstance implements Seri private final String contextId; // The actual type of the resulting bean instance private final transient Class instanceType; + private final transient Container container; private final transient BeanManagerImpl manager; private final transient CurrentInjectionPoint currentInjectionPoint; @@ -75,12 +76,15 @@ public ContextBeanInstance(Bean bean, BeanIdentifier id, String contextId) { this.contextId = contextId; this.instanceType = computeInstanceType(bean); BeanLogger.LOG.createdContextInstance(bean, id); - this.manager = Container.instance(contextId).deploymentManager(); + this.container = Container.instance(contextId); + this.manager = container.deploymentManager(); this.currentInjectionPoint = manager.getServices().get(CurrentInjectionPoint.class); } public T getInstance() { - if (!Container.isSet(contextId)) { + // Checking the container we were created for is a single volatile read; the registry lookup in + // Container.isSet() is only needed once that container has been cleaned up (shut down) + if (container.isCleanedUp() && !Container.isSet(contextId)) { throw ContextLogger.LOG.contextualReferenceNotValidAfterShutdown(bean, contextId); } T existingInstance = ContextualInstance.getIfExists(bean, manager); From bf644c6ac4bd70841aacc619bd11fe40e652fbdc Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sat, 3 Oct 2026 17:48:07 +0200 Subject: [PATCH 04/17] Reuse a per-thread InterceptionDecorationContext stack Every invocation of an intercepted subclass method (both intercepted and non-intercepted methods push the subclass handler) outside of a request allocated a new Stack and ArrayDeque, set the thread-local and cleared it again once the stack became empty. Within a request the stack was kept in the RequestScopedCache instead. The thread-local now holds a small Object[] holder (a JDK class) that references the per-thread stack strongly only while it is not empty and weakly otherwise. An idle thread never retains a Weld class, so there is no class loader leak, and the stack is reused until the next GC clears the weak reference. The RequestScopedCache registration and the validity tracking of the stack are no longer needed. --- .../proxy/InterceptionDecorationContext.java | 148 +++++++----------- 1 file changed, 59 insertions(+), 89 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java index 743ba4ae913..36b77e9184c 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java @@ -17,13 +17,11 @@ package org.jboss.weld.bean.proxy; +import java.lang.ref.WeakReference; import java.util.ArrayDeque; import java.util.Deque; import java.util.EmptyStackException; -import org.jboss.weld.contexts.cache.RequestScopedCache; -import org.jboss.weld.contexts.cache.RequestScopedItem; - /** * A class that holds the interception (and decoration) contexts which are currently in progress. *

@@ -35,32 +33,32 @@ *

* Classes may create new interception contexts as necessary (e.g. allowing client proxies to create new interception * contexts in order to make circular references interceptable multiple times). + *

+ * Each thread uses (at most) one {@link Stack} which is reused across invocations. The thread-local only ever holds a + * small {@code Object[]} holder (a JDK class): the stack is referenced strongly only while it is not empty and weakly + * otherwise. An idle thread therefore never retains a Weld class (no class loader leak) and an intercepted invocation + * does not need to allocate a new stack nor to set / clear the thread-local on every call. * * @author Marius Bogoevici */ public class InterceptionDecorationContext { - private static ThreadLocal interceptionContexts = new ThreadLocal(); - public static class Stack implements RequestScopedItem { + // holder[ACTIVE] - the stack of this thread while it is not empty, null otherwise + private static final int ACTIVE = 0; + // holder[REUSABLE] - a WeakReference to the (possibly empty) stack of this thread + private static final int REUSABLE = 1; + + private static final ThreadLocal interceptionContexts = new ThreadLocal(); + + public static class Stack { private static final int INITIAL_CAPACITY = 4; - private boolean removeWhenEmpty; private final Deque elements; - private final ThreadLocal interceptionContexts; - private boolean valid; + private final Object[] holder; - private Stack(ThreadLocal interceptionContexts) { - this.interceptionContexts = interceptionContexts; + private Stack(Object[] holder) { + this.holder = holder; // the stack is usually very shallow, do not allocate the default 16 slots this.elements = new ArrayDeque(INITIAL_CAPACITY); - /* - * Setting / removing of a thread-local is much more expensive compared to get. Therefore, - * if RequestScopedCache is active we register the thread-local for removal at the end of the - * request. This yields possitive results only if the number of intercepted invocations is large. - * If it is not, the performance characteristics are similar to explicitly removing the thread-local - * once the stack gets empty. - */ - this.removeWhenEmpty = !RequestScopedCache.addItemIfActive(this); - this.valid = true; } /** @@ -72,7 +70,6 @@ private Stack(ThreadLocal interceptionContexts) { * @return true if the given context was pushed to the top of the stack, false if the given context was on top already */ public boolean startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHandler context) { - checkState(); if (elements.isEmpty() || peek() != context) { push(context); return true; @@ -85,55 +82,24 @@ public void end() { } private void push(CombinedInterceptorAndDecoratorStackMethodHandler item) { - checkState(); + if (elements.isEmpty()) { + // the stack becomes active - reference it strongly so that it cannot be garbage collected + holder[ACTIVE] = this; + } elements.addFirst(item); } public CombinedInterceptorAndDecoratorStackMethodHandler peek() { - checkState(); return elements.peekFirst(); } private CombinedInterceptorAndDecoratorStackMethodHandler pop() { - checkState(); CombinedInterceptorAndDecoratorStackMethodHandler top = elements.removeFirst(); - removeIfEmpty(); - return top; - } - - private void checkState() { - if (!valid) { - throw new IllegalStateException("This InterceptionDecorationContext is no longer valid."); - } - } - - @Override - public void invalidate() { - /* - * This cached item is being invalidated. - * It does not necessarily mean that the request is being destroyed - it may just be the case that it is being - * flushed in the middle - * of a request (e.g. for AlterableContext.destroy()). - * Therefore, we cannot remove IDC now but we just set removeWhenEmpty flag and let it remove itself once the stack - * gets empty. - */ - removeWhenEmpty = true; - removeIfEmpty(); - } - - private void removeIfEmpty() { - if (removeWhenEmpty && elements.isEmpty()) { - /* - * Clear the value instead of calling ThreadLocal.remove(). remove() clears the weak reference - * of the ThreadLocalMap entry (a native call) and expunges the entry, so that the next set() - * has to allocate a new entry again. For an intercepted invocation outside of a request this - * happens on every call and is the most expensive part of it. A null value still does not - * retain any Weld class (the entry key is a weak reference to a java.lang.ThreadLocal), so - * there is no class loader leak. - */ - interceptionContexts.set(null); - valid = false; + if (elements.isEmpty()) { + // only weakly referenced from now on, the thread-local does not retain any Weld class + holder[ACTIVE] = null; } + return top; } public int size() { @@ -142,7 +108,7 @@ public int size() { @Override public String toString() { - return "Stack [valid=" + valid + ", cached=" + !removeWhenEmpty + ", elements=" + elements + "]"; + return "Stack [elements=" + elements + "]"; } } @@ -150,6 +116,14 @@ public String toString() { private InterceptionDecorationContext() { } + /** + * @return the stack of the current thread if it is not empty, null otherwise + */ + private static Stack activeStack() { + Object[] holder = interceptionContexts.get(); + return holder == null ? null : (Stack) holder[ACTIVE]; + } + /** * Peeks the current top of the stack. * @@ -157,7 +131,7 @@ private InterceptionDecorationContext() { * @throws EmptyStackException */ public static CombinedInterceptorAndDecoratorStackMethodHandler peek() { - return peek(interceptionContexts.get()); + return peek(activeStack()); } /** @@ -166,7 +140,7 @@ public static CombinedInterceptorAndDecoratorStackMethodHandler peek() { * @return the current top of the stack or returns null if the stack is empty */ public static CombinedInterceptorAndDecoratorStackMethodHandler peekIfNotEmpty() { - Stack stack = interceptionContexts.get(); + Stack stack = activeStack(); if (stack == null) { return null; } @@ -177,11 +151,11 @@ public static CombinedInterceptorAndDecoratorStackMethodHandler peekIfNotEmpty() * Indicates whether the stack is empty. */ public static boolean empty() { - return empty(interceptionContexts.get()); + return activeStack() == null; } public static void endInterceptorContext() { - pop(interceptionContexts.get()); + pop(activeStack()); } /** @@ -197,21 +171,13 @@ public static void endInterceptorContext() { * returned value. */ public static Stack startIfNotEmpty() { - Stack stack = interceptionContexts.get(); + Stack stack = activeStack(); if (stack == null) { - // Fast path: there is no interception context on this thread (the caller is not intercepted). - // Do not create (and immediately remove) an empty Stack - allocating it and setting/removing the - // thread-local on every client proxy invocation is expensive and has no observable effect. - return null; - } - if (!stack.elements.isEmpty()) { - stack.push(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE); - return stack; - } else { - // if RequestScopedCache is not active, remove now to prevent ThreadLocal leak - stack.removeIfEmpty(); + // there is no interception context on this thread (the caller is not intercepted) return null; } + stack.push(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE); + return stack; } /** @@ -231,15 +197,27 @@ public static Stack startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHa } /** - * Gets the current Stack. If the stack is not set, a new empty instance is created and set. + * Gets the current Stack. If there is no stack for the current thread, a new empty instance is created. + * The returned stack may be empty; it is only guaranteed to stay the stack of the current thread while the caller + * holds a reference to it. * - * @return + * @return the stack of the current thread */ + @SuppressWarnings("unchecked") public static Stack getStack() { - Stack stack = interceptionContexts.get(); + Object[] holder = interceptionContexts.get(); + if (holder == null) { + holder = new Object[2]; + interceptionContexts.set(holder); + } + Stack stack = (Stack) holder[ACTIVE]; if (stack == null) { - stack = new Stack(interceptionContexts); - interceptionContexts.set(stack); + WeakReference ref = (WeakReference) holder[REUSABLE]; + stack = ref == null ? null : ref.get(); + if (stack == null) { + stack = new Stack(holder); + holder[REUSABLE] = new WeakReference(stack); + } } return stack; } @@ -259,12 +237,4 @@ private static CombinedInterceptorAndDecoratorStackMethodHandler peek(Stack stac return stack.peek(); } } - - private static boolean empty(Stack stack) { - if (stack == null) { - return true; - } else { - return stack.elements.isEmpty(); - } - } } From 37a42bff64678030d4b9ac7252e247c7e1149eea Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sat, 3 Oct 2026 18:16:57 +0200 Subject: [PATCH 05/17] Skip the interception context lookup in client proxies until it is first used Client proxies call InterceptionDecorationContext.startIfNotEmpty() on every invocation, i.e. a ThreadLocal lookup. As long as no thread has ever created an interception context (no intercepted or decorated method has been invoked yet, which is always the case in deployments without interceptors and decorators) the lookup cannot find anything. startIfNotEmpty() now invokes a constant MethodHandle guarded by a SwitchPoint: it returns null without any lookup until the first per-thread stack holder is created, when the SwitchPoint is invalidated (once, before the holder is published to that thread). The JIT compiles the guard to nothing, so applications that do use interception do not pay for an additional check either. A thread can only have an active stack if it created its holder itself, so the switch cannot be observed too late. --- .../proxy/InterceptionDecorationContext.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java index 36b77e9184c..99590b19468 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java @@ -17,6 +17,10 @@ package org.jboss.weld.bean.proxy; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.SwitchPoint; import java.lang.ref.WeakReference; import java.util.ArrayDeque; import java.util.Deque; @@ -50,6 +54,31 @@ public class InterceptionDecorationContext { private static final ThreadLocal interceptionContexts = new ThreadLocal(); + /* + * Valid until any thread creates its stack holder for the first time (it is never re-validated). While it is valid, + * no thread can have an interception context, so client proxies do not need to look up the thread-local at all. + * This is the case whenever no intercepted or decorated method has been invoked yet, e.g. in deployments without + * interceptors and decorators. + * + * A SwitchPoint is used instead of a plain static flag because the JIT compiles the guard to nothing: neither + * deployments without interception nor deployments using it pay for the check. The invalidation (a + * deoptimization of the dependent code) happens once, in getStack(), before the holder is published to the + * current thread; a thread can only have an active stack if it has created its holder itself. + */ + private static final SwitchPoint NO_HOLDER_CREATED = new SwitchPoint(); + private static volatile boolean holderCreated; + private static final MethodHandle START_IF_NOT_EMPTY; + + static { + try { + MethodHandle lookup = MethodHandles.lookup().findStatic(InterceptionDecorationContext.class, + "startIfNotEmptyLookup", MethodType.methodType(Stack.class)); + START_IF_NOT_EMPTY = NO_HOLDER_CREATED.guardWithTest(MethodHandles.constant(Stack.class, null), lookup); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + public static class Stack { private static final int INITIAL_CAPACITY = 4; private final Deque elements; @@ -171,6 +200,17 @@ public static void endInterceptorContext() { * returned value. */ public static Stack startIfNotEmpty() { + try { + // constant null until the first stack holder is created, startIfNotEmptyLookup() afterwards + return (Stack) START_IF_NOT_EMPTY.invokeExact(); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + private static Stack startIfNotEmptyLookup() { Stack stack = activeStack(); if (stack == null) { // there is no interception context on this thread (the caller is not intercepted) @@ -207,6 +247,9 @@ public static Stack startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHa public static Stack getStack() { Object[] holder = interceptionContexts.get(); if (holder == null) { + if (!holderCreated) { + noHolderCreatedAnymore(); + } holder = new Object[2]; interceptionContexts.set(holder); } @@ -222,6 +265,14 @@ public static Stack getStack() { return stack; } + private static synchronized void noHolderCreatedAnymore() { + if (!holderCreated) { + // switch all client proxies to the thread-local lookup before this thread can start an interception context + SwitchPoint.invalidateAll(new SwitchPoint[] { NO_HOLDER_CREATED }); + holderCreated = true; + } + } + private static CombinedInterceptorAndDecoratorStackMethodHandler pop(Stack stack) { if (stack == null) { throw new EmptyStackException(); From 42b5dfb865b02a5009198375af6796fe1c99818d Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sat, 3 Oct 2026 17:48:43 +0200 Subject: [PATCH 06/17] Resolve the contextual instance strategy of Weld beans without a type check in client proxies ContextBeanInstance keeps the bean as RIBean when possible so that the client proxy invocation goes straight to the bean's ContextualInstanceStrategy (for @ApplicationScoped beans a volatile read of the cached instance) instead of going through the instanceof check in ContextualInstance. Invalidation is unchanged: the strategy remains the single place where the instance is cached. --- .../org/jboss/weld/bean/proxy/ContextBeanInstance.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java b/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java index 304d962bb4b..910008e7ecd 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java @@ -27,6 +27,7 @@ import org.jboss.weld.Container; import org.jboss.weld.bean.ContextualInstance; +import org.jboss.weld.bean.RIBean; import org.jboss.weld.contexts.CreationalContextImpl; import org.jboss.weld.contexts.WeldCreationalContext; import org.jboss.weld.injection.CurrentInjectionPoint; @@ -53,6 +54,8 @@ public class ContextBeanInstance extends AbstractBeanInstance implements Seri private static final long serialVersionUID = -8144230657830556503L; // The bean private transient Bean bean; + // The same bean if it is a Weld bean (null otherwise); avoids the type check of ContextualInstance on every invocation + private final transient RIBean riBean; // The bean index in the manager private final BeanIdentifier id; private final String contextId; @@ -72,6 +75,7 @@ public class ContextBeanInstance extends AbstractBeanInstance implements Seri */ public ContextBeanInstance(Bean bean, BeanIdentifier id, String contextId) { this.bean = bean; + this.riBean = bean instanceof RIBean ? cast(bean) : null; this.id = id; this.contextId = contextId; this.instanceType = computeInstanceType(bean); @@ -87,7 +91,9 @@ public T getInstance() { if (container.isCleanedUp() && !Container.isSet(contextId)) { throw ContextLogger.LOG.contextualReferenceNotValidAfterShutdown(bean, contextId); } - T existingInstance = ContextualInstance.getIfExists(bean, manager); + // for an @ApplicationScoped bean this is a single volatile read of the instance cached by its strategy + T existingInstance = riBean != null ? ContextualInstance.getIfExists(riBean, manager) + : ContextualInstance.getIfExists(bean, manager); if (existingInstance != null) { return existingInstance; } From 2f5a484ed058879106ce122933ac172b7fd40da9 Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 09:59:52 +0200 Subject: [PATCH 07/17] Use a plain array for the per-thread InterceptionDecorationContext stack Replace the ArrayDeque inside InterceptionDecorationContext.Stack with an array and a size index that grows on demand. push/pop/peek/startIfNotOnTop need fewer branches and no ring-buffer index arithmetic. Popped slots are cleared so that no handler is retained, and the weakly referenced idle stack (no class loader leak) is unchanged. Null elements are still rejected and popping an empty Stack still throws NoSuchElementException. --- .../proxy/InterceptionDecorationContext.java | 71 ++++++++++---- .../InterceptionDecorationContextTest.java | 94 +++++++++++++++++++ 2 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java index 99590b19468..93a3384ede2 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java @@ -22,9 +22,9 @@ import java.lang.invoke.MethodType; import java.lang.invoke.SwitchPoint; import java.lang.ref.WeakReference; -import java.util.ArrayDeque; -import java.util.Deque; +import java.util.Arrays; import java.util.EmptyStackException; +import java.util.NoSuchElementException; /** * A class that holds the interception (and decoration) contexts which are currently in progress. @@ -81,13 +81,15 @@ public class InterceptionDecorationContext { public static class Stack { private static final int INITIAL_CAPACITY = 4; - private final Deque elements; private final Object[] holder; + // elements[0] is the bottom of the stack, elements[size - 1] the top; unused slots are always null + private CombinedInterceptorAndDecoratorStackMethodHandler[] elements; + private int size; private Stack(Object[] holder) { this.holder = holder; - // the stack is usually very shallow, do not allocate the default 16 slots - this.elements = new ArrayDeque(INITIAL_CAPACITY); + // the stack is usually very shallow + this.elements = new CombinedInterceptorAndDecoratorStackMethodHandler[INITIAL_CAPACITY]; } /** @@ -99,8 +101,13 @@ private Stack(Object[] holder) { * @return true if the given context was pushed to the top of the stack, false if the given context was on top already */ public boolean startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHandler context) { - if (elements.isEmpty() || peek() != context) { - push(context); + int s = size; + if (s == 0) { + push(context, 0); + return true; + } + if (elements[s - 1] != context) { + push(context, s); return true; } return false; @@ -110,21 +117,45 @@ public void end() { pop(); } - private void push(CombinedInterceptorAndDecoratorStackMethodHandler item) { - if (elements.isEmpty()) { + private void push(CombinedInterceptorAndDecoratorStackMethodHandler item, int s) { + if (item == null) { + // null elements are not supported (consistent with the previous ArrayDeque based implementation) + throw new NullPointerException(); + } + CombinedInterceptorAndDecoratorStackMethodHandler[] es = elements; + if (s == es.length) { + es = grow(); + } + es[s] = item; + size = s + 1; + if (s == 0) { // the stack becomes active - reference it strongly so that it cannot be garbage collected holder[ACTIVE] = this; } - elements.addFirst(item); + } + + private CombinedInterceptorAndDecoratorStackMethodHandler[] grow() { + CombinedInterceptorAndDecoratorStackMethodHandler[] es = Arrays.copyOf(elements, elements.length << 1); + elements = es; + return es; } public CombinedInterceptorAndDecoratorStackMethodHandler peek() { - return elements.peekFirst(); + int s = size; + return s == 0 ? null : elements[s - 1]; } private CombinedInterceptorAndDecoratorStackMethodHandler pop() { - CombinedInterceptorAndDecoratorStackMethodHandler top = elements.removeFirst(); - if (elements.isEmpty()) { + int s = size - 1; + if (s < 0) { + throw new NoSuchElementException(); + } + CombinedInterceptorAndDecoratorStackMethodHandler[] es = elements; + CombinedInterceptorAndDecoratorStackMethodHandler top = es[s]; + // do not retain the handler (and its bean instance) once it is not on the stack anymore + es[s] = null; + size = s; + if (s == 0) { // only weakly referenced from now on, the thread-local does not retain any Weld class holder[ACTIVE] = null; } @@ -132,12 +163,20 @@ private CombinedInterceptorAndDecoratorStackMethodHandler pop() { } public int size() { - return elements.size(); + return size; } @Override public String toString() { - return "Stack [elements=" + elements + "]"; + // top of the stack first + StringBuilder builder = new StringBuilder("Stack [elements=["); + for (int i = size - 1; i >= 0; i--) { + builder.append(elements[i]); + if (i > 0) { + builder.append(", "); + } + } + return builder.append("]]").toString(); } } @@ -216,7 +255,7 @@ private static Stack startIfNotEmptyLookup() { // there is no interception context on this thread (the caller is not intercepted) return null; } - stack.push(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE); + stack.push(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE, stack.size); return stack; } diff --git a/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java b/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java new file mode 100644 index 00000000000..5f3a3079d34 --- /dev/null +++ b/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java @@ -0,0 +1,94 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2026, Red Hat, Inc. and/or its affiliates, and individual + * contributors by the @authors tag. See the copyright.txt in the + * distribution for a full listing of individual contributors. + * + * Licensed 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.jboss.weld.bean.proxy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.EmptyStackException; +import java.util.NoSuchElementException; + +import org.jboss.weld.bean.proxy.InterceptionDecorationContext.Stack; +import org.junit.Test; + +public class InterceptionDecorationContextTest { + + @Test + public void testStackSemantics() { + assertTrue(InterceptionDecorationContext.empty()); + assertNull(InterceptionDecorationContext.peekIfNotEmpty()); + assertNull(InterceptionDecorationContext.startIfNotEmpty()); + + CombinedInterceptorAndDecoratorStackMethodHandler a = new CombinedInterceptorAndDecoratorStackMethodHandler(); + CombinedInterceptorAndDecoratorStackMethodHandler b = new CombinedInterceptorAndDecoratorStackMethodHandler(); + + Stack stack = InterceptionDecorationContext.startIfNotOnTop(a); + assertNotNull(stack); + assertFalse(InterceptionDecorationContext.empty()); + assertSame(a, InterceptionDecorationContext.peek()); + // already on top - not pushed again + assertNull(InterceptionDecorationContext.startIfNotOnTop(a)); + assertFalse(stack.startIfNotOnTop(a)); + assertEquals(1, stack.size()); + + // a client proxy invocation starts a new context + assertSame(stack, InterceptionDecorationContext.startIfNotEmpty()); + assertSame(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE, stack.peek()); + assertTrue(stack.startIfNotOnTop(a)); + assertEquals(3, stack.size()); + + // grow beyond the initial capacity, recursive invocations + for (int i = 0; i < 20; i++) { + assertTrue(stack.startIfNotOnTop(i % 2 == 0 ? b : a)); + } + assertEquals(23, stack.size()); + assertSame(a, stack.peek()); + for (int i = 19; i >= 0; i--) { + assertSame(i % 2 == 0 ? b : a, stack.peek()); + stack.end(); + } + assertSame(a, stack.peek()); + stack.end(); + assertSame(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE, stack.peek()); + stack.end(); + assertSame(a, InterceptionDecorationContext.peekIfNotEmpty()); + assertTrue(stack.toString().startsWith("Stack [elements=[")); + + InterceptionDecorationContext.endInterceptorContext(); + assertTrue(InterceptionDecorationContext.empty()); + assertEquals(0, stack.size()); + assertNull(stack.peek()); + assertEquals("Stack [elements=[]]", stack.toString()); + assertThrows(EmptyStackException.class, InterceptionDecorationContext::peek); + assertThrows(EmptyStackException.class, InterceptionDecorationContext::endInterceptorContext); + assertThrows(NoSuchElementException.class, stack::end); + assertThrows(NullPointerException.class, () -> stack.startIfNotOnTop(null)); + assertTrue(InterceptionDecorationContext.empty()); + + // the idle stack is reused + Stack again = InterceptionDecorationContext.startIfNotOnTop(b); + assertSame(stack, again); + assertSame(b, again.peek()); + again.end(); + assertTrue(InterceptionDecorationContext.empty()); + } +} From 1c2b62c597f04eff047f872ce0b9c2d5c9f10323 Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 14:58:44 +0200 Subject: [PATCH 08/17] Invoke interceptor methods and the proceed method through method handles Interceptor methods (@AroundInvoke etc.) and the proceed method of an around invoke chain (WeldSubclass.method$$super() or the decorated method) used to be invoked by Method.invoke(). They are now invoked through a MethodHandle adapted to (Object, Object)Object or (Object, Object[])Object, created lazily upon the first invocation and cached per method (MethodInvoker; per interceptor metadata for interceptor methods, per declaring class via a ClassValue for proceed methods). The semantics of Method.invoke() are kept exactly: - the handle is only used if the receiver is an instance of the declaring class and every argument matches the parameter type exactly (reference type or exact wrapper of the primitive type), i.e. if the invocation cannot fail before reaching the method; otherwise (null receiver, wrong arguments, widening primitive conversions, static methods...) the Method is invoked reflectively and throws the same exceptions as before - any Throwable thrown by the invoked method is wrapped in an InvocationTargetException, so that the existing unwrapping code paths are unchanged - accessibility is never changed; a handle that cannot be created (e.g. module readability) results in reflective invocations --- .../proxy/AroundInvokeInvocationContext.java | 20 +- .../proxy/InterceptorMethodHandler.java | 28 ++- .../weld/interceptor/proxy/MethodInvoker.java | 194 ++++++++++++++++++ ...TerminalAroundInvokeInvocationContext.java | 16 +- ...TerminalAroundInvokeInvocationContext.java | 20 +- .../reader/AbstractInterceptorMetadata.java | 24 ++- .../reader/SimpleInterceptorInvocation.java | 30 ++- .../interceptor/proxy/MethodInvokerTest.java | 187 +++++++++++++++++ 8 files changed, 489 insertions(+), 30 deletions(-) create mode 100644 impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java create mode 100644 impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java index 71d2a9e00dc..8db8d7fbfc0 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java @@ -60,22 +60,30 @@ abstract class AroundInvokeInvocationContext extends AbstractInvocationContext { public static AroundInvokeInvocationContext create(Object instance, Method method, Method proceed, Object[] args, List chain, Set interceptorBindings, Stack stack) { + return create(instance, method, proceed, MethodInvoker.of(proceed), args, chain, interceptorBindings, stack); + } + + static AroundInvokeInvocationContext create(Object instance, Method method, Method proceed, MethodInvoker proceedInvoker, + Object[] args, List chain, Set interceptorBindings, Stack stack) { CombinedInterceptorAndDecoratorStackMethodHandler currentHandler = (stack == null) ? null : stack.peek(); if (chain.size() == 1) { - return new TerminalAroundInvokeInvocationContext(instance, method, proceed, args, null, interceptorBindings, - currentHandler); + return new TerminalAroundInvokeInvocationContext(instance, method, proceed, proceedInvoker, args, null, + interceptorBindings, currentHandler); } else { - return new NonTerminalAroundInvokeInvocationContext(instance, method, proceed, args, interceptorBindings, chain, - currentHandler); + return new NonTerminalAroundInvokeInvocationContext(instance, method, proceed, proceedInvoker, args, + interceptorBindings, chain, currentHandler); } } final CombinedInterceptorAndDecoratorStackMethodHandler currentHandler; + // invokes the proceed method + final MethodInvoker proceedInvoker; - AroundInvokeInvocationContext(Object target, Method method, Method proceed, Object[] parameters, - Map contextData, + AroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, + Object[] parameters, Map contextData, Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { super(target, method, proceed, parameters, contextData, interceptorBindings); + this.proceedInvoker = proceedInvoker; this.currentHandler = currentHandler; } diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java index 914b994bbb7..424be377ed6 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java @@ -62,7 +62,7 @@ public Object invoke(Stack stack, Object self, Method thisMethod, Method proceed protected Object executeInterception(Object instance, Method method, Method proceed, Object[] args, InterceptionType interceptionType, Stack stack) throws Throwable { - CachedInterceptionChain chain = getInterceptionChain(instance, method, interceptionType); + CachedInterceptionChain chain = getInterceptionChain(instance, method, proceed, interceptionType); if (chain.interceptorMethods.isEmpty()) { // shortcut if there are no interceptors if (proceed == null) { @@ -86,8 +86,8 @@ protected Object executeLifecycleInterception(Object instance, Method method, Me protected Object executeAroundInvoke(Object instance, Method method, Method proceed, Object[] args, CachedInterceptionChain chain, Stack stack) throws Throwable { - InvocationContext ctx = create(instance, method, proceed, args, chain.interceptorMethods, - chain.interceptorBindings, stack); + InvocationContext ctx = create(instance, method, proceed, chain.getProceedInvoker(proceed), args, + chain.interceptorMethods, chain.interceptorBindings, stack); try { return chain.interceptorMethods.get(0).invoke(ctx); } catch (InvocationTargetException e) { @@ -95,13 +95,15 @@ protected Object executeAroundInvoke(Object instance, Method method, Method proc } } - private CachedInterceptionChain getInterceptionChain(Object instance, Method method, InterceptionType interceptionType) { + private CachedInterceptionChain getInterceptionChain(Object instance, Method method, Method proceed, + InterceptionType interceptionType) { if (method != null) { CachedInterceptionChain cachedChain = cachedChains.get(method); if (cachedChain == null) { cachedChain = new CachedInterceptionChain( ctx.buildInterceptorMethodInvocations(instance, method, interceptionType), ctx.getInterceptionModel() - .getMemberInterceptorBindings(method)); + .getMemberInterceptorBindings(method), + proceed); CachedInterceptionChain old = cachedChains.putIfAbsent(method, cachedChain); if (old != null) { cachedChain = old; @@ -110,7 +112,7 @@ private CachedInterceptionChain getInterceptionChain(Object instance, Method met return cachedChain; } return new CachedInterceptionChain(ctx.buildInterceptorMethodInvocations(instance, null, interceptionType), - ctx.getInterceptionModel().getClassInterceptorBindings()); + ctx.getInterceptionModel().getClassInterceptorBindings(), proceed); } private boolean isInterceptorMethod(Method method) { @@ -125,10 +127,22 @@ private static class CachedInterceptionChain { private final List interceptorMethods; private final Set interceptorBindings; + // the proceed method the chain was created for and its invoker + private final Method proceed; + private final MethodInvoker proceedInvoker; - public CachedInterceptionChain(List chain, Set interceptorBindings) { + public CachedInterceptionChain(List chain, Set interceptorBindings, + Method proceed) { this.interceptorMethods = chain; this.interceptorBindings = interceptorBindings; + this.proceed = proceed; + // only needed if there are around invoke interceptors + this.proceedInvoker = chain.isEmpty() ? null : MethodInvoker.of(proceed); + } + + MethodInvoker getProceedInvoker(Method proceed) { + // the proceed method is the same for every invocation of a given method of a given instance + return proceed == this.proceed ? proceedInvoker : MethodInvoker.of(proceed); } } } diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java new file mode 100644 index 00000000000..693b8f7ebfd --- /dev/null +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java @@ -0,0 +1,194 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2026, Red Hat, Inc., and individual contributors + * by the @authors tag. See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed 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.jboss.weld.interceptor.proxy; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.jboss.weld.util.Primitives; + +/** + * Invokes an instance method with exactly the semantics of {@link Method#invoke(Object, Object...)}, but through a + * {@link MethodHandle} whenever the invocation cannot fail before reaching the method. + *

+ * The handle is only used if the receiver is an instance of the declaring class and every argument is either an instance + * of the corresponding reference parameter type (or {@code null}) or an instance of the exact wrapper type of the + * corresponding primitive parameter type. In this case the conversions applied by the handle cannot fail and any + * {@link Throwable} thrown by the invocation was thrown by the method itself; it is wrapped in an + * {@link InvocationTargetException}, as {@link Method#invoke(Object, Object...)} does. In all other cases (e.g. widening + * primitive conversions, wrong arguments, {@code null} receiver, static methods, a method handle that cannot be created) + * the given {@link Method} is invoked reflectively, so that the exceptions thrown remain exactly the same. + *

+ * Instances are immutable and shared; they are cached per method (see {@link #of(Method)}) and created lazily, upon the + * first invocation of the method, so that the bootstrap is not affected. + * + * @author Weld contributors + */ +public final class MethodInvoker { + + private static final Object[] NO_ARGUMENTS = new Object[0]; + private static final MethodType SPREAD_TYPE = MethodType.methodType(Object.class, Object.class, Object[].class); + private static final MethodType SINGLE_ARGUMENT_TYPE = MethodType.methodType(Object.class, Object.class, Object.class); + + /* + * Invokers are cached per declaring class. The cache is attached to the declaring class itself so that it does not + * prevent the class (loader) from being garbage collected. + */ + private static final ClassValue> INVOKERS = new ClassValue>() { + @Override + protected ConcurrentMap computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + /** + * Returns the (cached) invoker for the given method. The method has to be accessible to Weld already (i.e. it can be + * invoked reflectively); this method never changes its accessibility. + * + * @param method the method, may be null + * @return the invoker of the given method, null if the given method is null + */ + public static MethodInvoker of(Method method) { + if (method == null) { + return null; + } + ConcurrentMap invokers = INVOKERS.get(method.getDeclaringClass()); + MethodInvoker invoker = invokers.get(method); + if (invoker == null) { + invoker = new MethodInvoker(method); + MethodInvoker previous = invokers.putIfAbsent(method, invoker); + if (previous != null) { + invoker = previous; + } + } + return invoker; + } + + private final Class receiverType; + // the expected classes of the arguments; the wrapper class for a primitive parameter + private final Class[] argumentTypes; + private final boolean[] primitive; + // (Object, Object[])Object, null if reflection has to be used + private final MethodHandle spreadHandle; + // (Object, Object)Object, null if the method does not declare exactly one parameter or reflection has to be used + private final MethodHandle singleArgumentHandle; + + private MethodInvoker(Method method) { + this.receiverType = method.getDeclaringClass(); + Class[] parameterTypes = method.getParameterTypes(); + this.argumentTypes = new Class[parameterTypes.length]; + this.primitive = new boolean[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + primitive[i] = parameterTypes[i].isPrimitive(); + argumentTypes[i] = primitive[i] ? Primitives.wrap(parameterTypes[i]) : parameterTypes[i]; + } + MethodHandle handle = null; + if (!Modifier.isStatic(method.getModifiers())) { + try { + // no access check if the method is accessible; otherwise the same check as reflection, performed by Weld + handle = MethodHandles.lookup().unreflect(method); + } catch (IllegalAccessException | RuntimeException e) { + // e.g. a module not readable by Weld - use reflection + handle = null; + } + } + if (handle != null) { + this.spreadHandle = handle.asSpreader(Object[].class, parameterTypes.length).asType(SPREAD_TYPE); + this.singleArgumentHandle = parameterTypes.length == 1 ? handle.asType(SINGLE_ARGUMENT_TYPE) : null; + } else { + this.spreadHandle = null; + this.singleArgumentHandle = null; + } + } + + /** + * Equivalent to {@code method.invoke(target, args)}. + * + * @param method the method this invoker was created for (or an equal one), invoked if the method handle cannot be used + */ + public Object invoke(Method method, Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { + MethodHandle handle = spreadHandle; + if (handle != null && receiverType.isInstance(target) && accepts(args)) { + try { + return (Object) handle.invokeExact(target, args); + } catch (Throwable e) { + throw new InvocationTargetException(e); + } + } + return method.invoke(target, args); + } + + /** + * Equivalent to {@code method.invoke(target, arg)}. + * + * @param method the method this invoker was created for (or an equal one), invoked if the method handle cannot be used + */ + public Object invoke(Method method, Object target, Object arg) throws IllegalAccessException, InvocationTargetException { + MethodHandle handle = singleArgumentHandle; + if (handle != null && receiverType.isInstance(target) && accepts(0, arg)) { + try { + return (Object) handle.invokeExact(target, arg); + } catch (Throwable e) { + throw new InvocationTargetException(e); + } + } + return method.invoke(target, arg); + } + + /** + * Equivalent to {@code method.invoke(target)}. + * + * @param method the method this invoker was created for (or an equal one), invoked if the method handle cannot be used + */ + public Object invoke(Method method, Object target) throws IllegalAccessException, InvocationTargetException { + MethodHandle handle = spreadHandle; + if (handle != null && argumentTypes.length == 0 && receiverType.isInstance(target)) { + try { + return (Object) handle.invokeExact(target, NO_ARGUMENTS); + } catch (Throwable e) { + throw new InvocationTargetException(e); + } + } + return method.invoke(target); + } + + private boolean accepts(Object[] args) { + if (args == null || args.length != argumentTypes.length) { + return false; + } + for (int i = 0; i < args.length; i++) { + if (!accepts(i, args[i])) { + return false; + } + } + return true; + } + + private boolean accepts(int position, Object arg) { + if (arg == null) { + return !primitive[position]; + } + // no widening primitive conversions + return primitive[position] ? arg.getClass() == argumentTypes[position] : argumentTypes[position].isInstance(arg); + } +} diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java index 760572ec255..e29948d9409 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java @@ -47,21 +47,27 @@ class NonTerminalAroundInvokeInvocationContext extends AroundInvokeInvocationCon public NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, Object[] parameters, Set interceptorBindings, List chain, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { - this(target, method, proceed, parameters, new HashMap<>(), interceptorBindings, 0, chain, + this(target, method, proceed, MethodInvoker.of(proceed), parameters, interceptorBindings, chain, currentHandler); + } + + NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, + Object[] parameters, Set interceptorBindings, + List chain, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { + this(target, method, proceed, proceedInvoker, parameters, new HashMap<>(), interceptorBindings, 0, chain, currentHandler); } public NonTerminalAroundInvokeInvocationContext(NonTerminalAroundInvokeInvocationContext ctx) { - this(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.getParameters(), ctx.contextData, + this(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.proceedInvoker, ctx.getParameters(), ctx.contextData, ctx.getInterceptorBindings(), ctx.position + 1, ctx.chain, ctx.currentHandler); } - private NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, Object[] parameters, - Map contextData, + private NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, + MethodInvoker proceedInvoker, Object[] parameters, Map contextData, Set interceptorBindings, int position, List chain, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { - super(target, method, proceed, parameters, contextData, interceptorBindings, currentHandler); + super(target, method, proceed, proceedInvoker, parameters, contextData, interceptorBindings, currentHandler); this.position = position; this.chain = chain; } diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java index 46f520117a9..baec6860cc2 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java @@ -41,18 +41,30 @@ class TerminalAroundInvokeInvocationContext extends AroundInvokeInvocationContex public TerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, Object[] parameters, Map contextData, Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { - super(target, method, proceed, parameters, (contextData == null) ? null : new HashMap(contextData), - interceptorBindings, currentHandler); + this(target, method, proceed, MethodInvoker.of(proceed), parameters, contextData, interceptorBindings, + currentHandler); + } + + TerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, + Object[] parameters, Map contextData, + Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { + super(target, method, proceed, proceedInvoker, parameters, + (contextData == null) ? null : new HashMap(contextData), interceptorBindings, currentHandler); } public TerminalAroundInvokeInvocationContext(NonTerminalAroundInvokeInvocationContext ctx) { - super(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.getParameters(), ctx.contextData, + super(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.proceedInvoker, ctx.getParameters(), ctx.contextData, ctx.getInterceptorBindings(), ctx.currentHandler); } @Override public Object proceedInternal() throws Exception { - return getProceed().invoke(getTarget(), getParameters()); + // same semantics as getProceed().invoke(getTarget(), getParameters()) + MethodInvoker invoker = proceedInvoker; + if (invoker == null) { + return getProceed().invoke(getTarget(), getParameters()); + } + return invoker.invoke(getProceed(), getTarget(), getParameters()); } @Override diff --git a/impl/src/main/java/org/jboss/weld/interceptor/reader/AbstractInterceptorMetadata.java b/impl/src/main/java/org/jboss/weld/interceptor/reader/AbstractInterceptorMetadata.java index 3bee96de650..54e64639ae8 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/reader/AbstractInterceptorMetadata.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/reader/AbstractInterceptorMetadata.java @@ -21,8 +21,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReferenceArray; import org.jboss.weld.interceptor.proxy.InterceptorInvocation; +import org.jboss.weld.interceptor.proxy.MethodInvoker; import org.jboss.weld.interceptor.spi.metadata.InterceptorMetadata; import org.jboss.weld.interceptor.spi.model.InterceptionType; @@ -34,8 +36,12 @@ public abstract class AbstractInterceptorMetadata implements InterceptorMetadata protected final Map> interceptorMethodMap; + // invokers of the interceptor methods, per interception type (ordinal); created lazily upon first invocation + private final AtomicReferenceArray interceptorMethodInvokers; + public AbstractInterceptorMetadata(Map> interceptorMethodMap) { this.interceptorMethodMap = interceptorMethodMap; + this.interceptorMethodInvokers = new AtomicReferenceArray<>(InterceptionType.values().length); } public List getInterceptorMethods(InterceptionType interceptionType) { @@ -59,8 +65,22 @@ public boolean isEligible(InterceptionType interceptionType) { @Override public InterceptorInvocation getInterceptorInvocation(Object interceptorInstance, InterceptionType interceptionType) { - return new SimpleInterceptorInvocation(interceptorInstance, interceptionType, getInterceptorMethods(interceptionType), - isTargetClassInterceptor()); + List interceptorMethods = getInterceptorMethods(interceptionType); + return new SimpleInterceptorInvocation(interceptorInstance, interceptionType, interceptorMethods, + getInterceptorMethodInvokers(interceptionType, interceptorMethods), isTargetClassInterceptor()); + } + + private MethodInvoker[] getInterceptorMethodInvokers(InterceptionType interceptionType, List interceptorMethods) { + MethodInvoker[] invokers = interceptorMethodInvokers.get(interceptionType.ordinal()); + if (invokers == null) { + invokers = new MethodInvoker[interceptorMethods.size()]; + for (int i = 0; i < invokers.length; i++) { + invokers[i] = MethodInvoker.of(interceptorMethods.get(i)); + } + // a concurrent initialization creates an equivalent array + interceptorMethodInvokers.set(interceptionType.ordinal(), invokers); + } + return invokers; } protected abstract boolean isTargetClassInterceptor(); diff --git a/impl/src/main/java/org/jboss/weld/interceptor/reader/SimpleInterceptorInvocation.java b/impl/src/main/java/org/jboss/weld/interceptor/reader/SimpleInterceptorInvocation.java index 10912f5edc8..fc4a8153048 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/reader/SimpleInterceptorInvocation.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/reader/SimpleInterceptorInvocation.java @@ -24,6 +24,7 @@ import org.jboss.weld.interceptor.proxy.InterceptorInvocation; import org.jboss.weld.interceptor.proxy.InterceptorMethodInvocation; +import org.jboss.weld.interceptor.proxy.MethodInvoker; import org.jboss.weld.interceptor.spi.model.InterceptionType; import org.jboss.weld.util.collections.ImmutableList; @@ -39,6 +40,14 @@ class SimpleInterceptorInvocation implements InterceptorInvocation { public SimpleInterceptorInvocation(Object instance, InterceptionType interceptionType, List interceptorMethods, boolean targetClass) { + this(instance, interceptionType, interceptorMethods, null, targetClass); + } + + /** + * @param interceptorMethodInvokers the invokers of the given interceptor methods (same order), may be null + */ + SimpleInterceptorInvocation(Object instance, InterceptionType interceptionType, List interceptorMethods, + MethodInvoker[] interceptorMethodInvokers, boolean targetClass) { this.instance = instance; this.interceptionType = interceptionType; this.targetClass = targetClass; @@ -46,16 +55,22 @@ public SimpleInterceptorInvocation(Object instance, InterceptionType interceptio if (interceptorMethods.size() == 1) { // Very often there will be only one interceptor method interceptorMethodInvocations = ImmutableList - . of(new SimpleMethodInvocation(interceptorMethods.get(0))); + . of(new SimpleMethodInvocation(interceptorMethods.get(0), + invoker(interceptorMethodInvokers, 0, interceptorMethods))); } else { ImmutableList.Builder builder = ImmutableList.builder(); - for (Method method : interceptorMethods) { - builder.add(new SimpleMethodInvocation(method)); + for (int i = 0; i < interceptorMethods.size(); i++) { + builder.add(new SimpleMethodInvocation(interceptorMethods.get(i), + invoker(interceptorMethodInvokers, i, interceptorMethods))); } interceptorMethodInvocations = builder.build(); } } + private static MethodInvoker invoker(MethodInvoker[] invokers, int i, List interceptorMethods) { + return invokers != null ? invokers[i] : MethodInvoker.of(interceptorMethods.get(i)); + } + @Override public List getInterceptorMethodInvocations() { return interceptorMethodInvocations; @@ -64,17 +79,20 @@ public List getInterceptorMethodInvocations() { class SimpleMethodInvocation implements InterceptorMethodInvocation { private final Method method; + private final MethodInvoker invoker; - SimpleMethodInvocation(Method method) { + SimpleMethodInvocation(Method method, MethodInvoker invoker) { this.method = method; + this.invoker = invoker; } @Override public Object invoke(InvocationContext invocationContext) throws Exception { + // same semantics as method.invoke(instance, invocationContext) / method.invoke(instance) if (invocationContext != null) { - return method.invoke(instance, invocationContext); + return invoker.invoke(method, instance, invocationContext); } else { - return method.invoke(instance); + return invoker.invoke(method, instance); } } diff --git a/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java b/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java new file mode 100644 index 00000000000..108609358b1 --- /dev/null +++ b/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java @@ -0,0 +1,187 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2026, Red Hat, Inc. and/or its affiliates, and individual + * contributors by the @authors tag. See the copyright.txt in the + * distribution for a full listing of individual contributors. + * + * Licensed 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.jboss.weld.interceptor.proxy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.junit.Test; + +/** + * Verifies that {@link MethodInvoker} behaves exactly like {@link Method#invoke(Object, Object...)}: same results, same + * exception types (and causes) in all the cases, including those where the method handle cannot be used. + */ +public class MethodInvokerTest { + + public static class Target { + + public String echo(String value) { + return value; + } + + public int add(int a, int b) { + return a + b; + } + + public long widen(long value) { + return value; + } + + public Object object(Object value) { + return value; + } + + public String noArgs() { + return "noArgs"; + } + + public void nothing() { + } + + public Object fail(Throwable throwable) throws Throwable { + throw throwable; + } + + private String secret(String value) { + return "secret " + value; + } + + public static String staticMethod(String value) { + return "static " + value; + } + } + + public static class SubTarget extends Target { + + @Override + public String echo(String value) { + return "sub " + value; + } + } + + private interface Invocation { + Object invoke() throws Exception; + } + + private static String outcome(Invocation invocation) { + try { + return "result: " + invocation.invoke(); + } catch (InvocationTargetException e) { + return "InvocationTargetException caused by " + e.getCause(); + } catch (Exception e) { + return e.getClass().getName(); + } + } + + private static void assertSameOutcome(Method method, Object target, Object[] args) { + MethodInvoker invoker = MethodInvoker.of(method); + String expected = outcome(() -> method.invoke(target, args)); + assertEquals(expected, outcome(() -> invoker.invoke(method, target, args))); + if (args != null && args.length == 1) { + assertEquals(expected, outcome(() -> invoker.invoke(method, target, args[0]))); + } + if (args != null && args.length == 0) { + assertEquals(expected, outcome(() -> invoker.invoke(method, target))); + } + } + + private static Method method(String name, Class... parameterTypes) throws NoSuchMethodException { + return Target.class.getDeclaredMethod(name, parameterTypes); + } + + @Test + public void testResults() throws Exception { + Target target = new Target(); + assertSameOutcome(method("echo", String.class), target, new Object[] { "foo" }); + assertSameOutcome(method("echo", String.class), target, new Object[] { null }); + assertSameOutcome(method("echo", String.class), new SubTarget(), new Object[] { "foo" }); + assertSameOutcome(method("add", int.class, int.class), target, new Object[] { 1, 2 }); + assertSameOutcome(method("widen", long.class), target, new Object[] { 1L }); + assertSameOutcome(method("object", Object.class), target, new Object[] { 1 }); + assertSameOutcome(method("noArgs"), target, new Object[0]); + assertSameOutcome(method("nothing"), target, new Object[0]); + assertSameOutcome(method("nothing"), target, null); + assertSameOutcome(method("staticMethod", String.class), target, new Object[] { "foo" }); + assertSameOutcome(method("staticMethod", String.class), null, new Object[] { "foo" }); + Method secret = method("secret", String.class); + secret.setAccessible(true); + assertSameOutcome(secret, target, new Object[] { "foo" }); + } + + @Test + public void testIllegalArguments() throws Exception { + Target target = new Target(); + // widening primitive conversion (reflection only) + assertSameOutcome(method("widen", long.class), target, new Object[] { 1 }); + assertSameOutcome(method("add", int.class, int.class), target, new Object[] { (short) 1, (byte) 2 }); + // wrong types + assertSameOutcome(method("echo", String.class), target, new Object[] { 1 }); + assertSameOutcome(method("add", int.class, int.class), target, new Object[] { 1L, 2 }); + assertSameOutcome(method("add", int.class, int.class), target, new Object[] { "1", 2 }); + // null for a primitive parameter + assertSameOutcome(method("add", int.class, int.class), target, new Object[] { null, 2 }); + // wrong number of arguments + assertSameOutcome(method("add", int.class, int.class), target, new Object[] { 1 }); + assertSameOutcome(method("echo", String.class), target, new Object[0]); + assertSameOutcome(method("echo", String.class), target, null); + assertSameOutcome(method("noArgs"), target, new Object[] { 1 }); + // wrong or null receiver + assertSameOutcome(method("echo", String.class), "not a target", new Object[] { "foo" }); + assertSameOutcome(method("echo", String.class), null, new Object[] { "foo" }); + assertSameOutcome(method("noArgs"), null, new Object[0]); + } + + @Test + public void testExceptionsThrownByTheMethod() throws Exception { + Target target = new Target(); + Method fail = method("fail", Throwable.class); + assertSameOutcome(fail, target, new Object[] { new IOException("checked") }); + assertSameOutcome(fail, target, new Object[] { new IllegalStateException("unchecked") }); + assertSameOutcome(fail, target, new Object[] { new IllegalArgumentException("thrown by the method") }); + assertSameOutcome(fail, target, new Object[] { new NullPointerException("thrown by the method") }); + assertSameOutcome(fail, target, new Object[] { new ClassCastException("thrown by the method") }); + assertSameOutcome(fail, target, new Object[] { new AssertionError("error") }); + assertSameOutcome(fail, target, + new Object[] { new InvocationTargetException(new IOException("wrapped by the method")) }); + assertSameOutcome(fail, target, new Object[] { new Throwable("plain throwable") }); + } + + @Test + public void testExceptionIdentity() throws Exception { + IOException exception = new IOException(); + Method fail = method("fail", Throwable.class); + try { + MethodInvoker.of(fail).invoke(fail, new Target(), (Object) exception); + } catch (InvocationTargetException e) { + assertSame(exception, e.getCause()); + return; + } + throw new AssertionError("InvocationTargetException expected"); + } + + @Test + public void testCache() throws Exception { + Method echo = method("echo", String.class); + assertSame(MethodInvoker.of(echo), MethodInvoker.of(method("echo", String.class))); + assertNull(MethodInvoker.of(null)); + } +} From 59355145d4e2bd1fadca8e91a52009583a5fe34c Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 14:59:43 +0200 Subject: [PATCH 09/17] Skip the per-call accessibility and interceptor method checks for cached interception chains InterceptorMethodHandler.invoke() called Reflections.ensureAccessible() (AccessibleObject.canAccess()) and TargetClassInterceptorMetadata .isInterceptorMethod() (a hash set lookup) upon every invocation, before looking up the cached interception chain. A cached chain implies that both checks have been performed already for the given method and proceed method (the chain is created after them), and neither result can change, so the chain is now looked up first and used directly. --- .../proxy/InterceptorMethodHandler.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java index 424be377ed6..f2698915cb9 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java @@ -44,6 +44,22 @@ public Object invoke(Object self, Method thisMethod, Method proceed, Object[] ar } public Object invoke(Stack stack, Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { + if (proceed != null && getClass() == InterceptorMethodHandler.class) { + // fast path for an around invoke interception whose chain has been created already + CachedInterceptionChain chain = cachedChains.get(thisMethod); + if (chain != null && chain.proceed == proceed) { + /* + * A cached chain implies that (1) thisMethod is not an interceptor method of the target class and (2) the + * given proceed method has been made accessible already (if needed), when the chain was created by the + * slow path below. Neither can change for a given proceed method and target instance. + */ + if (chain.interceptorMethods.isEmpty()) { + // shortcut if there are no interceptors + return Reflections.invokeAndUnwrap(self, proceed, args); + } + return executeAroundInvoke(self, thisMethod, proceed, args, chain, stack); + } + } Reflections.ensureAccessible(proceed, self); if (proceed == null) { if (thisMethod.getName().equals(InterceptionUtils.POST_CONSTRUCT)) { From b268c8840059ca093042e96b1612a517548a75c6 Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 15:00:01 +0200 Subject: [PATCH 10/17] Remember the interception chain of the first intercepted method of an instance Avoids the ConcurrentHashMap lookup in InterceptorMethodHandler for the first intercepted method that has been invoked on a given instance (e.g. a bean with a single intercepted method). The field is set at most once, the chain is immutable (final fields only), other methods keep using the map. --- .../proxy/InterceptorMethodHandler.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java index f2698915cb9..8d8a444ceb4 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java @@ -32,6 +32,11 @@ public class InterceptorMethodHandler implements StackAwareMethodHandler, Serial private final InterceptionContext ctx; private final transient ConcurrentMap cachedChains; + /* + * The chain of the first intercepted method, avoids the map lookup for beans with a single (or a main) intercepted + * method. Set at most once; the chain is immutable (final fields only) so it is safely published by the plain write. + */ + private transient CachedInterceptionChain firstChain; public InterceptorMethodHandler(InterceptionContext ctx) { this.ctx = ctx; @@ -46,7 +51,10 @@ public Object invoke(Object self, Method thisMethod, Method proceed, Object[] ar public Object invoke(Stack stack, Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { if (proceed != null && getClass() == InterceptorMethodHandler.class) { // fast path for an around invoke interception whose chain has been created already - CachedInterceptionChain chain = cachedChains.get(thisMethod); + CachedInterceptionChain chain = firstChain; + if (chain == null || chain.method != thisMethod) { + chain = cachedChains.get(thisMethod); + } if (chain != null && chain.proceed == proceed) { /* * A cached chain implies that (1) thisMethod is not an interceptor method of the target class and (2) the @@ -119,16 +127,18 @@ private CachedInterceptionChain getInterceptionChain(Object instance, Method met cachedChain = new CachedInterceptionChain( ctx.buildInterceptorMethodInvocations(instance, method, interceptionType), ctx.getInterceptionModel() .getMemberInterceptorBindings(method), - proceed); + method, proceed); CachedInterceptionChain old = cachedChains.putIfAbsent(method, cachedChain); if (old != null) { cachedChain = old; + } else if (firstChain == null) { + firstChain = cachedChain; } } return cachedChain; } return new CachedInterceptionChain(ctx.buildInterceptorMethodInvocations(instance, null, interceptionType), - ctx.getInterceptionModel().getClassInterceptorBindings(), proceed); + ctx.getInterceptionModel().getClassInterceptorBindings(), null, proceed); } private boolean isInterceptorMethod(Method method) { @@ -143,14 +153,16 @@ private static class CachedInterceptionChain { private final List interceptorMethods; private final Set interceptorBindings; - // the proceed method the chain was created for and its invoker + // the intercepted method (null for lifecycle callbacks), the proceed method the chain was created for and its invoker + private final Method method; private final Method proceed; private final MethodInvoker proceedInvoker; public CachedInterceptionChain(List chain, Set interceptorBindings, - Method proceed) { + Method method, Method proceed) { this.interceptorMethods = chain; this.interceptorBindings = interceptorBindings; + this.method = method; this.proceed = proceed; // only needed if there are around invoke interceptors this.proceedInvoker = chain.isEmpty() ? null : MethodInvoker.of(proceed); From 799fb44aa6dc6f6d3e369338c8869f51cb51121b Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 15:01:03 +0200 Subject: [PATCH 11/17] Remove the unused timer and constructor fields from around invoke invocation contexts The timer field of AbstractInvocationContext was always null and the constructor field is only needed for around construct interception (SimpleInvocationContext). Moving the latter to SimpleInvocationContext reduces the size of the InvocationContext allocated upon every intercepted invocation by 8 bytes (compressed oops), which compensates for the method handle invoker field. No behavior change: getTimer() returned null before as well, getConstructor() and the parameter checks are unchanged. --- .../proxy/AbstractInvocationContext.java | 20 +++++++------------ .../proxy/SimpleInvocationContext.java | 13 ++++++++++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/AbstractInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AbstractInvocationContext.java index 13a61508978..981fb192aeb 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/AbstractInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AbstractInvocationContext.java @@ -37,8 +37,8 @@ abstract class AbstractInvocationContext implements InvocationContext { protected final Method method; protected Object[] parameters; protected final Object target; - protected final Object timer; - protected final Constructor constructor; + // no timer field: timeouts are not intercepted by this implementation, getTimer() always returns null + // no constructor field: only needed for around construct interception, see SimpleInvocationContext protected final Set interceptorBindings; protected final Method proceed; @@ -58,17 +58,10 @@ abstract class AbstractInvocationContext implements InvocationContext { protected AbstractInvocationContext(Object target, Method method, Method proceed, Object[] parameters, Map contextData, Set interceptorBindings) { - this(target, method, proceed, null, parameters, null, contextData, interceptorBindings); - } - - protected AbstractInvocationContext(Object target, Method method, Method proceed, Constructor constructor, - Object[] parameters, Object timer, Map contextData, Set interceptorBindings) { this.target = target; this.method = method; this.proceed = proceed; - this.constructor = constructor; this.parameters = parameters; - this.timer = timer; this.contextData = contextData; this.interceptorBindings = interceptorBindings != null ? interceptorBindings : Collections.emptySet(); } @@ -89,7 +82,7 @@ public Method getMethod() { @Override @SuppressFBWarnings("EI_EXPOSE_REP") public Object[] getParameters() { - if (this.method != null || this.constructor != null) { + if (this.method != null || getConstructor() != null) { return parameters; } else { throw new IllegalStateException("Illegal invocation to getParameters() during lifecycle invocation"); @@ -114,7 +107,8 @@ private static boolean isWideningPrimitive(Class argumentClass, Class targ @SuppressFBWarnings("EI_EXPOSE_REP") public void setParameters(Object[] params) { - if (this.method != null || this.constructor != null) { + Constructor constructor = getConstructor(); + if (this.method != null || constructor != null) { // there is no requirement to do anything if params is null // but this is theoretically possible only if the target method has no arguments int newParametersCount = params == null ? 0 : params.length; @@ -192,12 +186,12 @@ private void throwIAE(int i, Class methodParameterClass, Class newArgument @Override public Object getTimer() { - return timer; + return null; } @Override public Constructor getConstructor() { - return constructor; + return null; } @Override diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/SimpleInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/SimpleInvocationContext.java index 934892c8d04..2abfcc8153d 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/SimpleInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/SimpleInvocationContext.java @@ -34,14 +34,23 @@ */ public class SimpleInvocationContext extends AbstractInvocationContext { + private final Constructor constructor; + public SimpleInvocationContext(Object target, Method targetMethod, Method proceed, Object[] parameters, Set interceptorBindings) { - super(target, targetMethod, proceed, null, parameters, null, null, interceptorBindings); + super(target, targetMethod, proceed, parameters, null, interceptorBindings); + this.constructor = null; } public SimpleInvocationContext(Constructor constructor, Object[] parameters, Map contextData, Set interceptorBindings) { - super(null, null, null, constructor, parameters, null, contextData, interceptorBindings); + super(null, null, null, parameters, contextData, interceptorBindings); + this.constructor = constructor; + } + + @Override + public Constructor getConstructor() { + return constructor; } @Override From 4d95fdd806e41a37e3cc623f15ff338d87d6a274 Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 15:01:31 +0200 Subject: [PATCH 12/17] Do not allocate an empty arguments array for intercepted methods without parameters The intercepted subclass passes a shared, immutable empty array to the method handler for methods without parameters instead of allocating a new one upon every invocation (InvocationContext.getParameters() returns an empty array as before; setParameters() replaces the array, it never writes into it). --- ...binedInterceptorAndDecoratorStackMethodHandler.java | 5 +++++ .../weld/bean/proxy/InterceptedSubclassFactory.java | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/CombinedInterceptorAndDecoratorStackMethodHandler.java b/impl/src/main/java/org/jboss/weld/bean/proxy/CombinedInterceptorAndDecoratorStackMethodHandler.java index a92d7b1dfa2..69ab9276839 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/CombinedInterceptorAndDecoratorStackMethodHandler.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/CombinedInterceptorAndDecoratorStackMethodHandler.java @@ -15,6 +15,11 @@ */ public class CombinedInterceptorAndDecoratorStackMethodHandler implements StackAwareMethodHandler, Serializable { + /** + * The (immutable) arguments array passed to the handler by intercepted subclasses for methods without parameters. + */ + public static final Object[] NO_ARGUMENTS = new Object[0]; + public static final CombinedInterceptorAndDecoratorStackMethodHandler NULL_INSTANCE = new CombinedInterceptorAndDecoratorStackMethodHandler() { @Override public void setInterceptorMethodHandler(InterceptorMethodHandler interceptorMethodHandler) { diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptedSubclassFactory.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptedSubclassFactory.java index 970bbb06348..48a0992a78d 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptedSubclassFactory.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptedSubclassFactory.java @@ -494,8 +494,14 @@ protected void invokeMethodHandler(ClassMethod method, MethodInformation methodI b.aconstNull(); } - b.iconst(methodInfo.getParameterTypes().length); - b.anewarray(Object.class.getName()); + if (methodInfo.getParameterTypes().length == 0) { + // share an empty array instead of allocating one upon every invocation + b.getstatic(COMBINED_INTERCEPTOR_AND_DECORATOR_STACK_METHOD_HANDLER_CLASS_NAME, "NO_ARGUMENTS", + "[" + LJAVA_LANG_OBJECT); + } else { + b.iconst(methodInfo.getParameterTypes().length); + b.anewarray(Object.class.getName()); + } int localVariableCount = 1; From 225a1f495cda2f8b4fd57a4075f4159138e3befc Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 15:03:58 +0200 Subject: [PATCH 13/17] Avoid the thread-local lookup in InvocationContext.proceed() on the invoking thread An around invoke InvocationContext now keeps the interception stack of the thread that created it. proceed() uses it directly if it is called on the same thread (Thread.currentThread() comparison) instead of looking up the stack in the thread-local again. While the context references the stack, it is guaranteed to be the stack getStack() returns on that thread (see InterceptionDecorationContext.getStack()), so the behavior is the same. On another thread (e.g. asynchronous proceed), the thread-local lookup is performed as before. The additional field does not increase the size of the context (it fits into the padding left by the removal of the timer/constructor fields). --- .../proxy/InterceptionDecorationContext.java | 30 +++++++++++++++++++ .../proxy/AroundInvokeInvocationContext.java | 12 +++++--- ...TerminalAroundInvokeInvocationContext.java | 15 ++++++---- ...TerminalAroundInvokeInvocationContext.java | 11 ++++--- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java index 93a3384ede2..5cca7db4a52 100644 --- a/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java +++ b/impl/src/main/java/org/jboss/weld/bean/proxy/InterceptionDecorationContext.java @@ -82,12 +82,15 @@ public class InterceptionDecorationContext { public static class Stack { private static final int INITIAL_CAPACITY = 4; private final Object[] holder; + // the thread this stack belongs to + private final Thread owner; // elements[0] is the bottom of the stack, elements[size - 1] the top; unused slots are always null private CombinedInterceptorAndDecoratorStackMethodHandler[] elements; private int size; private Stack(Object[] holder) { this.holder = holder; + this.owner = Thread.currentThread(); // the stack is usually very shallow this.elements = new CombinedInterceptorAndDecoratorStackMethodHandler[INITIAL_CAPACITY]; } @@ -166,6 +169,14 @@ public int size() { return size; } + /** + * @return true if this is the stack of the current thread (the stack a {@link #getStack()} call on the current thread + * returns as long as the caller holds a reference to this stack) + */ + public boolean isOwnedByCurrentThread() { + return owner == Thread.currentThread(); + } + @Override public String toString() { // top of the stack first @@ -275,6 +286,25 @@ public static Stack startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHa return null; } + /** + * Same as {@link #startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHandler)} but avoids the thread-local lookup + * if the given stack (obtained earlier and still referenced by the caller) is the stack of the current thread. + * + * @param stack a stack obtained by {@link #getStack()}, possibly on another thread, may be null + * @param context the given context + * @return the stack if the given context was pushed to it, null if the given context was on top already + */ + public static Stack startIfNotOnTop(Stack stack, CombinedInterceptorAndDecoratorStackMethodHandler context) { + if (stack == null || !stack.isOwnedByCurrentThread()) { + // e.g. InvocationContext.proceed() called on another thread + return startIfNotOnTop(context); + } + if (stack.startIfNotOnTop(context)) { + return stack; + } + return null; + } + /** * Gets the current Stack. If there is no stack for the current thread, a new empty instance is created. * The returned stack may be empty; it is only guaranteed to stay the stack of the current thread while the caller diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java index 8db8d7fbfc0..35cd94da3e5 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java @@ -68,28 +68,32 @@ static AroundInvokeInvocationContext create(Object instance, Method method, Meth CombinedInterceptorAndDecoratorStackMethodHandler currentHandler = (stack == null) ? null : stack.peek(); if (chain.size() == 1) { return new TerminalAroundInvokeInvocationContext(instance, method, proceed, proceedInvoker, args, null, - interceptorBindings, currentHandler); + interceptorBindings, currentHandler, stack); } else { return new NonTerminalAroundInvokeInvocationContext(instance, method, proceed, proceedInvoker, args, - interceptorBindings, chain, currentHandler); + interceptorBindings, chain, currentHandler, stack); } } final CombinedInterceptorAndDecoratorStackMethodHandler currentHandler; // invokes the proceed method final MethodInvoker proceedInvoker; + // the stack of the thread that created this context (may be null), avoids the thread-local lookup upon proceed() + final Stack creatorStack; AroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, Object[] parameters, Map contextData, - Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { + Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler, + Stack stack) { super(target, method, proceed, parameters, contextData, interceptorBindings); this.proceedInvoker = proceedInvoker; this.currentHandler = currentHandler; + this.creatorStack = stack; } @Override public Object proceed() throws Exception { - final Stack stack = InterceptionDecorationContext.startIfNotOnTop(currentHandler); + final Stack stack = InterceptionDecorationContext.startIfNotOnTop(creatorStack, currentHandler); try { return proceedInternal(); } catch (InvocationTargetException e) { diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java index e29948d9409..32596c3b85e 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/NonTerminalAroundInvokeInvocationContext.java @@ -26,6 +26,7 @@ import jakarta.interceptor.InvocationContext; import org.jboss.weld.bean.proxy.CombinedInterceptorAndDecoratorStackMethodHandler; +import org.jboss.weld.bean.proxy.InterceptionDecorationContext.Stack; /** * The non-terminal {@link InvocationContext} in the interception chain. This implementation is used for the first n-1 @@ -47,27 +48,29 @@ class NonTerminalAroundInvokeInvocationContext extends AroundInvokeInvocationCon public NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, Object[] parameters, Set interceptorBindings, List chain, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { - this(target, method, proceed, MethodInvoker.of(proceed), parameters, interceptorBindings, chain, currentHandler); + this(target, method, proceed, MethodInvoker.of(proceed), parameters, interceptorBindings, chain, currentHandler, + null); } NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, Object[] parameters, Set interceptorBindings, - List chain, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { + List chain, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler, + Stack stack) { this(target, method, proceed, proceedInvoker, parameters, new HashMap<>(), interceptorBindings, 0, chain, - currentHandler); + currentHandler, stack); } public NonTerminalAroundInvokeInvocationContext(NonTerminalAroundInvokeInvocationContext ctx) { this(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.proceedInvoker, ctx.getParameters(), ctx.contextData, ctx.getInterceptorBindings(), ctx.position + 1, - ctx.chain, ctx.currentHandler); + ctx.chain, ctx.currentHandler, ctx.creatorStack); } private NonTerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, Object[] parameters, Map contextData, Set interceptorBindings, int position, List chain, - CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { - super(target, method, proceed, proceedInvoker, parameters, contextData, interceptorBindings, currentHandler); + CombinedInterceptorAndDecoratorStackMethodHandler currentHandler, Stack stack) { + super(target, method, proceed, proceedInvoker, parameters, contextData, interceptorBindings, currentHandler, stack); this.position = position; this.chain = chain; } diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java index baec6860cc2..9c4fe844ff8 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/TerminalAroundInvokeInvocationContext.java @@ -25,6 +25,7 @@ import jakarta.interceptor.InvocationContext; import org.jboss.weld.bean.proxy.CombinedInterceptorAndDecoratorStackMethodHandler; +import org.jboss.weld.bean.proxy.InterceptionDecorationContext.Stack; /** * The terminal {@link InvocationContext} in the interception chain. It is passed to the last interceptor in the chain and @@ -42,19 +43,21 @@ public TerminalAroundInvokeInvocationContext(Object target, Method method, Metho Map contextData, Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { this(target, method, proceed, MethodInvoker.of(proceed), parameters, contextData, interceptorBindings, - currentHandler); + currentHandler, null); } TerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, Object[] parameters, Map contextData, - Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { + Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler, + Stack stack) { super(target, method, proceed, proceedInvoker, parameters, - (contextData == null) ? null : new HashMap(contextData), interceptorBindings, currentHandler); + (contextData == null) ? null : new HashMap(contextData), interceptorBindings, currentHandler, + stack); } public TerminalAroundInvokeInvocationContext(NonTerminalAroundInvokeInvocationContext ctx) { super(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.proceedInvoker, ctx.getParameters(), ctx.contextData, - ctx.getInterceptorBindings(), ctx.currentHandler); + ctx.getInterceptorBindings(), ctx.currentHandler, ctx.creatorStack); } @Override From c4a0dcca3e260aac5658de90d79e31b1b808d70f Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Mon, 5 Oct 2026 09:49:02 +0200 Subject: [PATCH 14/17] Preserve method access overrides in cached invokers and cover stack handoff --- .../proxy/InterceptorMethodHandler.java | 3 +- .../weld/interceptor/proxy/MethodInvoker.java | 16 ++++-- .../InterceptionDecorationContextTest.java | 54 +++++++++++++++++++ .../interceptor/proxy/MethodInvokerTest.java | 44 +++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java index 8d8a444ceb4..4b19efaa87c 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java @@ -34,7 +34,8 @@ public class InterceptorMethodHandler implements StackAwareMethodHandler, Serial private final transient ConcurrentMap cachedChains; /* * The chain of the first intercepted method, avoids the map lookup for beans with a single (or a main) intercepted - * method. Set at most once; the chain is immutable (final fields only) so it is safely published by the plain write. + * method. Racing initializations may select different chains; final fields safely publish either result. + * A stale or missing entry only causes a fallback to the concurrent map. */ private transient CachedInterceptionChain firstChain; diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java index 693b8f7ebfd..945ccf25631 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java @@ -85,6 +85,7 @@ public static MethodInvoker of(Method method) { } private final Class receiverType; + private final boolean accessChecksSuppressed; // the expected classes of the arguments; the wrapper class for a primitive parameter private final Class[] argumentTypes; private final boolean[] primitive; @@ -93,7 +94,9 @@ public static MethodInvoker of(Method method) { // (Object, Object)Object, null if the method does not declare exactly one parameter or reflection has to be used private final MethodHandle singleArgumentHandle; + @SuppressWarnings("deprecation") private MethodInvoker(Method method) { + this.accessChecksSuppressed = method.isAccessible(); this.receiverType = method.getDeclaringClass(); Class[] parameterTypes = method.getParameterTypes(); this.argumentTypes = new Class[parameterTypes.length]; @@ -128,7 +131,7 @@ private MethodInvoker(Method method) { */ public Object invoke(Method method, Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { MethodHandle handle = spreadHandle; - if (handle != null && receiverType.isInstance(target) && accepts(args)) { + if (handle != null && permitsCachedAccess(method) && receiverType.isInstance(target) && accepts(args)) { try { return (Object) handle.invokeExact(target, args); } catch (Throwable e) { @@ -145,7 +148,7 @@ public Object invoke(Method method, Object target, Object[] args) throws Illegal */ public Object invoke(Method method, Object target, Object arg) throws IllegalAccessException, InvocationTargetException { MethodHandle handle = singleArgumentHandle; - if (handle != null && receiverType.isInstance(target) && accepts(0, arg)) { + if (handle != null && permitsCachedAccess(method) && receiverType.isInstance(target) && accepts(0, arg)) { try { return (Object) handle.invokeExact(target, arg); } catch (Throwable e) { @@ -162,7 +165,7 @@ public Object invoke(Method method, Object target, Object arg) throws IllegalAcc */ public Object invoke(Method method, Object target) throws IllegalAccessException, InvocationTargetException { MethodHandle handle = spreadHandle; - if (handle != null && argumentTypes.length == 0 && receiverType.isInstance(target)) { + if (handle != null && permitsCachedAccess(method) && argumentTypes.length == 0 && receiverType.isInstance(target)) { try { return (Object) handle.invokeExact(target, NO_ARGUMENTS); } catch (Throwable e) { @@ -172,6 +175,13 @@ public Object invoke(Method method, Object target) throws IllegalAccessException return method.invoke(target); } + @SuppressWarnings("deprecation") + private boolean permitsCachedAccess(Method method) { + // Method.equals() ignores the access override. A handle created with suppressed access checks must not + // authorize an equal Method without that override, or survive setAccessible(false) on the original Method. + return !accessChecksSuppressed || method.isAccessible(); + } + private boolean accepts(Object[] args) { if (args == null || args.length != argumentTypes.length) { return false; diff --git a/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java b/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java index 5f3a3079d34..46231451711 100644 --- a/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java +++ b/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; @@ -26,12 +27,65 @@ import java.util.EmptyStackException; import java.util.NoSuchElementException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; import org.jboss.weld.bean.proxy.InterceptionDecorationContext.Stack; import org.junit.Test; public class InterceptionDecorationContextTest { + @Test + public void testCachedStackOnOwnerThread() { + Stack stack = InterceptionDecorationContext.getStack(); + CombinedInterceptorAndDecoratorStackMethodHandler handler = new CombinedInterceptorAndDecoratorStackMethodHandler(); + assertTrue(stack.isOwnedByCurrentThread()); + assertSame(stack, InterceptionDecorationContext.startIfNotOnTop(stack, handler)); + try { + assertNull(InterceptionDecorationContext.startIfNotOnTop(stack, handler)); + assertEquals(1, stack.size()); + } finally { + stack.end(); + } + // A saved invocation context can proceed again after the original invocation has returned. + assertSame(stack, InterceptionDecorationContext.startIfNotOnTop(stack, handler)); + stack.end(); + assertTrue(InterceptionDecorationContext.empty()); + } + + @Test + public void testCachedStackOnAnotherThread() throws Exception { + CombinedInterceptorAndDecoratorStackMethodHandler ownerHandler = new CombinedInterceptorAndDecoratorStackMethodHandler(); + CombinedInterceptorAndDecoratorStackMethodHandler workerHandler = new CombinedInterceptorAndDecoratorStackMethodHandler(); + Stack ownerStack = InterceptionDecorationContext.startIfNotOnTop(ownerHandler); + FutureTask task = new FutureTask<>(() -> { + assertFalse(ownerStack.isOwnedByCurrentThread()); + Stack workerStack = InterceptionDecorationContext.startIfNotOnTop(ownerStack, workerHandler); + try { + assertNotSame(ownerStack, workerStack); + assertTrue(workerStack.isOwnedByCurrentThread()); + assertSame(workerHandler, InterceptionDecorationContext.peek()); + assertNull(InterceptionDecorationContext.startIfNotOnTop(ownerStack, workerHandler)); + assertEquals(1, workerStack.size()); + } finally { + workerStack.end(); + } + assertTrue(InterceptionDecorationContext.empty()); + return null; + }); + Thread worker = new Thread(task, "interception-stack-test"); + worker.setDaemon(true); + try { + worker.start(); + task.get(10, TimeUnit.SECONDS); + assertSame(ownerHandler, ownerStack.peek()); + assertEquals(1, ownerStack.size()); + } finally { + ownerStack.end(); + } + assertTrue(InterceptionDecorationContext.empty()); + } + @Test public void testStackSemantics() { assertTrue(InterceptionDecorationContext.empty()); diff --git a/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java b/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java index 108609358b1..e2a0bd8a730 100644 --- a/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java +++ b/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -65,6 +66,14 @@ private String secret(String value) { return "secret " + value; } + private String restricted(String value) { + return value; + } + + private String restrictedNoArgs() { + return "restricted"; + } + public static String staticMethod(String value) { return "static " + value; } @@ -178,6 +187,41 @@ public void testExceptionIdentity() throws Exception { throw new AssertionError("InvocationTargetException expected"); } + @Test + public void testCachedHandleDoesNotTransferAccessOverride() throws Exception { + Target target = new Target(); + Method allowed = method("restricted", String.class); + allowed.setAccessible(true); + MethodInvoker invoker = MethodInvoker.of(allowed); + assertEquals("value", invoker.invoke(allowed, target, (Object) "value")); + + Method restricted = method("restricted", String.class); + assertSame(invoker, MethodInvoker.of(restricted)); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(restricted, target, (Object) "value")); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(restricted, target, new Object[] { "value" })); + + allowed.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(allowed, target, (Object) "value")); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(allowed, target, new Object[] { "value" })); + allowed.setAccessible(true); + assertEquals("value", invoker.invoke(allowed, target, (Object) "value")); + } + + @Test + public void testNoArgumentHandleRespectsAccessOverride() throws Exception { + Target target = new Target(); + Method allowed = method("restrictedNoArgs"); + allowed.setAccessible(true); + MethodInvoker invoker = MethodInvoker.of(allowed); + assertEquals("restricted", invoker.invoke(allowed, target)); + Method restricted = method("restrictedNoArgs"); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(restricted, target)); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(restricted, target, new Object[0])); + allowed.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(allowed, target)); + assertThrows(IllegalAccessException.class, () -> invoker.invoke(allowed, target, new Object[0])); + } + @Test public void testCache() throws Exception { Method echo = method("echo", String.class); From 76c1b6ad9cc57691c82bd85ab4d77db6fa2b979c Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Sun, 4 Oct 2026 15:07:53 +0200 Subject: [PATCH 15/17] Invoke simple methods through LambdaMetafactory generated functions For methods with a non-void return type and no parameter or a single reference type parameter (e.g. @AroundInvoke interceptor methods and WeldSubclass.getter$$super()), MethodInvoker creates a Function/BiFunction via LambdaMetafactory (in the package of the declaring class, requires the package to be open to Weld) so that the JIT can inline the invocation, which is not possible with a method handle stored in a field. Same argument checks and exception wrapping as for the method handle; if the lambda cannot be created, the method handle (or reflection) is used. --- .../weld/interceptor/proxy/MethodInvoker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java index 945ccf25631..94872088f02 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java @@ -16,6 +16,8 @@ */ package org.jboss.weld.interceptor.proxy; +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -24,6 +26,8 @@ import java.lang.reflect.Modifier; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; +import java.util.function.Function; import org.jboss.weld.util.Primitives; @@ -93,6 +97,13 @@ public static MethodInvoker of(Method method) { private final MethodHandle spreadHandle; // (Object, Object)Object, null if the method does not declare exactly one parameter or reflection has to be used private final MethodHandle singleArgumentHandle; + /* + * Lambdas (LambdaMetafactory) invoking the method directly, for methods with a non-void return type and no parameter or + * a single reference type parameter (e.g. @AroundInvoke methods and getters), null if not applicable or not possible. + * Unlike a method handle stored in a field, a call through them can be inlined by the JIT. + */ + private final Function noArgumentFunction; + private final BiFunction singleArgumentFunction; @SuppressWarnings("deprecation") private MethodInvoker(Method method) { @@ -122,6 +133,47 @@ private MethodInvoker(Method method) { this.spreadHandle = null; this.singleArgumentHandle = null; } + Object function = handle != null ? createFunction(method, parameterTypes) : null; + this.noArgumentFunction = parameterTypes.length == 0 ? asFunction(function) : null; + this.singleArgumentFunction = parameterTypes.length == 1 ? asBiFunction(function) : null; + } + + @SuppressWarnings("unchecked") + private static Function asFunction(Object function) { + return (Function) function; + } + + @SuppressWarnings("unchecked") + private static BiFunction asBiFunction(Object function) { + return (BiFunction) function; + } + + private static Object createFunction(Method method, Class[] parameterTypes) { + Class returnType = method.getReturnType(); + if (returnType == void.class || parameterTypes.length > 1 + || (parameterTypes.length == 1 && parameterTypes[0].isPrimitive())) { + return null; + } + try { + // requires the package of the declaring class to be open to Weld; the lambda class is defined in that package + MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(method.getDeclaringClass(), MethodHandles.lookup()); + MethodHandle implementation = lookup.unreflect(method); + Class boxedReturnType = Primitives.wrap(returnType); + CallSite callSite; + if (parameterTypes.length == 0) { + callSite = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class), + MethodType.methodType(Object.class, Object.class), implementation, + MethodType.methodType(boxedReturnType, method.getDeclaringClass())); + } else { + callSite = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(BiFunction.class), + MethodType.methodType(Object.class, Object.class, Object.class), implementation, + MethodType.methodType(boxedReturnType, method.getDeclaringClass(), parameterTypes[0])); + } + return callSite.getTarget().invoke(); + } catch (Throwable e) { + // e.g. a package not open to Weld, a security manager - use the method handle + return null; + } } /** @@ -130,6 +182,14 @@ private MethodInvoker(Method method) { * @param method the method this invoker was created for (or an equal one), invoked if the method handle cannot be used */ public Object invoke(Method method, Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { + Function function = noArgumentFunction; + if (function != null && args != null && args.length == 0 && receiverType.isInstance(target)) { + try { + return function.apply(target); + } catch (Throwable e) { + throw new InvocationTargetException(e); + } + } MethodHandle handle = spreadHandle; if (handle != null && permitsCachedAccess(method) && receiverType.isInstance(target) && accepts(args)) { try { @@ -147,6 +207,14 @@ public Object invoke(Method method, Object target, Object[] args) throws Illegal * @param method the method this invoker was created for (or an equal one), invoked if the method handle cannot be used */ public Object invoke(Method method, Object target, Object arg) throws IllegalAccessException, InvocationTargetException { + BiFunction function = singleArgumentFunction; + if (function != null && receiverType.isInstance(target) && accepts(0, arg)) { + try { + return function.apply(target, arg); + } catch (Throwable e) { + throw new InvocationTargetException(e); + } + } MethodHandle handle = singleArgumentHandle; if (handle != null && permitsCachedAccess(method) && receiverType.isInstance(target) && accepts(0, arg)) { try { From 6661eeafa8a9f8a9cb7674a7397745b28214f215 Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Mon, 5 Oct 2026 09:58:57 +0200 Subject: [PATCH 16/17] Apply access-override guard to lambda invokers --- .../java/org/jboss/weld/interceptor/proxy/MethodInvoker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java index 94872088f02..e525f5365d6 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java @@ -183,7 +183,7 @@ private static Object createFunction(Method method, Class[] parameterTypes) { */ public Object invoke(Method method, Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { Function function = noArgumentFunction; - if (function != null && args != null && args.length == 0 && receiverType.isInstance(target)) { + if (function != null && permitsCachedAccess(method) && args != null && args.length == 0 && receiverType.isInstance(target)) { try { return function.apply(target); } catch (Throwable e) { @@ -208,7 +208,7 @@ public Object invoke(Method method, Object target, Object[] args) throws Illegal */ public Object invoke(Method method, Object target, Object arg) throws IllegalAccessException, InvocationTargetException { BiFunction function = singleArgumentFunction; - if (function != null && receiverType.isInstance(target) && accepts(0, arg)) { + if (function != null && permitsCachedAccess(method) && receiverType.isInstance(target) && accepts(0, arg)) { try { return function.apply(target, arg); } catch (Throwable e) { From 38f94f38cb2ea46c46913696148fc9b2fe28016c Mon Sep 17 00:00:00 2001 From: Angelo Rubens Date: Mon, 5 Oct 2026 10:03:34 +0200 Subject: [PATCH 17/17] Apply Weld formatter output from CI to lambda access guard --- .../java/org/jboss/weld/interceptor/proxy/MethodInvoker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java index e525f5365d6..aea3dbddfec 100644 --- a/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java @@ -183,7 +183,8 @@ private static Object createFunction(Method method, Class[] parameterTypes) { */ public Object invoke(Method method, Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { Function function = noArgumentFunction; - if (function != null && permitsCachedAccess(method) && args != null && args.length == 0 && receiverType.isInstance(target)) { + if (function != null && permitsCachedAccess(method) && args != null && args.length == 0 + && receiverType.isInstance(target)) { try { return function.apply(target); } catch (Throwable e) {