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/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/ContextBeanInstance.java b/impl/src/main/java/org/jboss/weld/bean/proxy/ContextBeanInstance.java index eca7240de43..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,11 +54,14 @@ 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; // 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; @@ -71,19 +75,25 @@ 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); 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); + // 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; } 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; 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..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 @@ -17,12 +17,14 @@ package org.jboss.weld.bean.proxy; -import java.util.ArrayDeque; -import java.util.Deque; +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.Arrays; import java.util.EmptyStackException; - -import org.jboss.weld.contexts.cache.RequestScopedCache; -import org.jboss.weld.contexts.cache.RequestScopedItem; +import java.util.NoSuchElementException; /** * A class that holds the interception (and decoration) contexts which are currently in progress. @@ -35,30 +37,62 @@ *

* 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 { - private boolean removeWhenEmpty; - private final Deque elements; - private final ThreadLocal interceptionContexts; - private boolean valid; - - private Stack(ThreadLocal interceptionContexts) { - this.interceptionContexts = interceptionContexts; - this.elements = new ArrayDeque(); - /* - * 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; + + // 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(); + + /* + * 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 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]; } /** @@ -70,9 +104,13 @@ 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); + int s = size; + if (s == 0) { + push(context, 0); + return true; + } + if (elements[s - 1] != context) { + push(context, s); return true; } return false; @@ -82,57 +120,74 @@ public void end() { pop(); } - private void push(CombinedInterceptorAndDecoratorStackMethodHandler item) { - checkState(); - elements.addFirst(item); + 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; + } } - public CombinedInterceptorAndDecoratorStackMethodHandler peek() { - checkState(); - return elements.peekFirst(); + private CombinedInterceptorAndDecoratorStackMethodHandler[] grow() { + CombinedInterceptorAndDecoratorStackMethodHandler[] es = Arrays.copyOf(elements, elements.length << 1); + elements = es; + return es; } - private CombinedInterceptorAndDecoratorStackMethodHandler pop() { - checkState(); - CombinedInterceptorAndDecoratorStackMethodHandler top = elements.removeFirst(); - removeIfEmpty(); - return top; + public CombinedInterceptorAndDecoratorStackMethodHandler peek() { + int s = size; + return s == 0 ? null : elements[s - 1]; } - private void checkState() { - if (!valid) { - throw new IllegalStateException("This InterceptionDecorationContext is no longer valid."); + private CombinedInterceptorAndDecoratorStackMethodHandler pop() { + int s = size - 1; + if (s < 0) { + throw new NoSuchElementException(); } - } - - @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()) { - interceptionContexts.remove(); - valid = false; + 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; } + return top; } public int size() { - return elements.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() { - return "Stack [valid=" + valid + ", cached=" + !removeWhenEmpty + ", 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(); } } @@ -140,6 +195,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. * @@ -147,7 +210,7 @@ private InterceptionDecorationContext() { * @throws EmptyStackException */ public static CombinedInterceptorAndDecoratorStackMethodHandler peek() { - return peek(interceptionContexts.get()); + return peek(activeStack()); } /** @@ -156,7 +219,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; } @@ -167,11 +230,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()); } /** @@ -187,15 +250,24 @@ public static void endInterceptorContext() { * returned value. */ public static Stack startIfNotEmpty() { - Stack stack = getStack(); - 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(); + 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) return null; } + stack.push(CombinedInterceptorAndDecoratorStackMethodHandler.NULL_INSTANCE, stack.size); + return stack; } /** @@ -215,19 +287,61 @@ public static Stack startIfNotOnTop(CombinedInterceptorAndDecoratorStackMethodHa } /** - * Gets the current Stack. If the stack is not set, a new empty instance is created and set. + * 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 + * 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) { + if (!holderCreated) { + noHolderCreatedAnymore(); + } + 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; } + 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(); @@ -243,12 +357,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(); - } - } } 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 1ee531345ad..ba3d7d35e3c 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 WeldInvocationContext { 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 WeldInvocationContext { 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(); } @@ -95,7 +88,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"); @@ -120,7 +113,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; @@ -198,12 +192,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/AroundInvokeInvocationContext.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/AroundInvokeInvocationContext.java index 71d2a9e00dc..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 @@ -60,28 +60,40 @@ 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, stack); } else { - return new NonTerminalAroundInvokeInvocationContext(instance, method, proceed, args, interceptorBindings, chain, - currentHandler); + return new NonTerminalAroundInvokeInvocationContext(instance, method, proceed, proceedInvoker, args, + 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, Object[] parameters, - Map contextData, - Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler) { + AroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, + Object[] parameters, Map contextData, + 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/InterceptorMethodHandler.java b/impl/src/main/java/org/jboss/weld/interceptor/proxy/InterceptorMethodHandler.java index 93449e7c463..bd05dc31571 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 @@ -30,6 +30,12 @@ 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. 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; public InterceptorMethodHandler(InterceptionContext ctx) { this.ctx = ctx; @@ -42,6 +48,25 @@ 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 = 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 + * 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)) { @@ -60,7 +85,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) { @@ -84,8 +109,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 { - org.jboss.weld.interceptor.WeldInvocationContext ctx = create(instance, method, proceed, args, chain.interceptorMethods, - chain.interceptorBindings, stack); + org.jboss.weld.interceptor.WeldInvocationContext 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) { @@ -93,22 +118,26 @@ 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), + 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()); + ctx.getInterceptionModel().getClassInterceptorBindings(), null, proceed); } private boolean isInterceptorMethod(Method method) { @@ -123,10 +152,24 @@ private static class CachedInterceptionChain { private final List interceptorMethods; private final Set interceptorBindings; + // 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) { + public CachedInterceptionChain(List chain, Set interceptorBindings, + 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); + } + + 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..aea3dbddfec --- /dev/null +++ b/impl/src/main/java/org/jboss/weld/interceptor/proxy/MethodInvoker.java @@ -0,0 +1,273 @@ +/* + * 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.CallSite; +import java.lang.invoke.LambdaMetafactory; +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 java.util.function.BiFunction; +import java.util.function.Function; + +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; + 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; + // (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; + /* + * 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) { + this.accessChecksSuppressed = method.isAccessible(); + 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; + } + 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; + } + } + + /** + * 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 { + Function function = noArgumentFunction; + if (function != null && permitsCachedAccess(method) && 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 { + 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 { + BiFunction function = singleArgumentFunction; + if (function != null && permitsCachedAccess(method) && 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 { + 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 && permitsCachedAccess(method) && 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); + } + + @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; + } + 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 6f5f236dba4..5b7c56c61c3 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 @@ -25,6 +25,7 @@ import jakarta.interceptor.InvocationContext; import org.jboss.weld.bean.proxy.CombinedInterceptorAndDecoratorStackMethodHandler; +import org.jboss.weld.bean.proxy.InterceptionDecorationContext.Stack; import org.jboss.weld.interceptor.WeldInvocationContext; /** @@ -47,21 +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, parameters, newContextData(interceptorBindings), interceptorBindings, 0, 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, + Stack stack) { + this(target, method, proceed, proceedInvoker, parameters, newContextData(interceptorBindings), interceptorBindings, + 0, chain, currentHandler, stack); } 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); + ctx.chain, ctx.currentHandler, ctx.creatorStack); } - 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); + 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/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 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..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 @@ -41,18 +42,32 @@ 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, null); + } + + TerminalAroundInvokeInvocationContext(Object target, Method method, Method proceed, MethodInvoker proceedInvoker, + Object[] parameters, Map contextData, + Set interceptorBindings, CombinedInterceptorAndDecoratorStackMethodHandler currentHandler, + Stack stack) { + super(target, method, proceed, proceedInvoker, parameters, + (contextData == null) ? null : new HashMap(contextData), interceptorBindings, currentHandler, + stack); } public TerminalAroundInvokeInvocationContext(NonTerminalAroundInvokeInvocationContext ctx) { - super(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.getParameters(), ctx.contextData, - ctx.getInterceptorBindings(), ctx.currentHandler); + super(ctx.getTarget(), ctx.getMethod(), ctx.getProceed(), ctx.proceedInvoker, ctx.getParameters(), ctx.contextData, + ctx.getInterceptorBindings(), ctx.currentHandler, ctx.creatorStack); } @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/bean/proxy/InterceptionDecorationContextTest.java b/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java new file mode 100644 index 00000000000..46231451711 --- /dev/null +++ b/impl/src/test/java/org/jboss/weld/bean/proxy/InterceptionDecorationContextTest.java @@ -0,0 +1,148 @@ +/* + * 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.assertNotSame; +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 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()); + 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()); + } +} 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..e2a0bd8a730 --- /dev/null +++ b/impl/src/test/java/org/jboss/weld/interceptor/proxy/MethodInvokerTest.java @@ -0,0 +1,231 @@ +/* + * 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 static org.junit.Assert.assertThrows; + +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; + } + + private String restricted(String value) { + return value; + } + + private String restrictedNoArgs() { + return "restricted"; + } + + 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 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); + assertSame(MethodInvoker.of(echo), MethodInvoker.of(method("echo", String.class))); + assertNull(MethodInvoker.of(null)); + } +}