quarkusio/quarkus-quickstarts

[Quarkus][v3.13.2] AmbiguousResolutionException caused by named beans with different names.

emakunin opened this issue · 3 comments

Hi folks,

I'm trying to migrate to Quarkus v3.13.2 (tried also v3.11 with the same effect). I have a separate extension to store REST clients. the extension has a REST interface, META-INF/bean.xml and a provoder that provides @nAmed beans.

My setup

Quarkus 3.13.2

// Rest interface:
@Path("/inbound/fba/2024-03-20")
@RegisterRestClient
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface FbaInboundV2024Api {
... 
    @POST
    @Path("inboundPlans")
    CreateInboundPlanResponse createInboundPlan(CreateInboundPlanRequest body);
...
}

// Producer code
@ApplicationScoped
public class ApiProducer {
    @Produces
    @ApplicationScoped
    @Named("FbaInboundV2024ApiEu")
    public FbaInboundV2024Api fbaInboundV2024ApiEu(@Named("builderEu") QuarkusRestClientBuilder builderEu) {
        return builderEu.build(FbaInboundV2024Api.class);
    }

    @Produces
    @ApplicationScoped
    @Named("FbaInboundV2024ApiNa")
    public FbaInboundV2024Api fbaInboundV2024ApiNa(@Named("builderNa") QuarkusRestClientBuilder builderNa) {
        return builderNa.build(FbaInboundV2024Api.class);
    }

    @Produces
    @ApplicationScoped
    @Named("builderEu")
    private QuarkusRestClientBuilder builderEu(@Named("spAuthInterceptorWithBodyEu") SpAuthInterceptorWithBody spAuthInterceptorWithBodyEu,
                                            @Named("spAuthInterceptorNoBodyEu") SpAuthInterceptorNoBody spAuthInterceptorNoBody,
                                            @ConfigProperty(name = "spa.url.eu", defaultValue = "https://sellingpartnerapi-eu.amazon.com") String baseUriEu) {
            return defaultBuilder(baseUriEu)
                    .register(spAuthInterceptorWithBodyEu)
                    .register(spAuthInterceptorNoBody);
        }
    
    @Produces
    @ApplicationScoped
    @Named("builderNa")
    private QuarkusRestClientBuilder builderNa(@Named("spAuthInterceptorWithBodyNa") SpAuthInterceptorWithBody spAuthInterceptorWithBodyNa,
                                        @Named("spAuthInterceptorNoBodyNa") SpAuthInterceptorNoBody spAuthInterceptorNoBodyNa,
                                        @ConfigProperty(name = "spa.url.na", defaultValue = "https://sellingpartnerapi-na.amazon.com") String baseUriNa) {
        return defaultBuilder(baseUriNa)
                .register(spAuthInterceptorWithBodyNa)
                .register(spAuthInterceptorNoBodyNa);
    }

    private static QuarkusRestClientBuilder defaultBuilder(String uri) {
        return QuarkusRestClientBuilder.newBuilder()
                .baseUri(URI.create(uri))
                .queryParamStyle(QueryParamStyle.COMMA_SEPARATED)
                .register(SpApiExceptionMapper.class);
    }
}

I have below dependencies in the extension runtime package (plus correspoinding ones with *-deployment suffix in deployment lib). Plus the same dependencies in my lambda-rest package.

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-rest-client</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-rest-client-jackson</artifactId>
        </dependency>

My rest service uses

        <!-- Quarkus rest lambda -->
        <dependency>
            <!-- Generates rest API handler lambda -->
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-amazon-lambda-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-rest-client</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-rest-client-jackson</artifactId>
        </dependency>

When I have META-INF/beans.xml file in my extension, then I get producers conflict in my REST lambda.

2024-08-13 13:29:03,656 ERROR [io.qua.run.boo.StartupActionImpl] (Quarkus Main Thread) Error running Quarkus: java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at io.quarkus.runner.bootstrap.StartupActionImpl$1.run(StartupActionImpl.java:116)
        at java.base/java.lang.Thread.run(Thread.java:1570)
Caused by: java.lang.ExceptionInInitializerError
        at java.base/jdk.internal.misc.Unsafe.ensureClassInitialized0(Native Method)
        at java.base/jdk.internal.misc.Unsafe.ensureClassInitialized(Unsafe.java:1160)
        at java.base/jdk.internal.reflect.MethodHandleAccessorFactory.ensureClassInitialized(MethodHandleAccessorFactory.java:340)
        at java.base/jdk.internal.reflect.MethodHandleAccessorFactory.newConstructorAccessor(MethodHandleAccessorFactory.java:103)
        at java.base/jdk.internal.reflect.ReflectionFactory.newConstructorAccessor(ReflectionFactory.java:173)
        at java.base/java.lang.reflect.Constructor.acquireConstructorAccessor(Constructor.java:549)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
        at io.quarkus.runtime.Quarkus.run(Quarkus.java:70)
        at io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
        at io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
        at io.quarkus.runner.GeneratedMain.main(Unknown Source)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        ... 3 more
Caused by: java.lang.RuntimeException: Failed to start quarkus
        at io.quarkus.runner.ApplicationImpl.<clinit>(Unknown Source)
        ... 16 more
Caused by: jakarta.enterprise.inject.AmbiguousResolutionException: Beans: [PRODUCER_METHOD bean [class=service.producer.ApiProducer, id=0u-Z3v-ex0P2-NgXll4A9eCH2ow], PRODUCER_METHOD bean [class=service.ApiProducer, id=9eslnypcwwFQYxpTIJsDaUk7ViI]]
        at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceSupplier(ArcContainerImpl.java:319)
        at io.quarkus.arc.runtime.BeanContainerImpl.beanInstanceFactory(BeanContainerImpl.java:31)
        at io.quarkus.resteasy.reactive.common.runtime.ArcBeanFactory.<init>(ArcBeanFactory.java:15)
        at io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveCommonRecorder.factory(ResteasyReactiveCommonRecorder.java:26)
        at io.quarkus.deployment.steps.ResteasyReactiveProcessor$setupEndpoints615463616.deploy_0(Unknown Source)
        at io.quarkus.deployment.steps.ResteasyReactiveProcessor$setupEndpoints615463616.deploy(Unknown Source)
        ... 17 more

An interesting thing is that I have ma ny API implementations and for each named bean with 2 names I have this error. Even though my rest lambda even doesn't use them. Generated ApplicationImpl seems to be containing all tehe beans injected somehow with @default qualifier.

Some debugging output in case it helps.

API provider injection is requested without any annotations, so a default one is injected

image

Then we have 2 resolved beans that have different names.

image

However the check in ArcContainerImpl::beanInstanceSupplier expects only 1 resolved bean

 if (resolvedBeans.size() > 1) {
            throw new AmbiguousResolutionException("Beans: " + resolvedBeans);
        } else {
            final InjectableBean<T> bean = resolvedBeans.size() != 1 ? null : (InjectableBean)resolvedBeans.iterator().next();
            return bean == null ? null : new Supplier<InstanceHandle<T>>() {
                public InstanceHandle<T> get() {
                    return ArcContainerImpl.beanInstanceHandle(bean, (CreationalContextImpl)null);
                }
            };
        }

Given the fact that generated injection point (not really sure why it appeared) has no qualifier. Arc behaves according to CDI 2.0 spec: https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#builtin_qualifiers. So we have 2 @default named beans.

As per CDI spec it's recommended to avoid @nAmed. I'll try to refactor the lib as I control it. But I guess it may cause compatibility issues for people depending on 3P libs.

Why is this injection point require? Is it possible to get rid of it? I'llc consume the beans later in my app. I don't understand why I need to initialize all the beans that are returned by Producer.

UPD. I solved the issue with a custom qualifier but will leave the ticket open in case you decide to get rid of the implicit @default injection point. Not sure whether it's done intentionally or not. At least some documentation/migration notes might be good for others as it can break existing dependencies.

I solved the issue with a custom qualifier that uses a Enum instead of the @nAmed string

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface RegionQualified {
    AmazonRegion value();
}

In my case I used runtime discovery DI.current().select(apiClass(), RegionQualifiedLiteral.of(region)).get(); So had to add all classess to Unremovable dependencies. In this specific case it's not clear for me why we need to inject the @default bean. But at least it helped to get rid of @nAmed qualifier that is not recommended.

It's interesting that with previous version it was enough to mark only "high level" beans as unremovable. Now I had to mark all of them. So the initialization seemed to be moved to runtime completely.

If there's a simpler/better option I'd be glad to learn. Thank you