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 202235e36c..402936a8fa 100644
--- a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java
+++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java
@@ -69,7 +69,7 @@ private void validateAndRegisterReturnType(AsyncHandler.ReturnType> handler) {
validateDirectImplementation(handlerClass, AsyncHandler.ReturnType.class);
Class> asyncType = extractAsyncType(handlerClass, AsyncHandler.ReturnType.class);
checkDuplicate(asyncType, handlerClass, true);
- handlers.put(asyncType, HandlerInfo.returnType(handler, asyncType));
+ handlers.putIfAbsent(asyncType, HandlerInfo.returnType(handler, asyncType));
}
private void validateAndRegisterParameterType(AsyncHandler.ParameterType> handler) {
@@ -77,7 +77,7 @@ private void validateAndRegisterParameterType(AsyncHandler.ParameterType> hand
validateDirectImplementation(handlerClass, AsyncHandler.ParameterType.class);
Class> asyncType = extractAsyncType(handlerClass, AsyncHandler.ParameterType.class);
checkDuplicate(asyncType, handlerClass, false);
- handlers.put(asyncType, HandlerInfo.parameterType(handler, asyncType));
+ handlers.putIfAbsent(asyncType, HandlerInfo.parameterType(handler, asyncType));
}
private void validateDirectImplementation(Class> handlerClass, Class> targetInterface) {
@@ -92,6 +92,15 @@ private void validateDirectImplementation(Class> handlerClass, Class> target
private void checkDuplicate(Class> asyncType, Class> handlerClass, boolean isReturnType) {
HandlerInfo existing = handlers.get(asyncType);
if (existing != null && !existing.isBuiltin()) {
+ // In a WAR with multiple BDAs (e.g. WEB-INF/classes + JARs in WEB-INF/lib),
+ // all BDAs share the same classloader. Since discovery runs per BDA, the same
+ // service file is found multiple times, yielding the same handler class.
+ // This is not an error — skip re-registration. In an EAR with isolated module
+ // classloaders, different Class objects would be loaded, so this identity check
+ // does not suppress genuine duplicates across modules.
+ if (existing.getHandlerClass() == handlerClass && existing.isReturnType() == isReturnType) {
+ return;
+ }
if (existing.getHandlerClass() == handlerClass && existing.isReturnType() != isReturnType) {
throw InvokerLogger.LOG.asyncHandlerBothKinds(handlerClass, asyncType);
}
diff --git a/pom.xml b/pom.xml
index 902131ab95..07fad42500 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,7 +61,7 @@
1.2.4.Final
2.0.1
- 5.0.0.Alpha2
+ 5.0.0.Alpha7
11.0.3
diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/LibraryBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/LibraryBean.java
new file mode 100644
index 0000000000..13834321dc
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/LibraryBean.java
@@ -0,0 +1,10 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class LibraryBean {
+ public String ping() {
+ return "pong";
+ }
+}
diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncType.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncType.java
new file mode 100644
index 0000000000..0035e92207
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncType.java
@@ -0,0 +1,15 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+import java.util.concurrent.CompletableFuture;
+
+public interface MyAsyncType {
+ boolean isComplete();
+
+ T getIfComplete();
+
+ MyAsyncType whenComplete(Runnable callback);
+
+ static MyAsyncType from(CompletableFuture future) {
+ return new MyAsyncTypeImpl<>(future);
+ }
+}
diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncTypeHandler.java
new file mode 100644
index 0000000000..f70ea92a1b
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncTypeHandler.java
@@ -0,0 +1,10 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+import jakarta.enterprise.invoke.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/wardedup/MyAsyncTypeImpl.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncTypeImpl.java
new file mode 100644
index 0000000000..9fc00f40bc
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/MyAsyncTypeImpl.java
@@ -0,0 +1,36 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+import java.util.concurrent.CompletableFuture;
+
+final class MyAsyncTypeImpl implements MyAsyncType {
+ private final CompletableFuture future;
+ private Runnable callback;
+
+ MyAsyncTypeImpl(CompletableFuture future) {
+ this.future = future;
+ }
+
+ @Override
+ public boolean isComplete() {
+ return future.isDone();
+ }
+
+ @Override
+ public T getIfComplete() {
+ if (future.isDone()) {
+ return future.getNow(null);
+ }
+ throw new IllegalStateException("not yet complete");
+ }
+
+ @Override
+ public MyAsyncType whenComplete(Runnable callback) {
+ this.callback = callback;
+ future.whenComplete((v, e) -> {
+ if (this.callback != null) {
+ this.callback.run();
+ }
+ });
+ return this;
+ }
+}
diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupAsyncHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupAsyncHandlerTest.java
new file mode 100644
index 0000000000..a5b5fdb72e
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupAsyncHandlerTest.java
@@ -0,0 +1,79 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+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.shrinkwrap.api.asset.EmptyAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.jboss.weld.test.util.Utils;
+import org.jboss.weld.tests.category.Integration;
+import org.jboss.weld.tests.invokable.async.DependentBean;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+/**
+ * Verifies that async handler discovery works in a WAR with multiple BDAs
+ * sharing the same classloader. The service file in WEB-INF/classes is visible
+ * to all BDAs, so the handler must be deduplicated rather than rejected.
+ */
+@RunWith(Arquillian.class)
+@Category(Integration.class)
+public class WarDedupAsyncHandlerTest {
+
+ @Deployment
+ public static Archive> deploy() {
+ // The library JAR creates a second BDA within the WAR. Both BDAs share
+ // the WAR classloader, so async handler discovery (which runs per BDA)
+ // finds the same META-INF/services/ file twice.
+ JavaArchive lib = ShrinkWrap.create(BeanArchive.class)
+ .addClass(LibraryBean.class);
+
+ return ShrinkWrap
+ .create(WebArchive.class, Utils.getDeploymentNameAsHash(WarDedupAsyncHandlerTest.class, Utils.ARCHIVE_TYPE.WAR))
+ .addClasses(WarDedupBean.class, WarDedupExtension.class,
+ MyAsyncType.class, MyAsyncTypeImpl.class, MyAsyncTypeHandler.class,
+ DependentBean.class)
+ .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
+ .addAsServiceProvider(Extension.class, WarDedupExtension.class)
+ .addAsServiceProvider(AsyncHandler.ReturnType.class, MyAsyncTypeHandler.class)
+ .addAsLibrary(lib);
+ }
+
+ @Inject
+ WarDedupExtension extension;
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testCustomReturnTypeHandlerInWar() throws Exception {
+ DependentBean.reset();
+ CompletableFuture future = new CompletableFuture<>();
+
+ assertEquals(0, DependentBean.destroyedCounter.get());
+
+ MyAsyncType result = (MyAsyncType) extension.getInvoker()
+ .invoke(null, new Object[] { null, future });
+
+ assertEquals(0, DependentBean.destroyedCounter.get());
+ assertFalse(result.isComplete());
+
+ future.complete("dedup");
+
+ assertEquals(1, DependentBean.destroyedCounter.get());
+ assertTrue(result.isComplete());
+ assertEquals("dedup", result.getIfComplete());
+ }
+}
diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupBean.java
new file mode 100644
index 0000000000..fa8be0b512
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupBean.java
@@ -0,0 +1,14 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+import java.util.concurrent.CompletableFuture;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.jboss.weld.tests.invokable.async.DependentBean;
+
+@ApplicationScoped
+public class WarDedupBean {
+ public MyAsyncType hello(DependentBean dep, CompletableFuture future) {
+ return MyAsyncType.from(future);
+ }
+}
diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupExtension.java
new file mode 100644
index 0000000000..4890c92e6e
--- /dev/null
+++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/wardedup/WarDedupExtension.java
@@ -0,0 +1,35 @@
+package org.jboss.weld.tests.invokable.async.wardedup;
+
+import java.util.Collection;
+
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.inject.spi.AfterDeploymentValidation;
+import jakarta.enterprise.inject.spi.AnnotatedMethod;
+import jakarta.enterprise.inject.spi.Extension;
+import jakarta.enterprise.inject.spi.ProcessManagedBean;
+import jakarta.enterprise.invoke.Invoker;
+
+public class WarDedupExtension implements Extension {
+
+ private Invoker invoker;
+
+ public void observeBean(@Observes ProcessManagedBean pmb) {
+ Collection> methods = pmb.getAnnotatedBeanClass().getMethods();
+ for (AnnotatedMethod super WarDedupBean> m : methods) {
+ if ("hello".equals(m.getJavaMember().getName())) {
+ invoker = pmb.createInvoker(m)
+ .withInstanceLookup()
+ .withArgumentLookup(0)
+ .build();
+ }
+ }
+ }
+
+ public void validate(@Observes AfterDeploymentValidation adv) {
+ adv.ensureAsyncHandlerExists(MyAsyncType.class);
+ }
+
+ public Invoker getInvoker() {
+ return invoker;
+ }
+}