Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ 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) {
Class<?> handlerClass = handler.getClass();
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) {
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<arquillian.tomcat.version>1.2.4.Final</arquillian.tomcat.version>
<atinject.tck.version>2.0.1</atinject.tck.version>
<!-- Version of the CDI 4.1 TCK release -->
<cdi.tck.version>5.0.0.Alpha2</cdi.tck.version>
<cdi.tck.version>5.0.0.Alpha7</cdi.tck.version>
<!-- Version of the Jakarta Platform TCK 4.1 release -->
<platform.tck.version>11.0.3</platform.tck.version>
<!-- By default, each mvn profile uses corresponding file from TCK repo, see jboss-tck-runner/pom.xml -->
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.jboss.weld.tests.invokable.async.wardedup;

import java.util.concurrent.CompletableFuture;

public interface MyAsyncType<T> {
boolean isComplete();

T getIfComplete();

MyAsyncType<T> whenComplete(Runnable callback);

static <T> MyAsyncType<T> from(CompletableFuture<T> future) {
return new MyAsyncTypeImpl<>(future);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.jboss.weld.tests.invokable.async.wardedup;

import jakarta.enterprise.invoke.AsyncHandler;

public class MyAsyncTypeHandler<T> implements AsyncHandler.ReturnType<MyAsyncType<T>> {
@Override
public MyAsyncType<T> transform(MyAsyncType<T> original, Runnable completion) {
return original.whenComplete(completion);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.jboss.weld.tests.invokable.async.wardedup;

import java.util.concurrent.CompletableFuture;

final class MyAsyncTypeImpl<T> implements MyAsyncType<T> {
private final CompletableFuture<T> future;
private Runnable callback;

MyAsyncTypeImpl(CompletableFuture<T> 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<T> whenComplete(Runnable callback) {
this.callback = callback;
future.whenComplete((v, e) -> {
if (this.callback != null) {
this.callback.run();
}
});
return this;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> future = new CompletableFuture<>();

assertEquals(0, DependentBean.destroyedCounter.get());

MyAsyncType<String> result = (MyAsyncType<String>) 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<String> hello(DependentBean dep, CompletableFuture<String> future) {
return MyAsyncType.from(future);
}
}
Original file line number Diff line number Diff line change
@@ -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<WarDedupBean, ?> invoker;

public void observeBean(@Observes ProcessManagedBean<WarDedupBean> pmb) {
Collection<AnnotatedMethod<? super WarDedupBean>> 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<WarDedupBean, ?> getInvoker() {
return invoker;
}
}
Loading