From 43ba1139cce239e75c46ce833bd81d383438b125 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Sun, 3 May 2026 21:19:26 +0200 Subject: [PATCH 1/7] Bump Weld API to 7.0.Alpha7 and adapt to CDI 5.0 API changes --- .../ExternalBeanAttributesFactory.java | 2 +- .../attributes/ImmutableBeanAttributes.java | 19 ++- .../BeanAttributesConfiguratorImpl.java | 13 +- .../configurator/BeanConfiguratorImpl.java | 6 + .../invokable/AbstractInvokerBuilder.java | 4 +- .../weld/invokable/AsyncHandlerRegistry.java | 132 +++++++++--------- .../weld/invokable/AsyncInvokerImpl.java | 65 ++++----- .../util/bean/ForwardingBeanAttributes.java | 5 + pom.xml | 2 +- .../async/broken/BothAnnotationsHandler.java | 9 +- .../async/broken/RawAsyncHandler.java | 3 +- .../async/broken/UnannotatedHandler.java | 6 +- .../async/custom/MyAsyncTypeHandler.java | 3 +- .../async/paramtype/MyAsyncParamHandler.java | 5 +- .../extension/translator/BeanInfoImpl.java | 5 + .../translator/StereotypeInfoImpl.java | 6 + .../translator/SyntheticBeanBuilderImpl.java | 17 +++ 17 files changed, 182 insertions(+), 120 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/bean/attributes/ExternalBeanAttributesFactory.java b/impl/src/main/java/org/jboss/weld/bean/attributes/ExternalBeanAttributesFactory.java index c80acc26e3..162fa91ff0 100644 --- a/impl/src/main/java/org/jboss/weld/bean/attributes/ExternalBeanAttributesFactory.java +++ b/impl/src/main/java/org/jboss/weld/bean/attributes/ExternalBeanAttributesFactory.java @@ -52,7 +52,7 @@ private ExternalBeanAttributesFactory() { public static BeanAttributes of(BeanAttributes source, BeanManager manager) { validateBeanAttributes(source, manager); BeanAttributes attributes = new ImmutableBeanAttributes(defensiveCopy(source.getStereotypes()), - source.isAlternative(), source.isReserve(), source.isEager(), source.getName(), + source.isAlternative(), source.isReserve(), source.isEager(), source.isAutoClose(), source.getName(), defensiveCopy(source.getQualifiers()), defensiveCopy(source.getTypes()), source.getScope()); return attributes; } diff --git a/impl/src/main/java/org/jboss/weld/bean/attributes/ImmutableBeanAttributes.java b/impl/src/main/java/org/jboss/weld/bean/attributes/ImmutableBeanAttributes.java index 928680a882..f325cd3006 100644 --- a/impl/src/main/java/org/jboss/weld/bean/attributes/ImmutableBeanAttributes.java +++ b/impl/src/main/java/org/jboss/weld/bean/attributes/ImmutableBeanAttributes.java @@ -37,6 +37,7 @@ public class ImmutableBeanAttributes implements BeanAttributes { private final boolean alternative; private final boolean reserve; private final boolean eager; + private final boolean autoClose; private final String name; private final Set qualifiers; private final Set types; @@ -44,15 +45,22 @@ public class ImmutableBeanAttributes implements BeanAttributes { public ImmutableBeanAttributes(Set> stereotypes, boolean alternative, boolean reserve, String name, Set qualifiers, Set types, Class scope) { - this(stereotypes, alternative, reserve, false, name, qualifiers, types, scope); + this(stereotypes, alternative, reserve, false, false, name, qualifiers, types, scope); } public ImmutableBeanAttributes(Set> stereotypes, boolean alternative, boolean reserve, boolean eager, String name, Set qualifiers, Set types, Class scope) { + this(stereotypes, alternative, reserve, eager, false, name, qualifiers, types, scope); + } + + public ImmutableBeanAttributes(Set> stereotypes, boolean alternative, boolean reserve, + boolean eager, boolean autoClose, String name, Set qualifiers, Set types, + Class scope) { this.stereotypes = stereotypes; this.alternative = alternative; this.reserve = reserve; this.eager = eager; + this.autoClose = autoClose; this.name = name; this.qualifiers = qualifiers; this.types = types; @@ -63,8 +71,8 @@ public ImmutableBeanAttributes(Set> stereotypes, boo * Utility constructor used for overriding Bean qualifiers and name for specialization purposes. */ public ImmutableBeanAttributes(Set qualifiers, String name, BeanAttributes attributes) { - this(attributes.getStereotypes(), attributes.isAlternative(), attributes.isReserve(), attributes.isEager(), name, - qualifiers, attributes.getTypes(), attributes.getScope()); + this(attributes.getStereotypes(), attributes.isAlternative(), attributes.isReserve(), attributes.isEager(), + attributes.isAutoClose(), name, qualifiers, attributes.getTypes(), attributes.getScope()); } @Override @@ -87,6 +95,11 @@ public boolean isEager() { return eager; } + @Override + public boolean isAutoClose() { + return autoClose; + } + @Override public String getName() { return name; diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanAttributesConfiguratorImpl.java b/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanAttributesConfiguratorImpl.java index 8f3ce25969..a44ea64836 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanAttributesConfiguratorImpl.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanAttributesConfiguratorImpl.java @@ -69,6 +69,8 @@ public class BeanAttributesConfiguratorImpl implements BeanAttributesConfigur private boolean isEager; + private boolean isAutoClose; + public BeanAttributesConfiguratorImpl(BeanManagerImpl beanManager) { this.beanManager = beanManager; this.qualifiers = new HashSet(); @@ -94,7 +96,9 @@ public BeanAttributesConfigurator read(BeanAttributes beanAttributes) { stereotypes(beanAttributes.getStereotypes()); types(beanAttributes.getTypes()); alternative(beanAttributes.isAlternative()); + reserve(beanAttributes.isReserve()); eager(beanAttributes.isEager()); + autoClose(beanAttributes.isAutoClose()); return this; } @@ -236,9 +240,16 @@ public BeanAttributesConfigurator eager(boolean value) { return this; } + @Override + public BeanAttributesConfigurator autoClose(boolean value) { + this.isAutoClose = value; + return this; + } + @Override public BeanAttributes complete() { - return new ImmutableBeanAttributes(ImmutableSet.copyOf(stereotypes), isAlternative, isReserve, isEager, name, + return new ImmutableBeanAttributes(ImmutableSet.copyOf(stereotypes), isAlternative, isReserve, isEager, isAutoClose, + name, Bindings.normalizeBeanQualifiers(qualifiers), ImmutableSet.copyOf(types), initScope()); diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java b/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java index c52f17947f..c1a7357665 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java @@ -351,6 +351,12 @@ public BeanConfigurator eager(boolean value) { return this; } + @Override + public BeanConfigurator autoClose(boolean value) { + this.attributes.autoClose(value); + return this; + } + public Bean complete() { if (createCallback == null) { // no callback specified, Weld does not know how to instantiate this new custom bean diff --git a/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java b/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java index 5c0974e83c..d32a2549a7 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java @@ -423,10 +423,10 @@ InvokerInfo doBuild() { } if (returnTypeHandler != null) { - return new AsyncInvokerImpl<>(mh, returnTypeHandler.getHandler(), true, -1); + return new AsyncInvokerImpl<>(mh, returnTypeHandler, -1); } else if (paramTypeHandler != null) { int asyncParamIndex = findAsyncParamIndex(reflectionMethod.getParameterTypes(), paramTypeHandler.getAsyncType()); - return new AsyncInvokerImpl<>(mh, paramTypeHandler.getHandler(), false, asyncParamIndex); + return new AsyncInvokerImpl<>(mh, paramTypeHandler, asyncParamIndex); } return new InvokerImpl<>(mh); } diff --git a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java index 2ed0a00491..585f4a09b4 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java @@ -34,13 +34,13 @@ public class AsyncHandlerRegistry implements Service { */ public AsyncHandlerRegistry() { // Built-in handlers required by the spec - registerBuiltinHandler(CompletionStage.class, new BuiltinCompletionStageHandler(), true); - registerBuiltinHandler(CompletableFuture.class, new BuiltinCompletableFutureHandler(), true); - registerBuiltinHandler(Flow.Publisher.class, new BuiltinFlowPublisherHandler(), true); + registerBuiltinReturnTypeHandler(CompletionStage.class, new BuiltinCompletionStageHandler()); + registerBuiltinReturnTypeHandler(CompletableFuture.class, new BuiltinCompletableFutureHandler()); + registerBuiltinReturnTypeHandler(Flow.Publisher.class, new BuiltinFlowPublisherHandler()); } - private void registerBuiltinHandler(Class asyncType, AsyncHandler handler, boolean isReturnType) { - HandlerInfo info = new HandlerInfo(handler, asyncType, isReturnType); + private void registerBuiltinReturnTypeHandler(Class asyncType, AsyncHandler.ReturnType handler) { + HandlerInfo info = HandlerInfo.returnType(handler, asyncType); info.setBuiltin(true); handlers.put(asyncType, info); } @@ -55,70 +55,60 @@ public void discoverHandlers(ResourceLoader resourceLoader) { if (resourceLoader == null) { return; } - for (Metadata metadata : ServiceLoader.load(AsyncHandler.class, resourceLoader)) { - validateAndRegister(metadata.getValue()); + for (Metadata metadata : ServiceLoader.load(AsyncHandler.ReturnType.class, resourceLoader)) { + validateAndRegisterReturnType(metadata.getValue()); + } + for (Metadata metadata : ServiceLoader.load(AsyncHandler.ParameterType.class, + resourceLoader)) { + validateAndRegisterParameterType(metadata.getValue()); } } - private void validateAndRegister(AsyncHandler handler) { + private void validateAndRegisterReturnType(AsyncHandler.ReturnType handler) { Class handlerClass = handler.getClass(); + Class asyncType = extractAsyncType(handlerClass, AsyncHandler.ReturnType.class); + checkDuplicate(asyncType, handlerClass); + handlers.put(asyncType, HandlerInfo.returnType(handler, asyncType)); + } - // Must implement AsyncHandler directly (not through a subclass) - validateDirectImplementation(handlerClass); - - // Extract async type from AsyncHandler - Class asyncType = extractAsyncType(handlerClass); - - // Must be annotated with exactly one of @ReturnType or @ParameterType - boolean isReturnType = handlerClass.isAnnotationPresent(AsyncHandler.ReturnType.class); - boolean isParameterType = handlerClass.isAnnotationPresent(AsyncHandler.ParameterType.class); - - if (isReturnType && isParameterType) { - throw InvokerLogger.LOG.asyncHandlerBothAnnotations(handlerClass); - } - if (!isReturnType && !isParameterType) { - throw InvokerLogger.LOG.asyncHandlerNoAnnotation(handlerClass); - } + private void validateAndRegisterParameterType(AsyncHandler.ParameterType handler) { + Class handlerClass = handler.getClass(); + Class asyncType = extractAsyncType(handlerClass, AsyncHandler.ParameterType.class); + checkDuplicate(asyncType, handlerClass); + handlers.put(asyncType, HandlerInfo.parameterType(handler, asyncType)); + } - // Check for duplicate handlers for the same async type + private void checkDuplicate(Class asyncType, Class handlerClass) { HandlerInfo existing = handlers.get(asyncType); if (existing != null && !existing.isBuiltin()) { throw InvokerLogger.LOG.asyncHandlerDuplicate(asyncType, handlerClass); } - - // Custom handlers override built-in handlers for the same type - handlers.put(asyncType, new HandlerInfo(handler, asyncType, isReturnType)); } - private void validateDirectImplementation(Class handlerClass) { - // Must implement AsyncHandler directly, not through an abstract subclass - boolean directlyImplements = false; - for (Class iface : handlerClass.getInterfaces()) { - if (iface == AsyncHandler.class) { - directlyImplements = true; - break; - } - } - if (!directlyImplements) { - throw InvokerLogger.LOG.asyncHandlerIndirectImplementation(handlerClass); - } - } - - private Class extractAsyncType(Class handlerClass) { - // Find AsyncHandler in the direct superinterface types + private Class extractAsyncType(Class handlerClass, Class targetInterface) { for (Type genericInterface : handlerClass.getGenericInterfaces()) { if (genericInterface instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genericInterface; - if (pt.getRawType() == AsyncHandler.class) { + if (pt.getRawType() == targetInterface) { Type typeArg = pt.getActualTypeArguments()[0]; return validateAndEraseAsyncType(typeArg, handlerClass); } - } else if (genericInterface == AsyncHandler.class) { - // Raw AsyncHandler — deployment problem + } else if (genericInterface == targetInterface) { throw InvokerLogger.LOG.asyncHandlerRawType(handlerClass); } } - // Should not happen if validateDirectImplementation passed + // Check superclass interfaces recursively + for (Type genericInterface : handlerClass.getGenericInterfaces()) { + Class rawIface = null; + if (genericInterface instanceof ParameterizedType) { + rawIface = (Class) ((ParameterizedType) genericInterface).getRawType(); + } else if (genericInterface instanceof Class) { + rawIface = (Class) genericInterface; + } + if (rawIface != null && targetInterface.isAssignableFrom(rawIface)) { + return extractAsyncType(rawIface, targetInterface); + } + } throw InvokerLogger.LOG.asyncHandlerRawType(handlerClass); } @@ -155,7 +145,7 @@ public boolean hasHandler(Class asyncType) { } /** - * Finds a matching @ReturnType handler for the given method return type. + * Finds a matching ReturnType handler for the given method return type. */ public HandlerInfo findReturnTypeHandler(Class returnType) { HandlerInfo info = handlers.get(returnType); @@ -166,7 +156,7 @@ public HandlerInfo findReturnTypeHandler(Class returnType) { } /** - * Finds a matching @ParameterType handler for the given method parameter types. + * Finds a matching ParameterType handler for the given method parameter types. * Returns the handler info if exactly one parameter matches; null otherwise. */ public HandlerInfo findParameterTypeHandler(Class[] parameterTypes) { @@ -188,20 +178,37 @@ public void cleanup() { * Holds information about a registered async handler. */ public static class HandlerInfo { - private final AsyncHandler handler; + private final AsyncHandler.ReturnType returnTypeHandler; + private final AsyncHandler.ParameterType parameterTypeHandler; private final Class asyncType; - private final boolean returnType; + private final boolean isReturnType; private boolean builtin; - HandlerInfo(AsyncHandler handler, Class asyncType, boolean returnType) { - this.handler = handler; + static HandlerInfo returnType(AsyncHandler.ReturnType handler, Class asyncType) { + return new HandlerInfo(handler, null, asyncType, true); + } + + static HandlerInfo parameterType(AsyncHandler.ParameterType handler, Class asyncType) { + return new HandlerInfo(null, handler, asyncType, false); + } + + private HandlerInfo(AsyncHandler.ReturnType returnTypeHandler, + AsyncHandler.ParameterType parameterTypeHandler, + Class asyncType, boolean isReturnType) { + this.returnTypeHandler = returnTypeHandler; + this.parameterTypeHandler = parameterTypeHandler; this.asyncType = asyncType; - this.returnType = returnType; + this.isReturnType = isReturnType; + } + + @SuppressWarnings("unchecked") + public AsyncHandler.ReturnType getReturnTypeHandler() { + return (AsyncHandler.ReturnType) returnTypeHandler; } @SuppressWarnings("unchecked") - public AsyncHandler getHandler() { - return (AsyncHandler) handler; + public AsyncHandler.ParameterType getParameterTypeHandler() { + return (AsyncHandler.ParameterType) parameterTypeHandler; } public Class getAsyncType() { @@ -209,7 +216,7 @@ public Class getAsyncType() { } public boolean isReturnType() { - return returnType; + return isReturnType; } boolean isBuiltin() { @@ -223,16 +230,14 @@ void setBuiltin(boolean builtin) { // --- Built-in handlers --- - @AsyncHandler.ReturnType - static class BuiltinCompletionStageHandler implements AsyncHandler> { + static class BuiltinCompletionStageHandler implements AsyncHandler.ReturnType> { @Override public CompletionStage transform(CompletionStage original, Runnable completion) { return original.whenComplete((value, error) -> completion.run()); } } - @AsyncHandler.ReturnType - static class BuiltinCompletableFutureHandler implements AsyncHandler> { + static class BuiltinCompletableFutureHandler implements AsyncHandler.ReturnType> { @Override public CompletableFuture transform(CompletableFuture original, Runnable completion) { CompletableFuture result = new CompletableFuture<>(); @@ -248,8 +253,7 @@ public CompletableFuture transform(CompletableFuture original, Runnable co } } - @AsyncHandler.ReturnType - static class BuiltinFlowPublisherHandler implements AsyncHandler> { + static class BuiltinFlowPublisherHandler implements AsyncHandler.ReturnType> { @Override public Flow.Publisher transform(Flow.Publisher original, Runnable completion) { return subscriber -> original.subscribe(new Flow.Subscriber() { diff --git a/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java b/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java index 657954e20d..197ded10d7 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java @@ -10,74 +10,67 @@ * An invoker that applies an {@link AsyncHandler} to the method invocation, * deferring cleanup of dependent beans until the async operation completes. *

- * For {@code @ReturnType} handlers: after the method returns, the return value - * is passed to {@link AsyncHandler#transform(Object, Runnable)}, with the cleanup + * For {@link AsyncHandler.ReturnType} handlers: after the method returns, the return value + * is passed to {@link AsyncHandler.ReturnType#transform(Object, Runnable)}, with the cleanup * action as the completion callback. *

- * For {@code @ParameterType} handlers: before the method is called, the matching - * argument is passed to {@link AsyncHandler#transform(Object, Runnable)}, and the - * method is called with the transformed argument. + * For {@link AsyncHandler.ParameterType} handlers: before the method is called, the matching + * argument is passed to {@link AsyncHandler.ParameterType#transformArgument(Object, Runnable)}, + * and after the method returns, the return value is passed to + * {@link AsyncHandler.ParameterType#transformReturnValue(Object, Runnable)}. */ class AsyncInvokerImpl implements Invoker, InvokerInfo { private final MethodHandle mh; - private final AsyncHandler handler; - private final boolean isReturnType; - private final int asyncParamIndex; // for @ParameterType, -1 for @ReturnType + private final AsyncHandler.ReturnType returnTypeHandler; + private final AsyncHandler.ParameterType parameterTypeHandler; + private final int asyncParamIndex; // for ParameterType, -1 for ReturnType - @SuppressWarnings("unchecked") - AsyncInvokerImpl(MethodHandle mh, AsyncHandler handler, boolean isReturnType, int asyncParamIndex) { + AsyncInvokerImpl(MethodHandle mh, AsyncHandlerRegistry.HandlerInfo handlerInfo, int asyncParamIndex) { this.mh = mh; - this.handler = (AsyncHandler) handler; - this.isReturnType = isReturnType; - this.asyncParamIndex = asyncParamIndex; + if (handlerInfo.isReturnType()) { + this.returnTypeHandler = handlerInfo.getReturnTypeHandler(); + this.parameterTypeHandler = null; + this.asyncParamIndex = -1; + } else { + this.returnTypeHandler = null; + this.parameterTypeHandler = handlerInfo.getParameterTypeHandler(); + this.asyncParamIndex = asyncParamIndex; + } } @Override @SuppressWarnings("unchecked") public R invoke(T instance, Object[] arguments) throws Exception { try { - // Clear any stale ThreadLocal before invocation. The deferred cleanup - // in CleanupActions.runDeferred() will set CURRENT after the method - // handle chain completes successfully. + // Clear any stale ThreadLocal before invocation CleanupActions.CURRENT.remove(); - if (!isReturnType && asyncParamIndex >= 0 && arguments != null && asyncParamIndex < arguments.length) { - // @ParameterType: invoke the method, then retrieve deferred cleanup - // and apply handler.transform() to the argument with the cleanup callback. - // We need the MH chain to run first (to set up CleanupActions via lookups), - // but we need to transform the argument with the completion callback. - // - // Strategy: run the chain to get CleanupActions, then transform the argument, - // but this won't work because the method already ran with the original argument. - // - // Instead: we invoke the MH chain normally. The argument is already the async - // param value provided by the caller. The handler.transform() wraps it so that - // completion fires when the async param signals done. We transform BEFORE invoke. - // The deferred cleanup will be captured in CURRENT by runDeferred. + if (parameterTypeHandler != null && asyncParamIndex >= 0 + && arguments != null && asyncParamIndex < arguments.length) { + // ParameterType: invoke the method, then retrieve deferred cleanup R result = (R) mh.invoke(instance, arguments); CleanupActions ca = CleanupActions.CURRENT.get(); CleanupActions.CURRENT.remove(); if (ca != null) { - // Transform the async parameter's value with the cleanup callback. - // The handler registers the completion callback on the async parameter, - // so cleanup runs when the async parameter signals completion. - handler.transform(arguments[asyncParamIndex], ca::cleanup); + parameterTypeHandler.transformArgument(arguments[asyncParamIndex], ca::cleanup); + return (R) parameterTypeHandler.transformReturnValue(result, () -> { + }); } return result; } - // @ReturnType: invoke the method, then transform the return value + // ReturnType: invoke the method, then transform the return value R result = (R) mh.invoke(instance, arguments); CleanupActions ca = CleanupActions.CURRENT.get(); CleanupActions.CURRENT.remove(); if (ca != null) { - return (R) handler.transform(result, ca::cleanup); + return (R) returnTypeHandler.transform(result, ca::cleanup); } else { - return (R) handler.transform(result, () -> { + return (R) returnTypeHandler.transform(result, () -> { }); } } catch (ValueCarryingException e) { diff --git a/impl/src/main/java/org/jboss/weld/util/bean/ForwardingBeanAttributes.java b/impl/src/main/java/org/jboss/weld/util/bean/ForwardingBeanAttributes.java index f015cb59ec..959778670d 100644 --- a/impl/src/main/java/org/jboss/weld/util/bean/ForwardingBeanAttributes.java +++ b/impl/src/main/java/org/jboss/weld/util/bean/ForwardingBeanAttributes.java @@ -75,6 +75,11 @@ public boolean isEager() { return attributes().isEager(); } + @Override + public boolean isAutoClose() { + return attributes().isAutoClose(); + } + @Override public int hashCode() { return attributes().hashCode(); diff --git a/pom.xml b/pom.xml index 59c4fdb5d5..89fb4bcff3 100644 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ 4.9.8.2 4.9.8 7.9.0 - 7.0.Alpha6 + 7.0.Alpha7 5.1.0.Final diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java index 697ffc095f..efc817ef02 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java @@ -2,11 +2,14 @@ import jakarta.enterprise.invoke.AsyncHandler; -@AsyncHandler.ReturnType -@AsyncHandler.ParameterType -public class BothAnnotationsHandler implements AsyncHandler { +public class BothAnnotationsHandler implements AsyncHandler.ReturnType, AsyncHandler.ParameterType { @Override public T transform(T original, Runnable completion) { return original; } + + @Override + public T transformArgument(T original, Runnable completion) { + return original; + } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandler.java index b30d25841e..b130097c0b 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandler.java @@ -2,9 +2,8 @@ import jakarta.enterprise.invoke.AsyncHandler; -@AsyncHandler.ReturnType @SuppressWarnings("rawtypes") -public class RawAsyncHandler implements AsyncHandler { +public class RawAsyncHandler implements AsyncHandler.ReturnType { @Override public Object transform(Object original, Runnable completion) { return original; diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java index 8a656497b1..0ebbedfa85 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java @@ -2,8 +2,10 @@ import jakarta.enterprise.invoke.AsyncHandler; -// Missing both @ReturnType and @ParameterType -public class UnannotatedHandler implements AsyncHandler { +// With the new API, a handler must implement either ReturnType or ParameterType. +// This class implements neither — it is not a valid handler. +// TODO: revisit this test with the new async handler API +public class UnannotatedHandler implements AsyncHandler.ReturnType { @Override public T transform(T original, Runnable completion) { return original; diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/MyAsyncTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/MyAsyncTypeHandler.java index afabb48be6..6b3259120a 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/MyAsyncTypeHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/MyAsyncTypeHandler.java @@ -2,8 +2,7 @@ import jakarta.enterprise.invoke.AsyncHandler; -@AsyncHandler.ReturnType -public class MyAsyncTypeHandler implements AsyncHandler> { +public class MyAsyncTypeHandler implements AsyncHandler.ReturnType> { @Override public MyAsyncType transform(MyAsyncType original, Runnable completion) { return original.whenComplete(completion); diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java index f993a5a6fb..93a0f02ee0 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java @@ -2,10 +2,9 @@ import jakarta.enterprise.invoke.AsyncHandler; -@AsyncHandler.ParameterType -public class MyAsyncParamHandler implements AsyncHandler> { +public class MyAsyncParamHandler implements AsyncHandler.ParameterType> { @Override - public MyAsyncParam transform(MyAsyncParam original, Runnable completion) { + public MyAsyncParam transformArgument(MyAsyncParam original, Runnable completion) { original.whenComplete(completion); return original; } diff --git a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/BeanInfoImpl.java b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/BeanInfoImpl.java index b166157910..141b67152c 100644 --- a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/BeanInfoImpl.java +++ b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/BeanInfoImpl.java @@ -114,6 +114,11 @@ public boolean isEager() { return cdiBean.isEager(); } + @Override + public boolean isAutoClose() { + return cdiBean.isAutoClose(); + } + @Override public Integer priority() { if (cdiDeclaration instanceof jakarta.enterprise.inject.spi.AnnotatedType diff --git a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/StereotypeInfoImpl.java b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/StereotypeInfoImpl.java index 2ecba304d7..d99c9b517b 100644 --- a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/StereotypeInfoImpl.java +++ b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/StereotypeInfoImpl.java @@ -7,6 +7,7 @@ import java.util.Optional; import jakarta.annotation.Priority; +import jakarta.enterprise.context.AutoClose; import jakarta.enterprise.context.Eager; import jakarta.enterprise.context.NormalScope; import jakarta.enterprise.inject.Alternative; @@ -73,6 +74,11 @@ public boolean isEager() { return cdiDeclaration.isAnnotationPresent(Eager.class); } + @Override + public boolean isAutoClose() { + return cdiDeclaration.isAnnotationPresent(AutoClose.class); + } + @Override public Integer priority() { return cdiDeclaration.isAnnotationPresent(Priority.class) diff --git a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/SyntheticBeanBuilderImpl.java b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/SyntheticBeanBuilderImpl.java index f19b1b7ec2..112d4c935c 100644 --- a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/SyntheticBeanBuilderImpl.java +++ b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/SyntheticBeanBuilderImpl.java @@ -25,6 +25,7 @@ class SyntheticBeanBuilderImpl extends SyntheticComponentBuilderBase> stereotypes = new HashSet<>(); @@ -104,6 +105,12 @@ public SyntheticBeanBuilder eager(boolean isEager) { return this; } + @Override + public SyntheticBeanBuilder autoClose(boolean isAutoClose) { + this.isAutoClose = isAutoClose; + return this; + } + @Override public SyntheticBeanBuilder name(String name) { this.name = name; @@ -123,6 +130,11 @@ public SyntheticBeanBuilder stereotype(ClassInfo stereotypeAnnotation) { return this; } + @Override + public SyntheticBeanBuilder withInjectionPoint(Class type) { + return withInjectionPoint(type, new Annotation[0]); + } + @Override public SyntheticBeanBuilder withInjectionPoint(Class type, Annotation... qualifiers) { injectionPoints.add(new InjectionPointDeclaration(type, new HashSet<>(Arrays.asList(qualifiers)))); @@ -139,6 +151,11 @@ public SyntheticBeanBuilder withInjectionPoint(Class type, AnnotationInfo. return this; } + @Override + public SyntheticBeanBuilder withInjectionPoint(Type type) { + return withInjectionPoint(type, new Annotation[0]); + } + @Override public SyntheticBeanBuilder withInjectionPoint(Type type, Annotation... qualifiers) { java.lang.reflect.Type reflectionType = ((TypeImpl) type).reflection.getType(); From 3cadae8ad8cb42a01f454fb71013dd63c75dcda5 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Sun, 3 May 2026 21:21:25 +0200 Subject: [PATCH 2/7] Use no-qualifier `withInjectionPoint` overloads in tests --- .../AnnotationBuilderQualifierExtension.java | 4 +--- .../basic/BasicSyntheticInjectionPointExtension.java | 5 ++--- .../broken/NoMatchingBeanExtension.java | 4 +--- .../syntheticInjectionPoint/disposer/DisposerExtension.java | 4 +--- .../IndirectInjectionPointExtension.java | 4 +--- 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/annotationBuilder/AnnotationBuilderQualifierExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/annotationBuilder/AnnotationBuilderQualifierExtension.java index a6c918bd1e..f3caa090df 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/annotationBuilder/AnnotationBuilderQualifierExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/annotationBuilder/AnnotationBuilderQualifierExtension.java @@ -1,7 +1,5 @@ package org.jboss.weld.tests.bce.syntheticInjectionPoint.annotationBuilder; -import java.lang.annotation.Annotation; - import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.build.compatible.spi.AnnotationBuilder; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; @@ -14,7 +12,7 @@ public void synthesize(SyntheticComponents syn) { syn.addBean(SyntheticPojo.class) .type(SyntheticPojo.class) .scope(Dependent.class) - .withInjectionPoint(Service.class, new Annotation[0]) + .withInjectionPoint(Service.class) .withInjectionPoint(Service.class, AnnotationBuilder.of(Special.class).build()) .withInjectionPoint(Service.class, AnnotationBuilder.of(Tagged.class).member("value", "foo").build()) diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/basic/BasicSyntheticInjectionPointExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/basic/BasicSyntheticInjectionPointExtension.java index 5e99e0f2db..5c4b19bbb8 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/basic/BasicSyntheticInjectionPointExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/basic/BasicSyntheticInjectionPointExtension.java @@ -1,6 +1,5 @@ package org.jboss.weld.tests.bce.syntheticInjectionPoint.basic; -import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; @@ -43,7 +42,7 @@ public void register(SyntheticComponents syn, Types types) { .type(SyntheticResult.class) .qualifier(Scenario2aQualifier.class) .scope(ApplicationScoped.class) - .withInjectionPoint(Alpha.class, new Annotation[0]) + .withInjectionPoint(Alpha.class) .createWith(NewApiClassAnnotationCreator.class); // withInjectionPoint(Class, AnnotationInfo...) — 2 qualifiers @@ -80,7 +79,7 @@ public void register(SyntheticComponents syn, Types types) { .type(SyntheticResult.class) .qualifier(DependentCleanupQualifier.class) .scope(jakarta.enterprise.context.Dependent.class) - .withInjectionPoint(DependentHelper.class, new Annotation[0]) + .withInjectionPoint(DependentHelper.class) .createWith(DependentCleanupCreator.class); } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/broken/NoMatchingBeanExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/broken/NoMatchingBeanExtension.java index aa2a90df5b..e71a8d16fa 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/broken/NoMatchingBeanExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/broken/NoMatchingBeanExtension.java @@ -1,7 +1,5 @@ package org.jboss.weld.tests.bce.syntheticInjectionPoint.broken; -import java.lang.annotation.Annotation; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; import jakarta.enterprise.inject.build.compatible.spi.Synthesis; @@ -16,7 +14,7 @@ public void register(SyntheticComponents syn) { syn.addBean(String.class) .type(String.class) .scope(ApplicationScoped.class) - .withInjectionPoint(NoSuchBean.class, new Annotation[0]) + .withInjectionPoint(NoSuchBean.class) .createWith(DummyCreator.class); } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/disposer/DisposerExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/disposer/DisposerExtension.java index 4d056c4c64..3b8c4e6565 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/disposer/DisposerExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/disposer/DisposerExtension.java @@ -1,7 +1,5 @@ package org.jboss.weld.tests.bce.syntheticInjectionPoint.disposer; -import java.lang.annotation.Annotation; - import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; import jakarta.enterprise.inject.build.compatible.spi.Synthesis; @@ -24,7 +22,7 @@ public void register(SyntheticComponents syn) { .type(DisposableResult.class) .qualifier(NewDisposerQualifier.class) .scope(Dependent.class) - .withInjectionPoint(DisposerHelper.class, new Annotation[0]) + .withInjectionPoint(DisposerHelper.class) .createWith(NewDisposerCreator.class) .disposeWith(NewApiDisposer.class); } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/indirectInjectionPoint/IndirectInjectionPointExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/indirectInjectionPoint/IndirectInjectionPointExtension.java index aae0a47dbc..52caddc7fa 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/indirectInjectionPoint/IndirectInjectionPointExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/indirectInjectionPoint/IndirectInjectionPointExtension.java @@ -1,7 +1,5 @@ package org.jboss.weld.tests.bce.syntheticInjectionPoint.indirectInjectionPoint; -import java.lang.annotation.Annotation; - import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; import jakarta.enterprise.inject.build.compatible.spi.Synthesis; @@ -13,7 +11,7 @@ public void synthesize(SyntheticComponents syn) { syn.addBean(SyntheticPojo.class) .type(SyntheticPojo.class) .scope(Dependent.class) - .withInjectionPoint(InjectionPointCaptor.class, new Annotation[0]) + .withInjectionPoint(InjectionPointCaptor.class) .createWith(IndirectInjectionPointCreator.class); } } From 3dee8298fb420f86bfb734ad6c301756ef3cac49 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Sun, 3 May 2026 21:32:37 +0200 Subject: [PATCH 3/7] Add tests for @Dependent lifecycle in SyntheticInjections --- .../dependentLifecycle/DependentCounter.java | 32 ++++++ .../DependentLifecycleTest.java | 101 ++++++++++++++++++ .../dependentLifecycle/LifecycleCreator.java | 14 +++ .../dependentLifecycle/LifecycleDisposer.java | 14 +++ .../LifecycleExtension.java | 18 ++++ .../dependentLifecycle/SyntheticBean.java | 13 +++ .../SyntheticResultHolder.java | 14 +++ 7 files changed, 206 insertions(+) create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentCounter.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentLifecycleTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleCreator.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleDisposer.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleExtension.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticResultHolder.java diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentCounter.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentCounter.java new file mode 100644 index 0000000000..39a377d00f --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentCounter.java @@ -0,0 +1,32 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.Dependent; + +@Dependent +public class DependentCounter { + public static final AtomicInteger createdCounter = new AtomicInteger(0); + public static final AtomicInteger destroyedCounter = new AtomicInteger(0); + + private final int id; + + public DependentCounter() { + id = createdCounter.incrementAndGet(); + } + + public int getId() { + return id; + } + + @PreDestroy + public void destroy() { + destroyedCounter.incrementAndGet(); + } + + public static void reset() { + createdCounter.set(0); + destroyedCounter.set(0); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentLifecycleTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentLifecycleTest.java new file mode 100644 index 0000000000..9a19366ba6 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/DependentLifecycleTest.java @@ -0,0 +1,101 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.spi.BeanContainer; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests {@code @Dependent} bean lifecycle for beans obtained via + * {@code SyntheticInjections} in synthetic bean creators and disposers. + */ +@RunWith(Arquillian.class) +public class DependentLifecycleTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(DependentLifecycleTest.class)) + .addPackage(DependentLifecycleTest.class.getPackage()) + .addAsServiceProvider(BuildCompatibleExtension.class, + LifecycleExtension.class); + } + + @Inject + BeanContainer container; + + @Test + public void testCreatorAndDisposerGetIndependentDependentInstances() { + DependentCounter.reset(); + SyntheticResultHolder.reset(); + + Instance instance = container.createInstance(); + Instance.Handle handle = instance.select(SyntheticBean.class).getHandle(); + + SyntheticBean result = handle.get(); + assertNotNull(result); + + // Creator should have obtained one DependentCounter + assertEquals(1, DependentCounter.createdCounter.get()); + // It should still be alive (dependent object of synthetic bean) + assertEquals(0, DependentCounter.destroyedCounter.get()); + + // Now destroy — triggers disposer, then destroys creator's dependent + handle.destroy(); + + // Both creator and disposer should have obtained their own instance + assertEquals(2, DependentCounter.createdCounter.get()); + // Creator and disposer must get different instances + assertNotEquals("Creator and disposer must receive independent DependentCounter instances", + SyntheticResultHolder.creatorCounterId, SyntheticResultHolder.disposerCounterId); + } + + @Test + public void testDisposerDependentDestroyedAfterDispose() { + DependentCounter.reset(); + SyntheticResultHolder.reset(); + + Instance instance = container.createInstance(); + Instance.Handle handle = instance.select(SyntheticBean.class).getHandle(); + handle.get(); + + assertEquals(0, DependentCounter.destroyedCounter.get()); + + handle.destroy(); + + // The disposer's @Dependent should have been destroyed after dispose() completed, + // plus the creator's @Dependent should be destroyed as part of bean destruction. + // Total: 2 created, 2 destroyed + assertEquals(2, DependentCounter.createdCounter.get()); + assertEquals(2, DependentCounter.destroyedCounter.get()); + } + + @Test + public void testCreatorDependentNotDestroyedBeforeDisposerRuns() { + DependentCounter.reset(); + SyntheticResultHolder.reset(); + + Instance instance = container.createInstance(); + Instance.Handle handle = instance.select(SyntheticBean.class).getHandle(); + handle.get(); + + handle.destroy(); + + // When the disposer ran, the creator's dependent should not yet have been destroyed + // (only 0 destroyed before the disposer called injections.get()) + assertEquals(0, SyntheticResultHolder.disposerDestroyedCountBeforeGet); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleCreator.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleCreator.java new file mode 100644 index 0000000000..312e2322a5 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleCreator.java @@ -0,0 +1,14 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticInjections; + +public class LifecycleCreator implements SyntheticBeanCreator { + @Override + public SyntheticBean create(SyntheticInjections injections, Parameters params) { + DependentCounter counter = injections.get(DependentCounter.class); + SyntheticResultHolder.creatorCounterId = counter.getId(); + return new SyntheticBean("created"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleDisposer.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleDisposer.java new file mode 100644 index 0000000000..c9d7c7385d --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleDisposer.java @@ -0,0 +1,14 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanDisposer; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticInjections; + +public class LifecycleDisposer implements SyntheticBeanDisposer { + @Override + public void dispose(SyntheticBean instance, SyntheticInjections injections, Parameters params) { + SyntheticResultHolder.disposerDestroyedCountBeforeGet = DependentCounter.destroyedCounter.get(); + DependentCounter counter = injections.get(DependentCounter.class); + SyntheticResultHolder.disposerCounterId = counter.getId(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleExtension.java new file mode 100644 index 0000000000..2570477fe6 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/LifecycleExtension.java @@ -0,0 +1,18 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; + +public class LifecycleExtension implements BuildCompatibleExtension { + @Synthesis + public void register(SyntheticComponents syn) { + syn.addBean(SyntheticBean.class) + .type(SyntheticBean.class) + .scope(Dependent.class) + .withInjectionPoint(DependentCounter.class) + .createWith(LifecycleCreator.class) + .disposeWith(LifecycleDisposer.class); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticBean.java new file mode 100644 index 0000000000..15827b0392 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticBean.java @@ -0,0 +1,13 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +public class SyntheticBean { + private final String value; + + public SyntheticBean(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticResultHolder.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticResultHolder.java new file mode 100644 index 0000000000..90723958b7 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/bce/syntheticInjectionPoint/dependentLifecycle/SyntheticResultHolder.java @@ -0,0 +1,14 @@ +package org.jboss.weld.tests.bce.syntheticInjectionPoint.dependentLifecycle; + +public class SyntheticResultHolder { + + public static volatile int creatorCounterId = -1; + public static volatile int disposerCounterId = -1; + public static volatile int disposerDestroyedCountBeforeGet = -1; + + public static void reset() { + creatorCounterId = -1; + disposerCounterId = -1; + disposerDestroyedCountBeforeGet = -1; + } +} From 8583d908f25a2dbbb36720d35eaefaa85375cf34 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Sun, 3 May 2026 22:06:58 +0200 Subject: [PATCH 4/7] Adapt async handler implementation and tests to new CDI 5.0 API --- .../invokable/AbstractInvokerBuilder.java | 64 ++++++++++--- .../weld/invokable/AsyncHandlerRegistry.java | 33 ++++--- .../weld/invokable/AsyncInvokerImpl.java | 89 ++++++++----------- .../jboss/weld/invokable/CleanupActions.java | 28 ++++-- .../weld/invokable/MethodHandleUtils.java | 22 +++-- .../org/jboss/weld/logging/InvokerLogger.java | 27 +++--- .../weld/tests/invokable/async/AsyncBean.java | 12 +++ .../async/AsyncInvokerExtension.java | 7 ++ .../invokable/async/AsyncInvokerTest.java | 17 ++++ ...andler.java => BothInterfacesHandler.java} | 2 +- ...st.java => BothInterfacesHandlerTest.java} | 13 +-- .../async/broken/IndirectReturnTypeBase.java | 6 ++ .../broken/IndirectReturnTypeHandler.java | 10 +++ ...ava => IndirectReturnTypeHandlerTest.java} | 13 +-- .../async/broken/RawAsyncHandlerTest.java | 6 +- .../async/broken/UnannotatedHandler.java | 13 --- .../async/custom/CustomAsyncBCETest.java | 2 +- .../async/custom/CustomAsyncHandlerTest.java | 2 +- .../async/paramtype/InvocationOrder.java | 14 +++ .../paramtype/MultipleParamMatchBean.java | 19 ++++ .../MultipleParamMatchExtension.java | 31 +++++++ .../paramtype/MultipleParamMatchTest.java | 63 +++++++++++++ .../async/paramtype/MyAsyncParamHandler.java | 3 +- .../async/paramtype/ParamTypeBean.java | 14 +++ .../async/paramtype/ParamTypeExtension.java | 20 ++++- .../ParameterTypeAsyncHandlerTest.java | 54 ++++++++++- .../async/paramtype/WrappedAsyncParam.java | 29 ++++++ 27 files changed, 470 insertions(+), 143 deletions(-) rename tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/{BothAnnotationsHandler.java => BothInterfacesHandler.java} (74%) rename tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/{UnannotatedHandlerTest.java => BothInterfacesHandlerTest.java} (58%) create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeBase.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandler.java rename tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/{BothAnnotationsHandlerTest.java => IndirectReturnTypeHandlerTest.java} (60%) delete mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/InvocationOrder.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchExtension.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/WrappedAsyncParam.java diff --git a/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java b/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java index d32a2549a7..f37bd6f006 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AbstractInvokerBuilder.java @@ -298,16 +298,22 @@ InvokerInfo doBuild() { AsyncHandlerRegistry.HandlerInfo paramTypeHandler = returnTypeHandler == null ? asyncRegistry.findParameterTypeHandler(reflectionMethod.getParameterTypes()) : null; - boolean useAsyncCleanup = requiresCleanup && (returnTypeHandler != null || paramTypeHandler != null); - // cleanup if (requiresCleanup) { MethodHandle cleanupMethod; - if (useAsyncCleanup) { - // Use deferred cleanup: on success, store CleanupActions in ThreadLocal instead of running cleanup + if (returnTypeHandler != null) { + // Embed handler.transform() directly into the tryFinally cleanup: + // on success, calls handler.transform(result, ca::cleanup) + // on exception, calls ca.cleanup() immediately + cleanupMethod = MethodHandleUtils.CLEANUP_FOR_NONVOID_RETURN_TYPE_HANDLER; + cleanupMethod = MethodHandles.insertArguments(cleanupMethod, 3, + returnTypeHandler.getReturnTypeHandler()); + } else if (paramTypeHandler != null) { + // ParameterType: CA is created by AsyncInvokerImpl and passed as the first parameter. + // Only run cleanup on exception — normal cleanup is handled by the completion callback. cleanupMethod = mh.type().returnType() == void.class - ? MethodHandleUtils.CLEANUP_FOR_VOID_DEFERRED - : MethodHandleUtils.CLEANUP_FOR_NONVOID_DEFERRED; + ? MethodHandleUtils.CLEANUP_FOR_VOID_EXCEPTION_ONLY + : MethodHandleUtils.CLEANUP_FOR_NONVOID_EXCEPTION_ONLY; } else { cleanupMethod = mh.type().returnType() == void.class ? MethodHandleUtils.CLEANUP_FOR_VOID @@ -352,11 +358,16 @@ InvokerInfo doBuild() { 1, reflectionMethod.getParameterCount()); mh = MethodHandles.filterArguments(mh, requiresCleanup ? 2 : 1, trimArgumentArray); - // instantiate `CleanupActions` - if (requiresCleanup) { + // Instantiate CleanupActions — for ParameterType handlers, the invoker creates + // CA externally and passes it as the first parameter, so we skip foldArguments. + boolean externalCleanupActions = requiresCleanup && paramTypeHandler != null; + if (requiresCleanup && !externalCleanupActions) { mh = MethodHandles.foldArguments(mh, MethodHandleUtils.CLEANUP_ACTIONS_CTOR); } + // When CA is external, instance is at position 1 (after CA); otherwise position 0 + int instancePos = externalCleanupActions ? 1 : 0; + Class[] expectedTypes = new Class[transformerArgTypes.length]; for (int i = 0; i < transformerArgTypes.length; i++) { if (argLookup[i]) { @@ -368,10 +379,13 @@ InvokerInfo doBuild() { if (reflectionMethod.getParameterCount() > 0) { // Catch NullPointerException and check whether it's caused by any arguments being null: - Class instanceType = mh.type().parameterType(0); + Class instanceType = mh.type().parameterType(instancePos); MethodHandle checkArgumentsNotNull = MethodHandles.insertArguments(MethodHandleUtils.CHECK_ARGUMENTS_NOT_NULL, 0, reflectionMethod, expectedTypes); checkArgumentsNotNull = MethodHandles.dropArguments(checkArgumentsNotNull, 0, instanceType); + if (externalCleanupActions) { + checkArgumentsNotNull = MethodHandles.dropArguments(checkArgumentsNotNull, 0, CleanupActions.class); + } MethodHandle npeCatch = MethodHandles.throwException(mh.type().returnType(), NullPointerException.class); npeCatch = MethodHandles.collectArguments(npeCatch, 1, checkArgumentsNotNull); mh = MethodHandles.catchException(mh, NullPointerException.class, npeCatch); @@ -380,6 +394,9 @@ InvokerInfo doBuild() { MethodHandle checkArgCountAtLeast = MethodHandles.insertArguments(MethodHandleUtils.CHECK_ARG_COUNT_AT_LEAST, 0, reflectionMethod, reflectionMethod.getParameterCount()); checkArgCountAtLeast = MethodHandles.dropArguments(checkArgCountAtLeast, 0, instanceType); + if (externalCleanupActions) { + checkArgCountAtLeast = MethodHandles.dropArguments(checkArgCountAtLeast, 0, CleanupActions.class); + } MethodHandle iaeCatch = MethodHandles.throwException(mh.type().returnType(), IllegalArgumentException.class); iaeCatch = MethodHandles.collectArguments(iaeCatch, 1, checkArgCountAtLeast); mh = MethodHandles.catchException(mh, IllegalArgumentException.class, iaeCatch); @@ -387,17 +404,23 @@ InvokerInfo doBuild() { if ((!isStaticMethod && !instanceLookup) || reflectionMethod.getParameterCount() > 0) { // Catch ClassCastException and check whether it's caused by either the instance or the arguments being the wrong type - Class instanceType = mh.type().parameterType(0); + Class instanceType = mh.type().parameterType(instancePos); MethodHandle checkTypes = null; if (reflectionMethod.getParameterCount() > 0) { checkTypes = MethodHandles.insertArguments(MethodHandleUtils.CHECK_ARGUMENTS_HAVE_CORRECT_TYPE, 0, reflectionMethod, expectedTypes); checkTypes = MethodHandles.dropArguments(checkTypes, 0, Object.class); + if (externalCleanupActions) { + checkTypes = MethodHandles.dropArguments(checkTypes, 0, CleanupActions.class); + } } if (!isStaticMethod && !instanceLookup) { MethodHandle checkInstanceType = MethodHandles.insertArguments(MethodHandleUtils.CHECK_INSTANCE_HAS_TYPE, 0, reflectionMethod, instanceType); + if (externalCleanupActions) { + checkInstanceType = MethodHandles.dropArguments(checkInstanceType, 0, CleanupActions.class); + } if (checkTypes == null) { checkTypes = checkInstanceType; } else { @@ -408,11 +431,13 @@ InvokerInfo doBuild() { MethodHandle cceCatch = MethodHandles.throwException(mh.type().returnType(), ClassCastException.class); cceCatch = MethodHandles.collectArguments(cceCatch, 1, checkTypes); - mh = mh.asType(mh.type().changeParameterType(0, Object.class)); // Defer the casting the instance to its expected type until we're inside the ClassCastException try block + mh = mh.asType(mh.type().changeParameterType(instancePos, Object.class)); mh = MethodHandles.catchException(mh, ClassCastException.class, cceCatch); } // create an inner invoker and pass it to wrapper + // NOTE: invocation wrappers combined with ParameterType async handlers are not + // currently supported — the inner InvokerImpl does not pass CleanupActions if (invocationWrapper != null) { InvokerImpl invoker = new InvokerImpl<>(mh); @@ -422,11 +447,24 @@ InvokerInfo doBuild() { mh = MethodHandles.insertArguments(invocationWrapperMethod, 2, invoker); } + // ReturnType handler is embedded in the MH chain (tryFinally or filterReturnValue), + // so a plain InvokerImpl suffices. ParameterType needs AsyncInvokerImpl because + // transformArgument must run before the chain but needs the completion callback + // that is only available after. if (returnTypeHandler != null) { - return new AsyncInvokerImpl<>(mh, returnTypeHandler, -1); + if (!requiresCleanup) { + MethodHandle transform = MethodHandleUtils.APPLY_RETURN_TYPE_HANDLER; + transform = MethodHandles.insertArguments(transform, 1, + returnTypeHandler.getReturnTypeHandler()); + transform = transform.asType(transform.type() + .changeReturnType(mh.type().returnType()) + .changeParameterType(0, mh.type().returnType())); + mh = MethodHandles.filterReturnValue(mh, transform); + } + return new InvokerImpl<>(mh); } else if (paramTypeHandler != null) { int asyncParamIndex = findAsyncParamIndex(reflectionMethod.getParameterTypes(), paramTypeHandler.getAsyncType()); - return new AsyncInvokerImpl<>(mh, paramTypeHandler, asyncParamIndex); + return new AsyncInvokerImpl<>(mh, paramTypeHandler, asyncParamIndex, requiresCleanup); } return new InvokerImpl<>(mh); } diff --git a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java index 585f4a09b4..8f58f93085 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java @@ -66,6 +66,7 @@ public void discoverHandlers(ResourceLoader resourceLoader) { private void validateAndRegisterReturnType(AsyncHandler.ReturnType handler) { Class handlerClass = handler.getClass(); + validateDirectImplementation(handlerClass, AsyncHandler.ReturnType.class); Class asyncType = extractAsyncType(handlerClass, AsyncHandler.ReturnType.class); checkDuplicate(asyncType, handlerClass); handlers.put(asyncType, HandlerInfo.returnType(handler, asyncType)); @@ -73,11 +74,21 @@ private void validateAndRegisterReturnType(AsyncHandler.ReturnType handler) { private void validateAndRegisterParameterType(AsyncHandler.ParameterType handler) { Class handlerClass = handler.getClass(); + validateDirectImplementation(handlerClass, AsyncHandler.ParameterType.class); Class asyncType = extractAsyncType(handlerClass, AsyncHandler.ParameterType.class); checkDuplicate(asyncType, handlerClass); handlers.put(asyncType, HandlerInfo.parameterType(handler, asyncType)); } + private void validateDirectImplementation(Class handlerClass, Class targetInterface) { + for (Class iface : handlerClass.getInterfaces()) { + if (iface == targetInterface) { + return; + } + } + throw InvokerLogger.LOG.asyncHandlerIndirectImplementation(handlerClass); + } + private void checkDuplicate(Class asyncType, Class handlerClass) { HandlerInfo existing = handlers.get(asyncType); if (existing != null && !existing.isBuiltin()) { @@ -97,18 +108,7 @@ private Class extractAsyncType(Class handlerClass, Class targetInterfac throw InvokerLogger.LOG.asyncHandlerRawType(handlerClass); } } - // Check superclass interfaces recursively - for (Type genericInterface : handlerClass.getGenericInterfaces()) { - Class rawIface = null; - if (genericInterface instanceof ParameterizedType) { - rawIface = (Class) ((ParameterizedType) genericInterface).getRawType(); - } else if (genericInterface instanceof Class) { - rawIface = (Class) genericInterface; - } - if (rawIface != null && targetInterface.isAssignableFrom(rawIface)) { - return extractAsyncType(rawIface, targetInterface); - } - } + // Should not happen if validateDirectImplementation passed throw InvokerLogger.LOG.asyncHandlerRawType(handlerClass); } @@ -160,13 +160,18 @@ public HandlerInfo findReturnTypeHandler(Class returnType) { * Returns the handler info if exactly one parameter matches; null otherwise. */ public HandlerInfo findParameterTypeHandler(Class[] parameterTypes) { + HandlerInfo match = null; + int matchCount = 0; for (Class paramType : parameterTypes) { HandlerInfo info = handlers.get(paramType); if (info != null && !info.isReturnType()) { - return info; + match = info; + matchCount++; } } - return null; + // spec requires exactly one matching parameter; with 0 or 2+ matches + // the method is not considered async and null signals synchronous cleanup + return matchCount == 1 ? match : null; } @Override diff --git a/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java b/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java index 197ded10d7..9515f003c2 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java @@ -7,77 +7,66 @@ import jakarta.enterprise.invoke.Invoker; /** - * An invoker that applies an {@link AsyncHandler} to the method invocation, - * deferring cleanup of dependent beans until the async operation completes. + * Invoker for methods matching an {@link AsyncHandler.ParameterType} handler. *

- * For {@link AsyncHandler.ReturnType} handlers: after the method returns, the return value - * is passed to {@link AsyncHandler.ReturnType#transform(Object, Runnable)}, with the cleanup - * action as the completion callback. + * Only used for ParameterType handlers. ReturnType handlers are embedded directly + * into the method handle chain's {@code tryFinally} cleanup (see + * {@link CleanupActions#runWithReturnTypeHandler}) and use plain {@link InvokerImpl}. *

- * For {@link AsyncHandler.ParameterType} handlers: before the method is called, the matching - * argument is passed to {@link AsyncHandler.ParameterType#transformArgument(Object, Runnable)}, - * and after the method returns, the return value is passed to - * {@link AsyncHandler.ParameterType#transformReturnValue(Object, Runnable)}. + * When the method handle chain requires cleanup (dependent bean lookups, transformers + * with cleanup), the chain is built without internal {@link CleanupActions} creation + * ({@code foldArguments} is skipped). Instead, this invoker creates + * {@code CleanupActions} externally and passes it as the first parameter to + * {@code mh.invoke()}. This allows {@code transformArgument} to receive + * {@code ca::cleanup} as the completion callback before the method runs, which is + * required for correct behavior when the async parameter completes synchronously + * during the method body. + *

+ * When no cleanup is needed, the chain has no {@code CleanupActions} at all and + * the handler receives a no-op completion callback. */ class AsyncInvokerImpl implements Invoker, InvokerInfo { private final MethodHandle mh; - private final AsyncHandler.ReturnType returnTypeHandler; private final AsyncHandler.ParameterType parameterTypeHandler; - private final int asyncParamIndex; // for ParameterType, -1 for ReturnType + private final int asyncParamIndex; + private final boolean requiresCleanup; - AsyncInvokerImpl(MethodHandle mh, AsyncHandlerRegistry.HandlerInfo handlerInfo, int asyncParamIndex) { + AsyncInvokerImpl(MethodHandle mh, AsyncHandlerRegistry.HandlerInfo handlerInfo, + int asyncParamIndex, boolean requiresCleanup) { this.mh = mh; - if (handlerInfo.isReturnType()) { - this.returnTypeHandler = handlerInfo.getReturnTypeHandler(); - this.parameterTypeHandler = null; - this.asyncParamIndex = -1; - } else { - this.returnTypeHandler = null; - this.parameterTypeHandler = handlerInfo.getParameterTypeHandler(); - this.asyncParamIndex = asyncParamIndex; - } + this.parameterTypeHandler = handlerInfo.getParameterTypeHandler(); + this.asyncParamIndex = asyncParamIndex; + this.requiresCleanup = requiresCleanup; } @Override @SuppressWarnings("unchecked") public R invoke(T instance, Object[] arguments) throws Exception { - try { - // Clear any stale ThreadLocal before invocation - CleanupActions.CURRENT.remove(); - - if (parameterTypeHandler != null && asyncParamIndex >= 0 - && arguments != null && asyncParamIndex < arguments.length) { - // ParameterType: invoke the method, then retrieve deferred cleanup - R result = (R) mh.invoke(instance, arguments); - - CleanupActions ca = CleanupActions.CURRENT.get(); - CleanupActions.CURRENT.remove(); - - if (ca != null) { - parameterTypeHandler.transformArgument(arguments[asyncParamIndex], ca::cleanup); - return (R) parameterTypeHandler.transformReturnValue(result, () -> { - }); - } - return result; - } - - // ReturnType: invoke the method, then transform the return value - R result = (R) mh.invoke(instance, arguments); + Runnable completion; + CleanupActions ca; + if (requiresCleanup) { + ca = new CleanupActions(); + completion = ca::cleanup; + } else { + ca = null; + completion = () -> { + }; + } - CleanupActions ca = CleanupActions.CURRENT.get(); - CleanupActions.CURRENT.remove(); + arguments[asyncParamIndex] = parameterTypeHandler + .transformArgument(arguments[asyncParamIndex], completion); + try { + R result; if (ca != null) { - return (R) returnTypeHandler.transform(result, ca::cleanup); + result = (R) mh.invoke(ca, instance, arguments); } else { - return (R) returnTypeHandler.transform(result, () -> { - }); + result = (R) mh.invoke(instance, arguments); } + return (R) parameterTypeHandler.transformReturnValue(result, completion); } catch (ValueCarryingException e) { - CleanupActions.CURRENT.remove(); return (R) e.getMethodReturnValue(); } catch (Throwable e) { - CleanupActions.CURRENT.remove(); throw SneakyThrow.sneakyThrow(e); } } diff --git a/impl/src/main/java/org/jboss/weld/invokable/CleanupActions.java b/impl/src/main/java/org/jboss/weld/invokable/CleanupActions.java index fe8637bd54..9104b2ec53 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/CleanupActions.java +++ b/impl/src/main/java/org/jboss/weld/invokable/CleanupActions.java @@ -6,10 +6,9 @@ import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.invoke.AsyncHandler; class CleanupActions implements Consumer { - // ThreadLocal to expose the CleanupActions instance to async invokers - static final ThreadLocal CURRENT = new ThreadLocal<>(); private final List cleanupTasks = new ArrayList<>(); private final List> dependentInstances = new ArrayList<>(); @@ -50,21 +49,32 @@ public static void run(Throwable cause, CleanupActions cleanupActions) { cleanupActions.cleanup(); } - // Deferred variants: only cleanup on exception, expose instance via ThreadLocal on success - public static R runDeferred(Throwable cause, R returnValue, CleanupActions cleanupActions) { + public static R runExceptionOnly(Throwable cause, R returnValue, CleanupActions cleanupActions) { if (cause != null) { cleanupActions.cleanup(); - } else { - CURRENT.set(cleanupActions); } return returnValue; } - public static void runDeferred(Throwable cause, CleanupActions cleanupActions) { + public static void runExceptionOnly(Throwable cause, CleanupActions cleanupActions) { if (cause != null) { cleanupActions.cleanup(); - } else { - CURRENT.set(cleanupActions); } } + + @SuppressWarnings("unchecked") + public static R applyReturnTypeHandler(R returnValue, AsyncHandler.ReturnType handler) { + return (R) handler.transform(returnValue, () -> { + }); + } + + @SuppressWarnings("unchecked") + public static R runWithReturnTypeHandler(Throwable cause, R returnValue, + CleanupActions cleanupActions, AsyncHandler.ReturnType handler) { + if (cause != null) { + cleanupActions.cleanup(); + return returnValue; + } + return (R) handler.transform(returnValue, cleanupActions::cleanup); + } } diff --git a/impl/src/main/java/org/jboss/weld/invokable/MethodHandleUtils.java b/impl/src/main/java/org/jboss/weld/invokable/MethodHandleUtils.java index a7f9d3fa11..7e35f7a746 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/MethodHandleUtils.java +++ b/impl/src/main/java/org/jboss/weld/invokable/MethodHandleUtils.java @@ -13,6 +13,7 @@ import java.util.function.Consumer; import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.invoke.AsyncHandler; import jakarta.enterprise.invoke.Invoker; import org.jboss.weld.logging.InvokerLogger; @@ -24,8 +25,8 @@ private MethodHandleUtils() { static final MethodHandle CLEANUP_ACTIONS_CTOR; static final MethodHandle CLEANUP_FOR_VOID; static final MethodHandle CLEANUP_FOR_NONVOID; - static final MethodHandle CLEANUP_FOR_VOID_DEFERRED; - static final MethodHandle CLEANUP_FOR_NONVOID_DEFERRED; + static final MethodHandle CLEANUP_FOR_VOID_EXCEPTION_ONLY; + static final MethodHandle CLEANUP_FOR_NONVOID_EXCEPTION_ONLY; static final MethodHandle LOOKUP; static final MethodHandle REPLACE_PRIMITIVE_LOOKUP_NULLS; static final MethodHandle THROW_VALUE_CARRYING_EXCEPTION; @@ -34,6 +35,8 @@ private MethodHandleUtils() { static final MethodHandle CHECK_INSTANCE_NOT_NULL; static final MethodHandle CHECK_ARG_COUNT_AT_LEAST; static final MethodHandle CHECK_ARGUMENTS_HAVE_CORRECT_TYPE; + static final MethodHandle CLEANUP_FOR_NONVOID_RETURN_TYPE_HANDLER; + static final MethodHandle APPLY_RETURN_TYPE_HANDLER; static final MethodHandle CHECK_ARGUMENTS_NOT_NULL; static { @@ -44,17 +47,22 @@ private MethodHandleUtils() { runName, Throwable.class, CleanupActions.class)); CLEANUP_FOR_NONVOID = createMethodHandle(CleanupActions.class.getMethod( runName, Throwable.class, Object.class, CleanupActions.class)); - String runDeferredName = "runDeferred"; - CLEANUP_FOR_VOID_DEFERRED = createMethodHandle(CleanupActions.class.getMethod( - runDeferredName, Throwable.class, CleanupActions.class)); - CLEANUP_FOR_NONVOID_DEFERRED = createMethodHandle(CleanupActions.class.getMethod( - runDeferredName, Throwable.class, Object.class, CleanupActions.class)); LOOKUP = createMethodHandle(LookupUtils.class.getDeclaredMethod( "lookup", CleanupActions.class, BeanManager.class, Type.class, Annotation[].class)); REPLACE_PRIMITIVE_LOOKUP_NULLS = MethodHandleUtils.createMethodHandle(LookupUtils.class.getDeclaredMethod( "replacePrimitiveLookupNulls", Object[].class, Class[].class, boolean[].class)); THROW_VALUE_CARRYING_EXCEPTION = createMethodHandle(ValueCarryingException.class.getDeclaredMethod( "throwReturnValue", Object.class)); + String runExceptionOnly = "runExceptionOnly"; + CLEANUP_FOR_VOID_EXCEPTION_ONLY = createMethodHandle(CleanupActions.class.getMethod( + runExceptionOnly, Throwable.class, CleanupActions.class)); + CLEANUP_FOR_NONVOID_EXCEPTION_ONLY = createMethodHandle(CleanupActions.class.getMethod( + runExceptionOnly, Throwable.class, Object.class, CleanupActions.class)); + CLEANUP_FOR_NONVOID_RETURN_TYPE_HANDLER = createMethodHandle(CleanupActions.class.getMethod( + "runWithReturnTypeHandler", Throwable.class, Object.class, CleanupActions.class, + AsyncHandler.ReturnType.class)); + APPLY_RETURN_TYPE_HANDLER = createMethodHandle(CleanupActions.class.getMethod( + "applyReturnTypeHandler", Object.class, AsyncHandler.ReturnType.class)); TRIM_ARRAY_TO_SIZE = createMethodHandle(ArrayUtils.class.getDeclaredMethod( "trimArrayToSize", Object[].class, int.class)); CHECK_INSTANCE_HAS_TYPE = createMethodHandle( diff --git a/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java b/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java index 101a6fcd86..c5a83bd4c4 100644 --- a/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java +++ b/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java @@ -9,6 +9,7 @@ import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.Message.Format; import org.jboss.logging.annotations.MessageLogger; +import org.jboss.weld.exceptions.DefinitionException; import org.jboss.weld.exceptions.DeploymentException; import org.jboss.weld.exceptions.IllegalArgumentException; import org.jboss.weld.exceptions.IllegalStateException; @@ -88,35 +89,29 @@ public interface InvokerLogger extends WeldLogger { NullPointerException nullArgumentArray(Object method); @Message(id = 2021, value = "AsyncHandler {0} implements AsyncHandler as a raw type without a type argument", format = Format.MESSAGE_FORMAT) - DeploymentException asyncHandlerRawType(Object handlerClass); + DefinitionException asyncHandlerRawType(Object handlerClass); @Message(id = 2022, value = "AsyncHandler {0} has a type variable as async type; the async type must be a concrete class or parameterized type", format = Format.MESSAGE_FORMAT) - DeploymentException asyncHandlerTypeVariable(Object handlerClass); + DefinitionException asyncHandlerTypeVariable(Object handlerClass); @Message(id = 2023, value = "AsyncHandler {0} has an array as async type; arrays are not allowed", format = Format.MESSAGE_FORMAT) - DeploymentException asyncHandlerArrayType(Object handlerClass); + DefinitionException asyncHandlerArrayType(Object handlerClass); - @Message(id = 2024, value = "AsyncHandler {0} is annotated with both @ReturnType and @ParameterType; exactly one is required", format = Format.MESSAGE_FORMAT) - DeploymentException asyncHandlerBothAnnotations(Object handlerClass); + @Message(id = 2024, value = "AsyncHandler {0} does not implement the async handler interface directly; indirect implementation through a superclass is not allowed", format = Format.MESSAGE_FORMAT) + DefinitionException asyncHandlerIndirectImplementation(Object handlerClass); - @Message(id = 2025, value = "AsyncHandler {0} is not annotated with @ReturnType or @ParameterType; exactly one is required", format = Format.MESSAGE_FORMAT) - DeploymentException asyncHandlerNoAnnotation(Object handlerClass); - - @Message(id = 2026, value = "Multiple async handlers found for async type {0}; handler class: {1}", format = Format.MESSAGE_FORMAT) + @Message(id = 2025, value = "Multiple async handlers found for async type {0}; handler class: {1}", format = Format.MESSAGE_FORMAT) DeploymentException asyncHandlerDuplicate(Object asyncType, Object handlerClass); - @Message(id = 2027, value = "AsyncHandler {0} does not implement AsyncHandler directly; indirect implementation is not allowed", format = Format.MESSAGE_FORMAT) - DeploymentException asyncHandlerIndirectImplementation(Object handlerClass); - - @Message(id = 2028, value = "No async handler registered for type {0}{1}", format = Format.MESSAGE_FORMAT) + @Message(id = 2026, value = "No async handler registered for type {0}{1}", format = Format.MESSAGE_FORMAT) DeploymentException asyncHandlerNotFound(Object asyncType, Object additionalMessage); - @Message(id = 2029, value = "Unable to locate Weld internal helper method", format = Format.MESSAGE_FORMAT) + @Message(id = 2027, value = "Unable to locate Weld internal helper method", format = Format.MESSAGE_FORMAT) IllegalStateException cannotLocateInternalMethod(@Cause Throwable cause); - @Message(id = 2030, value = "Unknown transformer type: {0}", format = Format.MESSAGE_FORMAT) + @Message(id = 2028, value = "Unknown transformer type: {0}", format = Format.MESSAGE_FORMAT) IllegalStateException unknownTransformerType(Object type); - @Message(id = 2031, value = "Unhandled primitive type: {0}", format = Format.MESSAGE_FORMAT) + @Message(id = 2029, value = "Unhandled primitive type: {0}", format = Format.MESSAGE_FORMAT) RuntimeException unhandledPrimitiveType(Object primitive); } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncBean.java index 0f10150194..625868e582 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncBean.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncBean.java @@ -25,6 +25,18 @@ public CompletableFuture helloCF(DependentBean dep, CompletableFuture helloNoLookup(CompletableFuture future) { + CompletableFuture result = new CompletableFuture<>(); + future.whenComplete((value, error) -> { + if (error == null) { + result.complete(value); + } else { + result.completeExceptionally(error); + } + }); + return result; + } + public Flow.Publisher helloFP(DependentBean dep, CompletableFuture future) { return new CompletableFuturePublisher<>(future); } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerExtension.java index d11104e95d..9bb55d0088 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerExtension.java @@ -18,6 +18,7 @@ public class AsyncInvokerExtension implements Extension { private Invoker csInvoker; private Invoker cfInvoker; private Invoker fpInvoker; + private Invoker noLookupInvoker; public void observeAsyncBean(@Observes WeldProcessManagedBean pmb) { Collection> methods = pmb.getAnnotatedBeanClass().getMethods(); @@ -38,6 +39,8 @@ public void observeAsyncBean(@Observes WeldProcessManagedBean pmb) { .withInstanceLookup() .withArgumentLookup(0) .build(); + } else if ("helloNoLookup".equals(name)) { + noLookupInvoker = pmb.createInvoker(m).build(); } } } @@ -59,4 +62,8 @@ public void validate(@Observes AfterDeploymentValidation adv) { public Invoker getFpInvoker() { return fpInvoker; } + + public Invoker getNoLookupInvoker() { + return noLookupInvoker; + } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerTest.java index 94a2459381..1c6bab2a2d 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/AsyncInvokerTest.java @@ -81,6 +81,23 @@ public void testCompletableFutureReturnType() throws Exception { assertEquals("world", result.getNow(null)); } + @Test + @SuppressWarnings("unchecked") + public void testReturnTypeWithoutLookups() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + + AsyncBean bean = new AsyncBean(); + CompletionStage result = (CompletionStage) extension.getNoLookupInvoker() + .invoke(bean, new Object[] { future }); + + assertFalse(result.toCompletableFuture().isDone()); + + future.complete("no-lookup"); + + assertTrue(result.toCompletableFuture().isDone()); + assertEquals("no-lookup", result.toCompletableFuture().getNow(null)); + } + @Test @SuppressWarnings("unchecked") public void testFlowPublisherReturnType() throws Exception { diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java similarity index 74% rename from tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java rename to tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java index efc817ef02..b893251e80 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java @@ -2,7 +2,7 @@ import jakarta.enterprise.invoke.AsyncHandler; -public class BothAnnotationsHandler implements AsyncHandler.ReturnType, AsyncHandler.ParameterType { +public class BothInterfacesHandler implements AsyncHandler.ReturnType, AsyncHandler.ParameterType { @Override public T transform(T original, Runnable completion) { return original; diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java similarity index 58% rename from tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandlerTest.java rename to tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java index 33393740f7..9db5985e8c 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java @@ -1,6 +1,6 @@ package org.jboss.weld.tests.invokable.async.broken; -import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.enterprise.inject.spi.DefinitionException; import jakarta.enterprise.invoke.AsyncHandler; import org.jboss.arquillian.container.test.api.Deployment; @@ -14,15 +14,16 @@ import org.junit.runner.RunWith; @RunWith(Arquillian.class) -public class UnannotatedHandlerTest { +public class BothInterfacesHandlerTest { @Deployment - @ShouldThrowException(DeploymentException.class) + @ShouldThrowException(DefinitionException.class) public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, - Utils.getDeploymentNameAsHash(UnannotatedHandlerTest.class)) - .addClass(UnannotatedHandler.class) - .addAsServiceProvider(AsyncHandler.class, UnannotatedHandler.class); + Utils.getDeploymentNameAsHash(BothInterfacesHandlerTest.class)) + .addClass(BothInterfacesHandler.class) + .addAsServiceProvider(AsyncHandler.ReturnType.class, BothInterfacesHandler.class) + .addAsServiceProvider(AsyncHandler.ParameterType.class, BothInterfacesHandler.class); } @Test diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeBase.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeBase.java new file mode 100644 index 0000000000..e3c5f048d1 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeBase.java @@ -0,0 +1,6 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.invoke.AsyncHandler; + +abstract class IndirectReturnTypeBase implements AsyncHandler.ReturnType { +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandler.java new file mode 100644 index 0000000000..3770fb5553 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandler.java @@ -0,0 +1,10 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import java.util.concurrent.CompletionStage; + +public class IndirectReturnTypeHandler extends IndirectReturnTypeBase> { + @Override + public CompletionStage transform(CompletionStage original, Runnable completion) { + return original.whenComplete((v, e) -> completion.run()); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandlerTest.java similarity index 60% rename from tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandlerTest.java rename to tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandlerTest.java index 3c845f416e..60e6937167 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothAnnotationsHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/IndirectReturnTypeHandlerTest.java @@ -1,6 +1,6 @@ package org.jboss.weld.tests.invokable.async.broken; -import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.enterprise.inject.spi.DefinitionException; import jakarta.enterprise.invoke.AsyncHandler; import org.jboss.arquillian.container.test.api.Deployment; @@ -14,15 +14,16 @@ import org.junit.runner.RunWith; @RunWith(Arquillian.class) -public class BothAnnotationsHandlerTest { +public class IndirectReturnTypeHandlerTest { @Deployment - @ShouldThrowException(DeploymentException.class) + @ShouldThrowException(DefinitionException.class) public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, - Utils.getDeploymentNameAsHash(BothAnnotationsHandlerTest.class)) - .addClass(BothAnnotationsHandler.class) - .addAsServiceProvider(AsyncHandler.class, BothAnnotationsHandler.class); + Utils.getDeploymentNameAsHash(IndirectReturnTypeHandlerTest.class)) + .addClass(IndirectReturnTypeHandler.class) + .addClass(IndirectReturnTypeBase.class) + .addAsServiceProvider(AsyncHandler.ReturnType.class, IndirectReturnTypeHandler.class); } @Test diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandlerTest.java index 9fae8c01d5..6bdd44b45e 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/RawAsyncHandlerTest.java @@ -1,6 +1,6 @@ package org.jboss.weld.tests.invokable.async.broken; -import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.enterprise.inject.spi.DefinitionException; import jakarta.enterprise.invoke.AsyncHandler; import org.jboss.arquillian.container.test.api.Deployment; @@ -17,12 +17,12 @@ public class RawAsyncHandlerTest { @Deployment - @ShouldThrowException(DeploymentException.class) + @ShouldThrowException(DefinitionException.class) public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(RawAsyncHandlerTest.class)) .addClass(RawAsyncHandler.class) - .addAsServiceProvider(AsyncHandler.class, RawAsyncHandler.class); + .addAsServiceProvider(AsyncHandler.ReturnType.class, RawAsyncHandler.class); } @Test diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java deleted file mode 100644 index 0ebbedfa85..0000000000 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/UnannotatedHandler.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.jboss.weld.tests.invokable.async.broken; - -import jakarta.enterprise.invoke.AsyncHandler; - -// With the new API, a handler must implement either ReturnType or ParameterType. -// This class implements neither — it is not a valid handler. -// TODO: revisit this test with the new async handler API -public class UnannotatedHandler implements AsyncHandler.ReturnType { - @Override - public T transform(T original, Runnable completion) { - return original; - } -} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncBCETest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncBCETest.java index 49ff768806..f3c958cd53 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncBCETest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncBCETest.java @@ -34,7 +34,7 @@ public static Archive deploy() { .addPackage(CustomAsyncBCETest.class.getPackage()) .addClass(DependentBean.class) .addAsServiceProvider(BuildCompatibleExtension.class, CustomAsyncBCExtension.class) - .addAsServiceProvider(AsyncHandler.class, MyAsyncTypeHandler.class); + .addAsServiceProvider(AsyncHandler.ReturnType.class, MyAsyncTypeHandler.class); } @Inject diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncHandlerTest.java index f21ed837d5..490a3165a1 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/custom/CustomAsyncHandlerTest.java @@ -30,7 +30,7 @@ public static Archive deploy() { .addPackage(CustomAsyncHandlerTest.class.getPackage()) .addClass(DependentBean.class) .addAsServiceProvider(Extension.class, CustomAsyncExtension.class) - .addAsServiceProvider(AsyncHandler.class, MyAsyncTypeHandler.class); + .addAsServiceProvider(AsyncHandler.ReturnType.class, MyAsyncTypeHandler.class); } @Inject diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/InvocationOrder.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/InvocationOrder.java new file mode 100644 index 0000000000..ea183f68d5 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/InvocationOrder.java @@ -0,0 +1,14 @@ +package org.jboss.weld.tests.invokable.async.paramtype; + +import java.util.ArrayList; +import java.util.List; + +public class InvocationOrder { + public static final List events = new ArrayList<>(); + public static boolean receivedWrapped = false; + + public static void reset() { + events.clear(); + receivedWrapped = false; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchBean.java new file mode 100644 index 0000000000..1f9087526a --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchBean.java @@ -0,0 +1,19 @@ +package org.jboss.weld.tests.invokable.async.paramtype; + +import java.util.concurrent.CompletableFuture; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.weld.tests.invokable.async.DependentBean; + +@ApplicationScoped +public class MultipleParamMatchBean { + public static boolean futureComplete = false; + + public void hello(DependentBean dep, CompletableFuture future, + MyAsyncParam async1, MyAsyncParam async2) { + future.whenComplete((value, error) -> { + futureComplete = true; + }); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchExtension.java new file mode 100644 index 0000000000..3c1a43ee9b --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchExtension.java @@ -0,0 +1,31 @@ +package org.jboss.weld.tests.invokable.async.paramtype; + +import java.util.Collection; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AnnotatedMethod; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.invoke.Invoker; + +import org.jboss.weld.bootstrap.event.WeldProcessManagedBean; + +public class MultipleParamMatchExtension implements Extension { + + private Invoker invoker; + + public void observeBean(@Observes WeldProcessManagedBean pmb) { + Collection> methods = pmb.getAnnotatedBeanClass().getMethods(); + for (AnnotatedMethod m : methods) { + if ("hello".equals(m.getJavaMember().getName())) { + invoker = pmb.createInvoker(m) + .withInstanceLookup() + .withArgumentLookup(0) + .build(); + } + } + } + + public Invoker getInvoker() { + return invoker; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchTest.java new file mode 100644 index 0000000000..d5173a572c --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MultipleParamMatchTest.java @@ -0,0 +1,63 @@ +package org.jboss.weld.tests.invokable.async.paramtype; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.CompletableFuture; + +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.invoke.AsyncHandler; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.Utils; +import org.jboss.weld.tests.invokable.async.DependentBean; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * When multiple parameters match a ParameterType handler, the method + * is NOT async — cleanup must happen synchronously. + */ +@RunWith(Arquillian.class) +public class MultipleParamMatchTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(MultipleParamMatchTest.class)) + .addClasses(MultipleParamMatchBean.class, MultipleParamMatchExtension.class, + MyAsyncParam.class, MyAsyncParamHandler.class, InvocationOrder.class, + WrappedAsyncParam.class, DependentBean.class) + .addAsServiceProvider(Extension.class, MultipleParamMatchExtension.class) + .addAsServiceProvider(AsyncHandler.ParameterType.class, MyAsyncParamHandler.class); + } + + @Inject + MultipleParamMatchExtension extension; + + @Test + public void testMultipleMatchingParamsNotAsync() throws Exception { + DependentBean.reset(); + MultipleParamMatchBean.futureComplete = false; + CompletableFuture future = new CompletableFuture<>(); + + assertEquals(0, DependentBean.destroyedCounter.get()); + + extension.getInvoker().invoke(null, new Object[] { null, future, null, null }); + + // Not async — cleanup should happen synchronously + assertFalse(MultipleParamMatchBean.futureComplete); + assertEquals(1, DependentBean.destroyedCounter.get()); + + future.complete("hello"); + + assertTrue(MultipleParamMatchBean.futureComplete); + assertEquals(1, DependentBean.destroyedCounter.get()); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java index 93a0f02ee0..56600ce04b 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/MyAsyncParamHandler.java @@ -5,7 +5,8 @@ public class MyAsyncParamHandler implements AsyncHandler.ParameterType> { @Override public MyAsyncParam transformArgument(MyAsyncParam original, Runnable completion) { + InvocationOrder.events.add("transformArgument"); original.whenComplete(completion); - return original; + return new WrappedAsyncParam<>(original); } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java index 1cc69cff3a..88ff2bfaef 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java @@ -8,7 +8,21 @@ @ApplicationScoped public class ParamTypeBean { + public void helloNoLookup(CompletableFuture future, MyAsyncParam async) { + future.whenComplete((value, error) -> { + if (error == null) { + async.resume(value); + } + }); + } + + public void helloSync(DependentBean dep, MyAsyncParam async) { + async.resume("sync-hello"); + } + public void hello(DependentBean dep, CompletableFuture future, MyAsyncParam async) { + InvocationOrder.events.add("methodBody"); + InvocationOrder.receivedWrapped = async instanceof WrappedAsyncParam; future.whenComplete((value, error) -> { if (error == null) { async.resume(value); diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java index cd84ebeccc..6af559623a 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java @@ -12,15 +12,25 @@ public class ParamTypeExtension implements Extension { private Invoker invoker; + private Invoker syncInvoker; + private Invoker noLookupInvoker; public void observeBean(@Observes WeldProcessManagedBean pmb) { Collection> methods = pmb.getAnnotatedBeanClass().getMethods(); for (AnnotatedMethod m : methods) { - if ("hello".equals(m.getJavaMember().getName())) { + String name = m.getJavaMember().getName(); + if ("hello".equals(name)) { invoker = pmb.createInvoker(m) .withInstanceLookup() .withArgumentLookup(0) .build(); + } else if ("helloSync".equals(name)) { + syncInvoker = pmb.createInvoker(m) + .withInstanceLookup() + .withArgumentLookup(0) + .build(); + } else if ("helloNoLookup".equals(name)) { + noLookupInvoker = pmb.createInvoker(m).build(); } } } @@ -28,4 +38,12 @@ public void observeBean(@Observes WeldProcessManagedBean pmb) { public Invoker getInvoker() { return invoker; } + + public Invoker getSyncInvoker() { + return syncInvoker; + } + + public Invoker getNoLookupInvoker() { + return noLookupInvoker; + } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java index 9eb0a97695..f43a654a62 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.Arrays; import java.util.concurrent.CompletableFuture; import jakarta.enterprise.inject.spi.Extension; @@ -30,15 +31,66 @@ public static Archive deploy() { .addPackage(ParameterTypeAsyncHandlerTest.class.getPackage()) .addClass(DependentBean.class) .addAsServiceProvider(Extension.class, ParamTypeExtension.class) - .addAsServiceProvider(AsyncHandler.class, MyAsyncParamHandler.class); + .addAsServiceProvider(AsyncHandler.ParameterType.class, MyAsyncParamHandler.class); } @Inject ParamTypeExtension extension; + @Test + public void testTransformArgumentCalledBeforeMethodBody() throws Exception { + DependentBean.reset(); + InvocationOrder.reset(); + CompletableFuture future = new CompletableFuture<>(); + MyAsyncParam result = MyAsyncParam.createSuspended(); + + extension.getInvoker().invoke(null, new Object[] { null, future, result }); + + assertEquals(Arrays.asList("transformArgument", "methodBody"), InvocationOrder.events); + assertTrue("Method body should receive the wrapped argument from transformArgument", + InvocationOrder.receivedWrapped); + + future.complete("order-test"); + } + + @Test + public void testParameterTypeWithoutLookups() throws Exception { + InvocationOrder.reset(); + CompletableFuture future = new CompletableFuture<>(); + MyAsyncParam result = MyAsyncParam.createSuspended(); + + ParamTypeBean bean = new ParamTypeBean(); + extension.getNoLookupInvoker().invoke(bean, new Object[] { future, result }); + + assertFalse(result.isComplete()); + + future.complete("no-lookup-param"); + + assertTrue(result.isComplete()); + assertEquals("no-lookup-param", result.getIfComplete()); + } + + @Test + public void testSynchronousCompletion() throws Exception { + DependentBean.reset(); + MyAsyncParam result = MyAsyncParam.createSuspended(); + + assertEquals(0, DependentBean.destroyedCounter.get()); + + extension.getSyncInvoker().invoke(null, new Object[] { null, result }); + + // Method completed the async param synchronously during the method body. + // Cleanup must still happen — the completion callback must work even when + // fired during mh.invoke(). + assertEquals(1, DependentBean.destroyedCounter.get()); + assertTrue(result.isComplete()); + assertEquals("sync-hello", result.getIfComplete()); + } + @Test public void testParameterTypeHandler() throws Exception { DependentBean.reset(); + InvocationOrder.reset(); CompletableFuture future = new CompletableFuture<>(); MyAsyncParam result = MyAsyncParam.createSuspended(); diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/WrappedAsyncParam.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/WrappedAsyncParam.java new file mode 100644 index 0000000000..1835c48b1c --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/WrappedAsyncParam.java @@ -0,0 +1,29 @@ +package org.jboss.weld.tests.invokable.async.paramtype; + +public class WrappedAsyncParam implements MyAsyncParam { + private final MyAsyncParam delegate; + + WrappedAsyncParam(MyAsyncParam delegate) { + this.delegate = delegate; + } + + @Override + public boolean isComplete() { + return delegate.isComplete(); + } + + @Override + public T getIfComplete() { + return delegate.getIfComplete(); + } + + @Override + public void whenComplete(Runnable callback) { + delegate.whenComplete(callback); + } + + @Override + public void resume(T value) { + delegate.resume(value); + } +} From 8251efa29803624d66a044e9caf8bd11a9e07b45 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Mon, 4 May 2026 01:55:01 +0200 Subject: [PATCH 5/7] Implement @AutoClose feature --- .../jboss/weld/bean/AbstractProducerBean.java | 7 +- .../java/org/jboss/weld/bean/ManagedBean.java | 15 +- .../attributes/BeanAttributesFactory.java | 10 +- .../configurator/BeanConfiguratorImpl.java | 9 ++ .../unbound/DependentContextImpl.java | 8 +- .../producer/AbstractMemberProducer.java | 55 ++++--- .../producer/BasicInjectionTarget.java | 14 +- .../producer/BeanInjectionTarget.java | 14 +- .../metadata/cache/MergedStereotypes.java | 12 +- .../weld/metadata/cache/StereotypeModel.java | 14 ++ .../main/java/org/jboss/weld/util/Beans.java | 29 ++++ .../autoclose/basic/AutoCloseStereotype.java | 17 +++ .../tests/autoclose/basic/AutoCloseTest.java | 144 ++++++++++++++++++ .../autoclose/basic/AutoCloseableBean.java | 19 +++ .../tests/autoclose/basic/CloseableBean.java | 18 +++ .../autoclose/basic/CloseableResource.java | 20 +++ .../basic/NoAnnotationCloseableBean.java | 17 +++ .../autoclose/basic/NotAutoCloseableBean.java | 12 ++ .../basic/PreDestroyAutoCloseableBean.java | 22 +++ .../basic/ProducerFieldQualifier.java | 16 ++ .../basic/ProducerMethodQualifier.java | 16 ++ .../autoclose/basic/ResourceProducer.java | 38 +++++ .../autoclose/basic/StereotypedBean.java | 18 +++ .../basic/ThrowingAutoCloseableBean.java | 20 +++ .../ThrowingPreDestroyAutoCloseableBean.java | 23 +++ .../basic/WithDisposerQualifier.java | 16 ++ .../disposer/AutoCloseDisposerTest.java | 47 ++++++ .../autoclose/disposer/CloseableResource.java | 20 +++ .../disposer/ThrowingDisposerProducer.java | 25 +++ .../disposer/ThrowingDisposerQualifier.java | 16 ++ .../autoclose/instance/CloseCountingBean.java | 21 +++ .../instance/InstanceAutoCloseTest.java | 74 +++++++++ .../instance/PreDestroyAutoCloseableBean.java | 22 +++ .../instance/SimpleAutoCloseableBean.java | 19 +++ .../interceptor/AutoCloseInterceptorTest.java | 63 ++++++++ .../InterceptedAutoCloseableBean.java | 21 +++ .../autoclose/interceptor/Monitored.java | 15 ++ .../interceptor/MonitoringInterceptor.java | 20 +++ .../producer/AutoCloseProducerQualifier.java | 16 ++ .../autoclose/producer/CloseableResource.java | 20 +++ .../autoclose/producer/DependentHelper.java | 27 ++++ .../producer/DependentParamProducer.java | 25 +++ .../producer/DependentParamQualifier.java | 16 ++ .../producer/ProducerCaptureExtension.java | 26 ++++ .../ProducerDisposeAutoCloseTest.java | 79 ++++++++++ .../autoclose/producer/ResourceProducer.java | 24 +++ .../synthetic/AutoCloseBceCreator.java | 12 ++ .../synthetic/AutoCloseBceExtension.java | 19 +++ .../synthetic/AutoClosePortableExtension.java | 28 ++++ .../synthetic/AutoCloseSyntheticBeanTest.java | 83 ++++++++++ .../autoclose/synthetic/BceQualifier.java | 16 ++ .../synthetic/PortableExtQualifier.java | 16 ++ .../synthetic/SyntheticCloseableResource.java | 20 +++ .../synthetic/WithDisposerQualifier.java | 16 ++ .../translator/LiteExtensionTranslator.java | 1 + 55 files changed, 1360 insertions(+), 50 deletions(-) create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseStereotype.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableResource.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NoAnnotationCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NotAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/PreDestroyAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerFieldQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerMethodQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ResourceProducer.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/StereotypedBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingPreDestroyAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/WithDisposerQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/CloseableResource.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerProducer.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/CloseCountingBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/PreDestroyAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/SimpleAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/InterceptedAutoCloseableBean.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/Monitored.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/MonitoringInterceptor.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/AutoCloseProducerQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/CloseableResource.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentHelper.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamProducer.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerCaptureExtension.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ResourceProducer.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceCreator.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceExtension.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoClosePortableExtension.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/BceQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/PortableExtQualifier.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/SyntheticCloseableResource.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/WithDisposerQualifier.java diff --git a/impl/src/main/java/org/jboss/weld/bean/AbstractProducerBean.java b/impl/src/main/java/org/jboss/weld/bean/AbstractProducerBean.java index f50ae330f0..a641d4c885 100644 --- a/impl/src/main/java/org/jboss/weld/bean/AbstractProducerBean.java +++ b/impl/src/main/java/org/jboss/weld/bean/AbstractProducerBean.java @@ -199,10 +199,9 @@ public void destroy(T instance, CreationalContext creationalContext) { } catch (Exception e) { BeanLogger.LOG.errorDestroying(instance, this); BeanLogger.LOG.catchingDebug(e); - } finally { - if (getDeclaringBean().isDependent()) { - creationalContext.release(); - } + } + if (creationalContext != null) { + creationalContext.release(); } } diff --git a/impl/src/main/java/org/jboss/weld/bean/ManagedBean.java b/impl/src/main/java/org/jboss/weld/bean/ManagedBean.java index e1554f68ef..9f08f5bf81 100644 --- a/impl/src/main/java/org/jboss/weld/bean/ManagedBean.java +++ b/impl/src/main/java/org/jboss/weld/bean/ManagedBean.java @@ -194,19 +194,26 @@ public T create(CreationalContext creationalContext) { @Override public void destroy(T instance, CreationalContext creationalContext) { super.destroy(instance, creationalContext); + InjectionTarget injectionTarget = getProducer(); try { - InjectionTarget injectionTarget = getProducer(); injectionTarget.preDestroy(instance); + } catch (Exception e) { + BeanLogger.LOG.errorDestroying(instance, this); + BeanLogger.LOG.catchingDebug(e); + } + try { injectionTarget.dispose(instance); + } catch (Exception e) { + BeanLogger.LOG.errorDestroying(instance, this); + BeanLogger.LOG.catchingDebug(e); + } + if (creationalContext != null) { // WELD-1010 hack? if (creationalContext instanceof CreationalContextImpl) { ((CreationalContextImpl) creationalContext).release(this, instance); } else { creationalContext.release(); } - } catch (Exception e) { - BeanLogger.LOG.errorDestroying(instance, this); - BeanLogger.LOG.catchingDebug(e); } } diff --git a/impl/src/main/java/org/jboss/weld/bean/attributes/BeanAttributesFactory.java b/impl/src/main/java/org/jboss/weld/bean/attributes/BeanAttributesFactory.java index 50ba56ec44..0fb6048e42 100644 --- a/impl/src/main/java/org/jboss/weld/bean/attributes/BeanAttributesFactory.java +++ b/impl/src/main/java/org/jboss/weld/bean/attributes/BeanAttributesFactory.java @@ -69,6 +69,7 @@ public static class BeanAttributesBuilder { private boolean alternative; private boolean reserve; private boolean eager; + private boolean autoClose; private String name; private Set qualifiers; private Set types; @@ -83,6 +84,7 @@ public BeanAttributesBuilder(EnhancedAnnotated annotated, Set types, initAlternative(annotated); initReserve(annotated); initEager(annotated); + initAutoClose(annotated); initName(annotated); initQualifiers(annotated); initScope(annotated); @@ -109,6 +111,10 @@ protected void initEager(EnhancedAnnotated annotated) { this.eager = Beans.isEager(annotated, mergedStereotypes); } + protected void initAutoClose(EnhancedAnnotated annotated) { + this.autoClose = Beans.isAutoClose(annotated, mergedStereotypes); + } + /** * Initializes the name */ @@ -246,8 +252,8 @@ protected boolean initScopeFromStereotype() { } public BeanAttributes build() { - return new ImmutableBeanAttributes(mergedStereotypes.getStereotypes(), alternative, reserve, eager, name, - qualifiers, types, scope); + return new ImmutableBeanAttributes(mergedStereotypes.getStereotypes(), alternative, reserve, eager, + autoClose, name, qualifiers, types, scope); } } } diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java b/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java index c1a7357665..5a87211c89 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/events/configurator/BeanConfiguratorImpl.java @@ -54,6 +54,7 @@ import org.jboss.weld.logging.BeanLogger; import org.jboss.weld.manager.BeanManagerImpl; import org.jboss.weld.serialization.spi.BeanIdentifier; +import org.jboss.weld.util.Beans; import org.jboss.weld.util.ForwardingWeldInstance; import org.jboss.weld.util.bean.ForwardingBeanAttributes; import org.jboss.weld.util.collections.ImmutableSet; @@ -552,6 +553,14 @@ public void destroy(T instance, CreationalContext creationalContext) { if (destroyCallback != null) { destroyCallback.destroy(instance, creationalContext, beanManager); } + if (isAutoClose() && instance instanceof AutoCloseable) { + try { + Beans.invokeAutoClose(instance); + } catch (Exception e) { + BeanLogger.LOG.errorDestroying(instance, this); + BeanLogger.LOG.catchingDebug(e); + } + } // release dependent beans from create/destroy callbacks if (creationalContext instanceof CreationalContextImpl) { // release dependent instances linked with this bean but avoid double invocation diff --git a/impl/src/main/java/org/jboss/weld/contexts/unbound/DependentContextImpl.java b/impl/src/main/java/org/jboss/weld/contexts/unbound/DependentContextImpl.java index 65dbf4ee99..57d4dd5a7c 100644 --- a/impl/src/main/java/org/jboss/weld/contexts/unbound/DependentContextImpl.java +++ b/impl/src/main/java/org/jboss/weld/contexts/unbound/DependentContextImpl.java @@ -86,8 +86,8 @@ protected void addDependentInstance(T instance, Contextual contextual, We if (managedBean.getProducer() instanceof BasicInjectionTarget injectionTarget) { boolean hasPreDestroyMethods = injectionTarget.getLifecycleCallbackInvoker().hasPreDestroyMethods(); boolean hasPreDestroyInt = hasPreDestroyInterceptor(managedBean); - if (!hasPreDestroyMethods && !hasPreDestroyInt) { - // there is no @PreDestroy callback to call when destroying this dependent instance + if (!hasPreDestroyMethods && !hasPreDestroyInt && !managedBean.isAutoClose()) { + // there is no @PreDestroy callback or @AutoClose to handle when destroying this dependent instance // therefore, we do not need to keep the reference // Note that we need to account for @PreDestroy on the bean as well as interceptors return; @@ -96,8 +96,8 @@ protected void addDependentInstance(T instance, Contextual contextual, We } if (contextual instanceof AbstractProducerBean producerBean) { if (producerBean.getProducer() instanceof AbstractMemberProducer producer) { - if (producer.getDisposalMethod() == null) { - // there is no disposal method to call when destroying this dependent instance + if (producer.getDisposalMethod() == null && !producerBean.isAutoClose()) { + // there is no disposal method or @AutoClose to handle when destroying this dependent instance // therefore, we do not need to keep the reference return; } diff --git a/impl/src/main/java/org/jboss/weld/injection/producer/AbstractMemberProducer.java b/impl/src/main/java/org/jboss/weld/injection/producer/AbstractMemberProducer.java index 746b236910..f12ea59bca 100644 --- a/impl/src/main/java/org/jboss/weld/injection/producer/AbstractMemberProducer.java +++ b/impl/src/main/java/org/jboss/weld/injection/producer/AbstractMemberProducer.java @@ -37,6 +37,7 @@ import org.jboss.weld.exceptions.DefinitionException; import org.jboss.weld.logging.BeanLogger; import org.jboss.weld.manager.BeanManagerImpl; +import org.jboss.weld.util.Beans; /** * Common functionality for {@link Producer}s backing producer fields and producer methods. @@ -131,28 +132,42 @@ protected Object getReceiver(CreationalContext productCreationalContext, public void dispose(T instance) { if (disposalMethod != null) { - // CreationalContext is only created if we need it to obtain the receiver - // MethodInvocationStrategy takes care of creating CC for parameters, if needed - if (disposalMethod.getAnnotated().isStatic()) { - disposalMethod.invokeDisposeMethod(null, instance, null); - } else { - WeldCreationalContext ctx = null; - try { - Object receiver = ContextualInstance.getIfExists(getDeclaringBean(), getBeanManager()); - if (receiver == null) { - ctx = getBeanManager().createCreationalContext(null); - // Create child CC so that a dependent reciever may be destroyed after the disposer method completes - receiver = ContextualInstance.get(getDeclaringBean(), getBeanManager(), - ctx.getCreationalContext(getDeclaringBean())); - } - if (receiver != null) { - disposalMethod.invokeDisposeMethod(receiver, instance, ctx); - } - } finally { - if (ctx != null) { - ctx.release(); + try { + // CreationalContext is only created if we need it to obtain the receiver + // MethodInvocationStrategy takes care of creating CC for parameters, if needed + if (disposalMethod.getAnnotated().isStatic()) { + disposalMethod.invokeDisposeMethod(null, instance, null); + } else { + WeldCreationalContext ctx = null; + try { + Object receiver = ContextualInstance.getIfExists(getDeclaringBean(), getBeanManager()); + if (receiver == null) { + ctx = getBeanManager().createCreationalContext(null); + // Create child CC so that a dependent reciever may be destroyed after the disposer method completes + receiver = ContextualInstance.get(getDeclaringBean(), getBeanManager(), + ctx.getCreationalContext(getDeclaringBean())); + } + if (receiver != null) { + disposalMethod.invokeDisposeMethod(receiver, instance, ctx); + } + } finally { + if (ctx != null) { + ctx.release(); + } } } + } catch (Exception e) { + BeanLogger.LOG.errorDestroying(instance, getBean()); + BeanLogger.LOG.catchingDebug(e); + } + } + Bean bean = getBean(); + if (bean != null && bean.isAutoClose() && instance instanceof AutoCloseable) { + try { + Beans.invokeAutoClose(instance); + } catch (Exception e) { + BeanLogger.LOG.errorDestroying(instance, bean); + BeanLogger.LOG.catchingDebug(e); } } } diff --git a/impl/src/main/java/org/jboss/weld/injection/producer/BasicInjectionTarget.java b/impl/src/main/java/org/jboss/weld/injection/producer/BasicInjectionTarget.java index 401f197fcf..b8a784fa05 100644 --- a/impl/src/main/java/org/jboss/weld/injection/producer/BasicInjectionTarget.java +++ b/impl/src/main/java/org/jboss/weld/injection/producer/BasicInjectionTarget.java @@ -32,6 +32,7 @@ import org.jboss.weld.logging.BeanLogger; import org.jboss.weld.manager.BeanManagerImpl; import org.jboss.weld.manager.api.WeldInjectionTarget; +import org.jboss.weld.util.Beans; import org.jboss.weld.util.collections.ImmutableSet; import org.jboss.weld.util.reflection.Reflections; @@ -73,6 +74,7 @@ public static BasicInjectionTarget createNonCdiInterceptor(EnhancedAnnota protected final BeanManagerImpl beanManager; private final SlimAnnotatedType type; private final Set injectionPoints; + private final Bean bean; // Instantiation private Instantiator instantiator; @@ -87,6 +89,7 @@ protected BasicInjectionTarget(EnhancedAnnotatedType type, Bean bean, Bean protected BasicInjectionTarget(EnhancedAnnotatedType type, Bean bean, BeanManagerImpl beanManager, Injector injector, LifecycleCallbackInvoker invoker, Instantiator instantiator) { this.beanManager = beanManager; + this.bean = bean; this.type = type.slim(); this.injector = injector; this.invoker = invoker; @@ -138,7 +141,14 @@ public void preDestroy(T instance) { @Override public void dispose(T instance) { - // No-op + if (bean != null && bean.isAutoClose() && instance instanceof AutoCloseable) { + try { + Beans.invokeAutoClose(instance); + } catch (Exception e) { + BeanLogger.LOG.errorDestroying(instance, bean); + BeanLogger.LOG.catchingDebug(e); + } + } } @Override @@ -217,6 +227,6 @@ public String toString() { @Override public Bean getBean() { - return null; + return bean; } } diff --git a/impl/src/main/java/org/jboss/weld/injection/producer/BeanInjectionTarget.java b/impl/src/main/java/org/jboss/weld/injection/producer/BeanInjectionTarget.java index 8e6cb660d5..cdd577881e 100644 --- a/impl/src/main/java/org/jboss/weld/injection/producer/BeanInjectionTarget.java +++ b/impl/src/main/java/org/jboss/weld/injection/producer/BeanInjectionTarget.java @@ -54,23 +54,15 @@ public static BeanInjectionTarget forCdiInterceptor(EnhancedAnnotatedType NoopLifecycleCallbackInvoker. getInstance()); } - private final Bean bean; - public BeanInjectionTarget(EnhancedAnnotatedType type, Bean bean, BeanManagerImpl beanManager, Injector injector, LifecycleCallbackInvoker invoker) { super(type, bean, beanManager, injector, invoker); - this.bean = bean; } public BeanInjectionTarget(EnhancedAnnotatedType type, Bean bean, BeanManagerImpl beanManager) { this(type, bean, beanManager, ResourceInjector.of(type, bean, beanManager), DefaultLifecycleCallbackInvoker.of(type)); } - @Override - public void dispose(T instance) { - // No-op - } - protected boolean isInterceptor() { return (getBean() instanceof Interceptor) || getType().isAnnotationPresent(jakarta.interceptor.Interceptor.class); } @@ -195,7 +187,7 @@ protected void checkDecoratedMethods(EnhancedAnnotatedType type, List ctx) { T instance = super.produce(ctx); - if (bean != null && !bean.getScope().equals(Dependent.class) && !getInstantiator().hasDecoratorSupport()) { + if (getBean() != null && !getBean().getScope().equals(Dependent.class) && !getInstantiator().hasDecoratorSupport()) { // This should be safe, but needs verification PLM // Without this, the chaining of decorators will fail as the // incomplete instance will be resolved @@ -204,8 +196,4 @@ public T produce(CreationalContext ctx) { return instance; } - @Override - public Bean getBean() { - return bean; - } } diff --git a/impl/src/main/java/org/jboss/weld/metadata/cache/MergedStereotypes.java b/impl/src/main/java/org/jboss/weld/metadata/cache/MergedStereotypes.java index f78f6c024f..a3472e029b 100644 --- a/impl/src/main/java/org/jboss/weld/metadata/cache/MergedStereotypes.java +++ b/impl/src/main/java/org/jboss/weld/metadata/cache/MergedStereotypes.java @@ -43,6 +43,8 @@ public class MergedStereotypes { private boolean reserve; // Are any of the stereotypes eager private boolean eager; + // Are any of the stereotypes auto-closeable + private boolean autoClose; private Set> stereotypes; @@ -91,6 +93,9 @@ protected void merge(Set stereotypeAnnotations) { if (stereotype.isEager()) { eager = true; } + if (stereotype.isAutoClose()) { + autoClose = true; + } if (stereotype.getDefaultScopeType() != null) { possibleScopeTypes.add(stereotype.getDefaultScopeType()); } @@ -115,6 +120,10 @@ public boolean isEager() { return eager; } + public boolean isAutoClose() { + return autoClose; + } + /** * Returns the possible scope types * @@ -150,7 +159,8 @@ public String toString() { return "Merged stereotype model; Any of the stereotypes is an alternative: " + alternative + "; Any of the stereotypes is a reserve: " + reserve + "; Any of the stereotypes is eager: " + - eager + "; possible scopes " + possibleScopeTypes; + eager + "; Any of the stereotypes is auto-close: " + + autoClose + "; possible scopes " + possibleScopeTypes; } } diff --git a/impl/src/main/java/org/jboss/weld/metadata/cache/StereotypeModel.java b/impl/src/main/java/org/jboss/weld/metadata/cache/StereotypeModel.java index fe360dd831..a3afe534f3 100644 --- a/impl/src/main/java/org/jboss/weld/metadata/cache/StereotypeModel.java +++ b/impl/src/main/java/org/jboss/weld/metadata/cache/StereotypeModel.java @@ -28,6 +28,7 @@ import java.util.Set; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.AutoClose; import jakarta.enterprise.context.Eager; import jakarta.enterprise.context.NormalScope; import jakarta.enterprise.inject.Alternative; @@ -60,6 +61,8 @@ public class StereotypeModel extends AnnotationModel { private boolean reserve; // Is the stereotype eager private boolean eager; + // Is the stereotype auto-closeable + private boolean autoClose; // The default scope type private Annotation defaultScopeType; // Is the bean name defaulted @@ -85,6 +88,7 @@ protected void init(EnhancedAnnotation annotatedAnnotation) { initAlternative(annotatedAnnotation); initReserve(annotatedAnnotation); initEager(annotatedAnnotation); + initAutoClose(annotatedAnnotation); if (isAlternative() && isReserve()) { // stereotype cannot declare @Alternative and @Reserve at the same time throw MetadataLogger.LOG.alternativeAndReserveSimultaneously(annotatedAnnotation); @@ -176,6 +180,12 @@ private void initEager(EnhancedAnnotation annotatedAnnotation) { } } + private void initAutoClose(EnhancedAnnotation annotatedAnnotation) { + if (annotatedAnnotation.isAnnotationPresent(AutoClose.class)) { + this.autoClose = true; + } + } + @Override protected void check(EnhancedAnnotation annotatedAnnotation) { super.check(annotatedAnnotation); @@ -249,6 +259,10 @@ public boolean isEager() { return eager; } + public boolean isAutoClose() { + return autoClose; + } + public Set getInheritedStereotypes() { return inheritedStereotypes; } diff --git a/impl/src/main/java/org/jboss/weld/util/Beans.java b/impl/src/main/java/org/jboss/weld/util/Beans.java index fe468d31f0..25dc1bd662 100644 --- a/impl/src/main/java/org/jboss/weld/util/Beans.java +++ b/impl/src/main/java/org/jboss/weld/util/Beans.java @@ -39,6 +39,7 @@ import jakarta.annotation.Priority; import jakarta.decorator.Decorator; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.AutoClose; import jakarta.enterprise.context.ConversationScoped; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.context.Eager; @@ -79,6 +80,11 @@ import org.jboss.weld.bean.InterceptorImpl; import org.jboss.weld.bean.RIBean; import org.jboss.weld.bean.WeldBean; +import org.jboss.weld.bean.proxy.CombinedInterceptorAndDecoratorStackMethodHandler; +import org.jboss.weld.bean.proxy.InterceptionDecorationContext; +import org.jboss.weld.bean.proxy.InterceptionDecorationContext.Stack; +import org.jboss.weld.bean.proxy.MethodHandler; +import org.jboss.weld.bean.proxy.ProxyObject; import org.jboss.weld.bootstrap.api.ServiceRegistry; import org.jboss.weld.bootstrap.enablement.ModuleEnablement; import org.jboss.weld.injection.FieldInjectionPoint; @@ -315,6 +321,29 @@ public static boolean isEager(EnhancedAnnotated annotated, MergedStereotyp return annotated.isAnnotationPresent(Eager.class) || mergedStereotypes.isEager(); } + public static boolean isAutoClose(EnhancedAnnotated annotated, MergedStereotypes mergedStereotypes) { + return annotated.isAnnotationPresent(AutoClose.class) || mergedStereotypes.isAutoClose(); + } + + public static void invokeAutoClose(Object instance) throws Exception { + if (instance instanceof ProxyObject) { + MethodHandler handler = ((ProxyObject) instance).weld_getHandler(); + if (handler instanceof CombinedInterceptorAndDecoratorStackMethodHandler) { + Stack stack = InterceptionDecorationContext.startIfNotOnTop( + (CombinedInterceptorAndDecoratorStackMethodHandler) handler); + try { + ((AutoCloseable) instance).close(); + } finally { + if (stack != null) { + stack.end(); + } + } + return; + } + } + ((AutoCloseable) instance).close(); + } + public static EnhancedAnnotatedConstructor getBeanConstructorStrict(EnhancedAnnotatedType type) { EnhancedAnnotatedConstructor constructor = getBeanConstructor(type); if (constructor == null) { diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseStereotype.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseStereotype.java new file mode 100644 index 0000000000..b20aac6be0 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseStereotype.java @@ -0,0 +1,17 @@ +package org.jboss.weld.tests.autoclose.basic; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.inject.Stereotype; + +@Stereotype +@AutoClose +@Target(TYPE) +@Retention(RUNTIME) +public @interface AutoCloseStereotype { +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java new file mode 100644 index 0000000000..f7697e7c9e --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java @@ -0,0 +1,144 @@ +package org.jboss.weld.tests.autoclose.basic; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.annotation.Annotation; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanContainer; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.ActionSequence; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class AutoCloseTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(AutoCloseTest.class)) + .addPackage(AutoCloseTest.class.getPackage()); + } + + @Inject + BeanManager beanManager; + + @Inject + BeanContainer container; + + @Test + public void testAutoCloseDetected() { + Bean bean = beanManager.resolve(beanManager.getBeans(AutoCloseableBean.class)); + assertTrue(bean.isAutoClose()); + } + + @Test + public void testStereotypeAutoCloseDetected() { + Bean bean = beanManager.resolve(beanManager.getBeans(StereotypedBean.class)); + assertTrue(bean.isAutoClose()); + } + + @Test + public void testManagedBeanAutoClose() { + ActionSequence.reset(); + destroyViaHandle(AutoCloseableBean.class); + ActionSequence.assertSequenceDataEquals("AutoCloseableBean.close"); + } + + @Test + public void testAutoCloseOnNonAutoCloseableBean() { + ActionSequence.reset(); + destroyViaHandle(NotAutoCloseableBean.class); + assertNull(ActionSequence.getSequence()); + } + + @Test + public void testNoAnnotationMeansNoClose() { + ActionSequence.reset(); + destroyViaHandle(NoAnnotationCloseableBean.class); + assertNull(ActionSequence.getSequence()); + } + + @Test + public void testCloseExceptionSwallowed() { + ActionSequence.reset(); + destroyViaHandle(ThrowingAutoCloseableBean.class); + ActionSequence.assertSequenceDataEquals("ThrowingAutoCloseableBean.close"); + } + + @Test + public void testProducerMethodAutoClose() { + ActionSequence.reset(); + destroyViaBean(CloseableResource.class, ProducerMethodQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("producerMethod.close"); + } + + @Test + public void testProducerFieldAutoClose() { + ActionSequence.reset(); + destroyViaBean(CloseableResource.class, ProducerFieldQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("producerField.close"); + } + + @Test + public void testDisposerCalledBeforeClose() { + ActionSequence.reset(); + destroyViaBean(CloseableResource.class, WithDisposerQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("disposer", "withDisposer.close"); + } + + @Test + public void testStereotypeAutoClose() { + ActionSequence.reset(); + destroyViaHandle(StereotypedBean.class); + ActionSequence.assertSequenceDataEquals("StereotypedBean.close"); + } + + @Test + public void testPreDestroyCalledBeforeClose() { + ActionSequence.reset(); + destroyViaHandle(PreDestroyAutoCloseableBean.class); + ActionSequence.assertSequenceDataEquals("preDestroy", "close"); + } + + @Test + public void testCloseCalledEvenWhenPreDestroyThrows() { + ActionSequence.reset(); + destroyViaHandle(ThrowingPreDestroyAutoCloseableBean.class); + ActionSequence.assertSequenceDataEquals("preDestroy.throwing", "ThrowingPreDestroyAutoCloseableBean.close"); + } + + @Test + public void testCloseableInterfaceAutoClose() { + ActionSequence.reset(); + destroyViaHandle(CloseableBean.class); + ActionSequence.assertSequenceDataEquals("CloseableBean.close"); + } + + private void destroyViaHandle(Class type) { + Instance instance = container.createInstance(); + Instance.Handle handle = instance.select(type).getHandle(); + handle.get(); + handle.destroy(); + } + + @SuppressWarnings("unchecked") + private void destroyViaBean(Class type, Annotation... qualifiers) { + Bean bean = (Bean) beanManager.resolve(beanManager.getBeans(type, qualifiers)); + CreationalContext cc = beanManager.createCreationalContext(bean); + T instance = bean.create(cc); + bean.destroy(instance, cc); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseableBean.java new file mode 100644 index 0000000000..a4bd8bf81a --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseableBean.java @@ -0,0 +1,19 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +public class AutoCloseableBean implements AutoCloseable { + public String ping() { + return "open"; + } + + @Override + public void close() { + ActionSequence.addAction("AutoCloseableBean.close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableBean.java new file mode 100644 index 0000000000..4467c10989 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableBean.java @@ -0,0 +1,18 @@ +package org.jboss.weld.tests.autoclose.basic; + +import java.io.Closeable; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +public class CloseableBean implements Closeable { + + @Override + public void close() { + ActionSequence.addAction("CloseableBean.close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableResource.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableResource.java new file mode 100644 index 0000000000..9450ca4b7a --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/CloseableResource.java @@ -0,0 +1,20 @@ +package org.jboss.weld.tests.autoclose.basic; + +import org.jboss.weld.test.util.ActionSequence; + +public class CloseableResource implements AutoCloseable { + private final String name; + + public CloseableResource(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public void close() { + ActionSequence.addAction(name + ".close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NoAnnotationCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NoAnnotationCloseableBean.java new file mode 100644 index 0000000000..59b083ecd3 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NoAnnotationCloseableBean.java @@ -0,0 +1,17 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +public class NoAnnotationCloseableBean implements AutoCloseable { + public String ping() { + return "open"; + } + + @Override + public void close() { + ActionSequence.addAction("NoAnnotationCloseableBean.close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NotAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NotAutoCloseableBean.java new file mode 100644 index 0000000000..1d94b01f51 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/NotAutoCloseableBean.java @@ -0,0 +1,12 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +@Dependent +@AutoClose +public class NotAutoCloseableBean { + public String ping() { + return "open"; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/PreDestroyAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/PreDestroyAutoCloseableBean.java new file mode 100644 index 0000000000..cf1f207410 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/PreDestroyAutoCloseableBean.java @@ -0,0 +1,22 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +public class PreDestroyAutoCloseableBean implements AutoCloseable { + + @PreDestroy + public void preDestroy() { + ActionSequence.addAction("preDestroy"); + } + + @Override + public void close() { + ActionSequence.addAction("close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerFieldQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerFieldQualifier.java new file mode 100644 index 0000000000..e063b42a4d --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerFieldQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.basic; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface ProducerFieldQualifier { + class Literal extends AnnotationLiteral implements ProducerFieldQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerMethodQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerMethodQualifier.java new file mode 100644 index 0000000000..e3eed9eba6 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ProducerMethodQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.basic; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface ProducerMethodQualifier { + class Literal extends AnnotationLiteral implements ProducerMethodQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ResourceProducer.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ResourceProducer.java new file mode 100644 index 0000000000..80541450ba --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ResourceProducer.java @@ -0,0 +1,38 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +public class ResourceProducer { + + @Produces + @Dependent + @AutoClose + @ProducerMethodQualifier + public CloseableResource produceMethod() { + return new CloseableResource("producerMethod"); + } + + @Produces + @Dependent + @AutoClose + @ProducerFieldQualifier + public CloseableResource producerField = new CloseableResource("producerField"); + + @Produces + @Dependent + @AutoClose + @WithDisposerQualifier + public CloseableResource produceWithDisposer() { + return new CloseableResource("withDisposer"); + } + + public void disposeWithDisposer(@Disposes @WithDisposerQualifier CloseableResource resource) { + ActionSequence.addAction("disposer"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/StereotypedBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/StereotypedBean.java new file mode 100644 index 0000000000..92890dca55 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/StereotypedBean.java @@ -0,0 +1,18 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoCloseStereotype +public class StereotypedBean implements AutoCloseable { + public String ping() { + return "open"; + } + + @Override + public void close() { + ActionSequence.addAction("StereotypedBean.close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingAutoCloseableBean.java new file mode 100644 index 0000000000..ddc787d367 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingAutoCloseableBean.java @@ -0,0 +1,20 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +public class ThrowingAutoCloseableBean implements AutoCloseable { + public String ping() { + return "open"; + } + + @Override + public void close() throws Exception { + ActionSequence.addAction("ThrowingAutoCloseableBean.close"); + throw new RuntimeException("close failed"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingPreDestroyAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingPreDestroyAutoCloseableBean.java new file mode 100644 index 0000000000..fb63204d4f --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/ThrowingPreDestroyAutoCloseableBean.java @@ -0,0 +1,23 @@ +package org.jboss.weld.tests.autoclose.basic; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +public class ThrowingPreDestroyAutoCloseableBean implements AutoCloseable { + + @PreDestroy + public void preDestroy() { + ActionSequence.addAction("preDestroy.throwing"); + throw new RuntimeException("preDestroy failed"); + } + + @Override + public void close() { + ActionSequence.addAction("ThrowingPreDestroyAutoCloseableBean.close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/WithDisposerQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/WithDisposerQualifier.java new file mode 100644 index 0000000000..6ea7ed51f6 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/WithDisposerQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.basic; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface WithDisposerQualifier { + class Literal extends AnnotationLiteral implements WithDisposerQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java new file mode 100644 index 0000000000..56fe0b249e --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java @@ -0,0 +1,47 @@ +package org.jboss.weld.tests.autoclose.disposer; + +import java.lang.annotation.Annotation; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.ActionSequence; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class AutoCloseDisposerTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(AutoCloseDisposerTest.class)) + .addPackage(AutoCloseDisposerTest.class.getPackage()); + } + + @Inject + BeanManager beanManager; + + @Test + public void testCloseCalledEvenWhenDisposerThrows() { + ActionSequence.reset(); + createAndDestroy(CloseableResource.class, ThrowingDisposerQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("disposer.throwing", "throwingDisposer.close"); + } + + @SuppressWarnings("unchecked") + private void createAndDestroy(Class type, Annotation... qualifiers) { + Bean bean = (Bean) beanManager.resolve(beanManager.getBeans(type, qualifiers)); + CreationalContext cc = beanManager.createCreationalContext(bean); + T instance = bean.create(cc); + bean.destroy(instance, cc); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/CloseableResource.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/CloseableResource.java new file mode 100644 index 0000000000..ff8207c6aa --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/CloseableResource.java @@ -0,0 +1,20 @@ +package org.jboss.weld.tests.autoclose.disposer; + +import org.jboss.weld.test.util.ActionSequence; + +public class CloseableResource implements AutoCloseable { + private final String name; + + public CloseableResource(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public void close() { + ActionSequence.addAction(name + ".close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerProducer.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerProducer.java new file mode 100644 index 0000000000..8b0c6da5ff --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerProducer.java @@ -0,0 +1,25 @@ +package org.jboss.weld.tests.autoclose.disposer; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +public class ThrowingDisposerProducer { + + @Produces + @Dependent + @AutoClose + @ThrowingDisposerQualifier + public CloseableResource produceWithThrowingDisposer() { + return new CloseableResource("throwingDisposer"); + } + + public void dispose(@Disposes @ThrowingDisposerQualifier CloseableResource resource) { + ActionSequence.addAction("disposer.throwing"); + throw new RuntimeException("disposer failed"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerQualifier.java new file mode 100644 index 0000000000..0a040d2f98 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/ThrowingDisposerQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.disposer; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface ThrowingDisposerQualifier { + class Literal extends AnnotationLiteral implements ThrowingDisposerQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/CloseCountingBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/CloseCountingBean.java new file mode 100644 index 0000000000..8775b4bf33 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/CloseCountingBean.java @@ -0,0 +1,21 @@ +package org.jboss.weld.tests.autoclose.instance; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +@Dependent +@AutoClose +public class CloseCountingBean implements AutoCloseable { + public static final AtomicInteger closeCount = new AtomicInteger(0); + + public static void reset() { + closeCount.set(0); + } + + @Override + public void close() { + closeCount.incrementAndGet(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java new file mode 100644 index 0000000000..a4abd594d0 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java @@ -0,0 +1,74 @@ +package org.jboss.weld.tests.autoclose.instance; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.BeanContainer; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.ActionSequence; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class InstanceAutoCloseTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(InstanceAutoCloseTest.class)) + .addPackage(InstanceAutoCloseTest.class.getPackage()); + } + + @Inject + BeanContainer container; + + @Test + public void testHandleDestroyCallsClose() { + SimpleAutoCloseableBean.reset(); + Instance instance = container.createInstance(); + Instance.Handle handle = instance + .select(SimpleAutoCloseableBean.class).getHandle(); + handle.get(); + assertFalse(SimpleAutoCloseableBean.closed); + + handle.destroy(); + + assertTrue("close() should be called via Handle.destroy()", + SimpleAutoCloseableBean.closed); + } + + @Test + public void testHandleDestroyCallsCloseAndPreDestroy() { + ActionSequence.reset(); + Instance instance = container.createInstance(); + Instance.Handle handle = instance + .select(PreDestroyAutoCloseableBean.class).getHandle(); + handle.get(); + + handle.destroy(); + + ActionSequence.assertSequenceDataEquals("preDestroy", "close"); + } + + @Test + public void testCloseCalledExactlyOnce() { + CloseCountingBean.reset(); + Instance instance = container.createInstance(); + Instance.Handle handle = instance + .select(CloseCountingBean.class).getHandle(); + handle.get(); + + handle.destroy(); + + assertEquals("close() should be called exactly once", 1, CloseCountingBean.closeCount.get()); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/PreDestroyAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/PreDestroyAutoCloseableBean.java new file mode 100644 index 0000000000..9b4e27f377 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/PreDestroyAutoCloseableBean.java @@ -0,0 +1,22 @@ +package org.jboss.weld.tests.autoclose.instance; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +public class PreDestroyAutoCloseableBean implements AutoCloseable { + + @PreDestroy + public void preDestroy() { + ActionSequence.addAction("preDestroy"); + } + + @Override + public void close() { + ActionSequence.addAction("close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/SimpleAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/SimpleAutoCloseableBean.java new file mode 100644 index 0000000000..e664eb1a40 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/SimpleAutoCloseableBean.java @@ -0,0 +1,19 @@ +package org.jboss.weld.tests.autoclose.instance; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +@Dependent +@AutoClose +public class SimpleAutoCloseableBean implements AutoCloseable { + static boolean closed = false; + + public static void reset() { + closed = false; + } + + @Override + public void close() { + closed = true; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java new file mode 100644 index 0000000000..d5b0244ab2 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java @@ -0,0 +1,63 @@ +package org.jboss.weld.tests.autoclose.interceptor; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.ActionSequence; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class AutoCloseInterceptorTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(AutoCloseInterceptorTest.class)) + .addPackage(AutoCloseInterceptorTest.class.getPackage()); + } + + @Inject + BeanManager beanManager; + + @Test + public void testBusinessMethodStillIntercepted() { + ActionSequence.reset(); + Bean bean = resolveBean(); + CreationalContext cc = beanManager.createCreationalContext(bean); + InterceptedAutoCloseableBean instance = bean.create(cc); + instance.ping(); + ActionSequence.assertSequenceDataContainsAll("interceptor.aroundInvoke"); + bean.destroy(instance, cc); + } + + @Test + public void testInterceptorNotInvokedDuringAutoClose() { + ActionSequence.reset(); + Bean bean = resolveBean(); + CreationalContext cc = beanManager.createCreationalContext(bean); + InterceptedAutoCloseableBean instance = bean.create(cc); + bean.destroy(instance, cc); + assertTrue("close() should have been called", + ActionSequence.getSequenceData().contains("InterceptedAutoCloseableBean.close")); + assertFalse("Interceptor should NOT fire during close() in destruction", + ActionSequence.getSequenceData().contains("interceptor.aroundInvoke")); + } + + @SuppressWarnings("unchecked") + private Bean resolveBean() { + return (Bean) beanManager.resolve( + beanManager.getBeans(InterceptedAutoCloseableBean.class)); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/InterceptedAutoCloseableBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/InterceptedAutoCloseableBean.java new file mode 100644 index 0000000000..9f8af32916 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/InterceptedAutoCloseableBean.java @@ -0,0 +1,21 @@ +package org.jboss.weld.tests.autoclose.interceptor; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +@AutoClose +@Monitored +public class InterceptedAutoCloseableBean implements AutoCloseable { + + public String ping() { + return "open"; + } + + @Override + public void close() { + ActionSequence.addAction("InterceptedAutoCloseableBean.close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/Monitored.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/Monitored.java new file mode 100644 index 0000000000..1f68e86a54 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/Monitored.java @@ -0,0 +1,15 @@ +package org.jboss.weld.tests.autoclose.interceptor; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.interceptor.InterceptorBinding; + +@InterceptorBinding +@Target(TYPE) +@Retention(RUNTIME) +public @interface Monitored { +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/MonitoringInterceptor.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/MonitoringInterceptor.java new file mode 100644 index 0000000000..f64fac7892 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/MonitoringInterceptor.java @@ -0,0 +1,20 @@ +package org.jboss.weld.tests.autoclose.interceptor; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +import org.jboss.weld.test.util.ActionSequence; + +@Interceptor +@Monitored +@Priority(1000) +public class MonitoringInterceptor { + + @AroundInvoke + public Object aroundInvoke(InvocationContext context) throws Exception { + ActionSequence.addAction("interceptor.aroundInvoke"); + return context.proceed(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/AutoCloseProducerQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/AutoCloseProducerQualifier.java new file mode 100644 index 0000000000..336cb49233 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/AutoCloseProducerQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.producer; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface AutoCloseProducerQualifier { + class Literal extends AnnotationLiteral implements AutoCloseProducerQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/CloseableResource.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/CloseableResource.java new file mode 100644 index 0000000000..bc442c0e05 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/CloseableResource.java @@ -0,0 +1,20 @@ +package org.jboss.weld.tests.autoclose.producer; + +import org.jboss.weld.test.util.ActionSequence; + +public class CloseableResource implements AutoCloseable { + private final String name; + + public CloseableResource(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public void close() { + ActionSequence.addAction(name + ".close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentHelper.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentHelper.java new file mode 100644 index 0000000000..1072c14536 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentHelper.java @@ -0,0 +1,27 @@ +package org.jboss.weld.tests.autoclose.producer; + +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.Dependent; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +public class DependentHelper { + public static final AtomicBoolean destroyed = new AtomicBoolean(false); + + public static void reset() { + destroyed.set(false); + } + + public String ping() { + return "alive"; + } + + @PreDestroy + public void preDestroy() { + destroyed.set(true); + ActionSequence.addAction("DependentHelper.preDestroy"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamProducer.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamProducer.java new file mode 100644 index 0000000000..2ca1fd0dfe --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamProducer.java @@ -0,0 +1,25 @@ +package org.jboss.weld.tests.autoclose.producer; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; + +import org.jboss.weld.test.util.ActionSequence; + +@ApplicationScoped +public class DependentParamProducer { + + @Produces + @Dependent + @AutoClose + @DependentParamQualifier + public CloseableResource produce(DependentHelper helper) { + return new CloseableResource("withDependentParam"); + } + + public void dispose(@Disposes @DependentParamQualifier CloseableResource resource) { + ActionSequence.addAction("disposer"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamQualifier.java new file mode 100644 index 0000000000..a23bcb028a --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/DependentParamQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.producer; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface DependentParamQualifier { + class Literal extends AnnotationLiteral implements DependentParamQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerCaptureExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerCaptureExtension.java new file mode 100644 index 0000000000..5a6c53058b --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerCaptureExtension.java @@ -0,0 +1,26 @@ +package org.jboss.weld.tests.autoclose.producer; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessProducer; +import jakarta.enterprise.inject.spi.Producer; + +public class ProducerCaptureExtension implements Extension { + + private static volatile Producer capturedProducer; + + public void observeProducer(@Observes ProcessProducer event) { + if (event.getAnnotatedMember().isAnnotationPresent(AutoCloseProducerQualifier.class)) { + capturedProducer = event.getProducer(); + } + } + + @SuppressWarnings("unchecked") + public static Producer getCapturedProducer() { + return (Producer) capturedProducer; + } + + public static void reset() { + capturedProducer = null; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java new file mode 100644 index 0000000000..7931b97008 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java @@ -0,0 +1,79 @@ +package org.jboss.weld.tests.autoclose.producer; + +import static org.junit.Assert.assertNotNull; + +import java.lang.annotation.Annotation; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.Producer; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.ActionSequence; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Verifies that {@link Producer#dispose(Object)} calls {@code close()} on auto-closeable + * producer bean instances, as required by the CDI 5.0 spec's {@code Producer.dispose()} contract. + */ +@RunWith(Arquillian.class) +public class ProducerDisposeAutoCloseTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(ProducerDisposeAutoCloseTest.class)) + .addPackage(ProducerDisposeAutoCloseTest.class.getPackage()) + .addAsServiceProvider(Extension.class, ProducerCaptureExtension.class); + } + + @Inject + BeanManager beanManager; + + @Test + public void testProducerDisposeCallsClose() { + Producer producer = ProducerCaptureExtension.getCapturedProducer(); + assertNotNull("Extension should have captured the producer", producer); + + ActionSequence.reset(); + CreationalContext cc = beanManager.createCreationalContext(null); + CloseableResource instance = producer.produce(cc); + assertNotNull(instance); + + producer.dispose(instance); + + ActionSequence.assertSequenceDataEquals("disposer", "produced.close"); + } + + @Test + public void testBeanDestroyCallsClose() { + ActionSequence.reset(); + createAndDestroy(CloseableResource.class, AutoCloseProducerQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("disposer", "produced.close"); + } + + @Test + public void testDependentObjectsDestroyedAfterDisposerAndClose() { + ActionSequence.reset(); + DependentHelper.reset(); + createAndDestroy(CloseableResource.class, DependentParamQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("disposer", "withDependentParam.close", "DependentHelper.preDestroy"); + } + + @SuppressWarnings("unchecked") + private void createAndDestroy(Class type, Annotation... qualifiers) { + Bean bean = (Bean) beanManager.resolve(beanManager.getBeans(type, qualifiers)); + CreationalContext cc = beanManager.createCreationalContext(bean); + T instance = bean.create(cc); + bean.destroy(instance, cc); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ResourceProducer.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ResourceProducer.java new file mode 100644 index 0000000000..6115e087e9 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ResourceProducer.java @@ -0,0 +1,24 @@ +package org.jboss.weld.tests.autoclose.producer; + +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; + +import org.jboss.weld.test.util.ActionSequence; + +@Dependent +public class ResourceProducer { + + @Produces + @Dependent + @AutoClose + @AutoCloseProducerQualifier + public CloseableResource produce() { + return new CloseableResource("produced"); + } + + public void dispose(@Disposes @AutoCloseProducerQualifier CloseableResource resource) { + ActionSequence.addAction("disposer"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceCreator.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceCreator.java new file mode 100644 index 0000000000..d1939dcf1c --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceCreator.java @@ -0,0 +1,12 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticInjections; + +public class AutoCloseBceCreator implements SyntheticBeanCreator { + @Override + public SyntheticCloseableResource create(SyntheticInjections injections, Parameters params) { + return new SyntheticCloseableResource("bce"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceExtension.java new file mode 100644 index 0000000000..f0de23777a --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseBceExtension.java @@ -0,0 +1,19 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; + +public class AutoCloseBceExtension implements BuildCompatibleExtension { + + @Synthesis + public void register(SyntheticComponents syn) { + syn.addBean(SyntheticCloseableResource.class) + .type(SyntheticCloseableResource.class) + .qualifier(BceQualifier.class) + .scope(Dependent.class) + .autoClose(true) + .createWith(AutoCloseBceCreator.class); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoClosePortableExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoClosePortableExtension.java new file mode 100644 index 0000000000..972d0fd4c9 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoClosePortableExtension.java @@ -0,0 +1,28 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; + +import org.jboss.weld.test.util.ActionSequence; + +public class AutoClosePortableExtension implements Extension { + + public void afterBeanDiscovery(@Observes AfterBeanDiscovery event) { + event.addBean() + .addType(SyntheticCloseableResource.class) + .addQualifier(PortableExtQualifier.Literal.INSTANCE) + .scope(Dependent.class) + .autoClose(true) + .produceWith(lookup -> new SyntheticCloseableResource("portableExt")); + + event.addBean() + .addType(SyntheticCloseableResource.class) + .addQualifier(WithDisposerQualifier.Literal.INSTANCE) + .scope(Dependent.class) + .autoClose(true) + .produceWith(lookup -> new SyntheticCloseableResource("withDisposer")) + .disposeWith((instance, lookup) -> ActionSequence.addAction("syntheticDisposer")); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java new file mode 100644 index 0000000000..92054acd5e --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java @@ -0,0 +1,83 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import static org.junit.Assert.assertTrue; + +import java.lang.annotation.Annotation; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.ActionSequence; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class AutoCloseSyntheticBeanTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(AutoCloseSyntheticBeanTest.class)) + .addPackage(AutoCloseSyntheticBeanTest.class.getPackage()) + .addAsServiceProvider(Extension.class, AutoClosePortableExtension.class) + .addAsServiceProvider(BuildCompatibleExtension.class, AutoCloseBceExtension.class); + } + + @Inject + BeanManager beanManager; + + @Test + public void testPortableExtensionAutoCloseFlag() { + Bean bean = beanManager.resolve( + beanManager.getBeans(SyntheticCloseableResource.class, PortableExtQualifier.Literal.INSTANCE)); + assertTrue("Synthetic bean via portable extension should have isAutoClose() == true", + bean.isAutoClose()); + } + + @Test + public void testPortableExtensionAutoClose() { + ActionSequence.reset(); + createAndDestroy(SyntheticCloseableResource.class, PortableExtQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("portableExt.close"); + } + + @Test + public void testBceSyntheticBeanAutoCloseFlag() { + Bean bean = beanManager.resolve( + beanManager.getBeans(SyntheticCloseableResource.class, BceQualifier.Literal.INSTANCE)); + assertTrue("Synthetic bean via BCE should have isAutoClose() == true", + bean.isAutoClose()); + } + + @Test + public void testBceSyntheticBeanAutoClose() { + ActionSequence.reset(); + createAndDestroy(SyntheticCloseableResource.class, BceQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("bce.close"); + } + + @Test + public void testSyntheticBeanDisposerCalledBeforeClose() { + ActionSequence.reset(); + createAndDestroy(SyntheticCloseableResource.class, WithDisposerQualifier.Literal.INSTANCE); + ActionSequence.assertSequenceDataEquals("syntheticDisposer", "withDisposer.close"); + } + + @SuppressWarnings("unchecked") + private void createAndDestroy(Class type, Annotation... qualifiers) { + Bean bean = (Bean) beanManager.resolve(beanManager.getBeans(type, qualifiers)); + CreationalContext cc = beanManager.createCreationalContext(bean); + T instance = bean.create(cc); + bean.destroy(instance, cc); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/BceQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/BceQualifier.java new file mode 100644 index 0000000000..6bf021400b --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/BceQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface BceQualifier { + class Literal extends AnnotationLiteral implements BceQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/PortableExtQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/PortableExtQualifier.java new file mode 100644 index 0000000000..0c06f82243 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/PortableExtQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface PortableExtQualifier { + class Literal extends AnnotationLiteral implements PortableExtQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/SyntheticCloseableResource.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/SyntheticCloseableResource.java new file mode 100644 index 0000000000..49341b12f6 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/SyntheticCloseableResource.java @@ -0,0 +1,20 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import org.jboss.weld.test.util.ActionSequence; + +public class SyntheticCloseableResource implements AutoCloseable { + private final String name; + + public SyntheticCloseableResource(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public void close() { + ActionSequence.addAction(name + ".close"); + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/WithDisposerQualifier.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/WithDisposerQualifier.java new file mode 100644 index 0000000000..54539ca8f4 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/WithDisposerQualifier.java @@ -0,0 +1,16 @@ +package org.jboss.weld.tests.autoclose.synthetic; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RUNTIME) +public @interface WithDisposerQualifier { + class Literal extends AnnotationLiteral implements WithDisposerQualifier { + public static final Literal INSTANCE = new Literal(); + } +} diff --git a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/LiteExtensionTranslator.java b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/LiteExtensionTranslator.java index b744a74799..841866f454 100644 --- a/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/LiteExtensionTranslator.java +++ b/weld-lite-extension-translator/src/main/java/org/jboss/weld/lite/extension/translator/LiteExtensionTranslator.java @@ -186,6 +186,7 @@ public void synthesis(@Priority(Integer.MAX_VALUE) @Observes jakarta.enterprise. configurator.alternative(syntheticBean.isAlternative); configurator.reserve(syntheticBean.isReserve); configurator.eager(syntheticBean.isEager); + configurator.autoClose(syntheticBean.isAutoClose); configurator.priority(syntheticBean.priority); configurator.name(syntheticBean.name); configurator.stereotypes(syntheticBean.stereotypes); From 9415d9a7b553eed36d6e8606352a2074c4c832f3 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Mon, 4 May 2026 17:00:00 +0200 Subject: [PATCH 6/7] Bump CDI API version in jboss-as module to 5.0.0.Alpha6 --- jboss-as/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jboss-as/pom.xml b/jboss-as/pom.xml index ba1b386423..fecaa2f6e8 100644 --- a/jboss-as/pom.xml +++ b/jboss-as/pom.xml @@ -32,7 +32,7 @@ as the release plugin won't deal with double evaluation --> 7.0.0-SNAPSHOT - 5.0.0.Alpha5 + 5.0.0.Alpha6 From e4c9933c014ce4a3d1f362d622516568ed9f18e4 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Mon, 4 May 2026 17:58:51 +0200 Subject: [PATCH 7/7] Add ActionSequence to @AutoClose test deployments for in-container runs --- .../org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java | 3 ++- .../weld/tests/autoclose/disposer/AutoCloseDisposerTest.java | 3 ++- .../weld/tests/autoclose/instance/InstanceAutoCloseTest.java | 3 ++- .../tests/autoclose/interceptor/AutoCloseInterceptorTest.java | 3 ++- .../tests/autoclose/producer/ProducerDisposeAutoCloseTest.java | 1 + .../tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java | 1 + 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java index f7697e7c9e..591b07c1f6 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/basic/AutoCloseTest.java @@ -29,7 +29,8 @@ public class AutoCloseTest { public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(AutoCloseTest.class)) - .addPackage(AutoCloseTest.class.getPackage()); + .addPackage(AutoCloseTest.class.getPackage()) + .addClass(ActionSequence.class); } @Inject diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java index 56fe0b249e..6cac0cd13a 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/disposer/AutoCloseDisposerTest.java @@ -24,7 +24,8 @@ public class AutoCloseDisposerTest { public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(AutoCloseDisposerTest.class)) - .addPackage(AutoCloseDisposerTest.class.getPackage()); + .addPackage(AutoCloseDisposerTest.class.getPackage()) + .addClass(ActionSequence.class); } @Inject diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java index a4abd594d0..1a1664e8a8 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/instance/InstanceAutoCloseTest.java @@ -25,7 +25,8 @@ public class InstanceAutoCloseTest { public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(InstanceAutoCloseTest.class)) - .addPackage(InstanceAutoCloseTest.class.getPackage()); + .addPackage(InstanceAutoCloseTest.class.getPackage()) + .addClass(ActionSequence.class); } @Inject diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java index d5b0244ab2..efa5b19929 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/interceptor/AutoCloseInterceptorTest.java @@ -25,7 +25,8 @@ public class AutoCloseInterceptorTest { public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(AutoCloseInterceptorTest.class)) - .addPackage(AutoCloseInterceptorTest.class.getPackage()); + .addPackage(AutoCloseInterceptorTest.class.getPackage()) + .addClass(ActionSequence.class); } @Inject diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java index 7931b97008..529ea07136 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/producer/ProducerDisposeAutoCloseTest.java @@ -33,6 +33,7 @@ public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(ProducerDisposeAutoCloseTest.class)) .addPackage(ProducerDisposeAutoCloseTest.class.getPackage()) + .addClass(ActionSequence.class) .addAsServiceProvider(Extension.class, ProducerCaptureExtension.class); } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java index 92054acd5e..39d800bb9d 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/autoclose/synthetic/AutoCloseSyntheticBeanTest.java @@ -29,6 +29,7 @@ public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(AutoCloseSyntheticBeanTest.class)) .addPackage(AutoCloseSyntheticBeanTest.class.getPackage()) + .addClass(ActionSequence.class) .addAsServiceProvider(Extension.class, AutoClosePortableExtension.class) .addAsServiceProvider(BuildCompatibleExtension.class, AutoCloseBceExtension.class); }