CDI bean-type resolution issue when the bean class implements parameterized interfaces that also extend some parameterized interface

StackOverflow https://stackoverflow.com/questions/15582027

  •  29-03-2022
  •  | 
  •  

Frage

Given I lack the vocabulary to explain the issue, I show it by means of an example that reproduces the failure and helps to find the cause:

public interface BaseType<P> {}
public interface DerivedType<T> extends BaseType<T> {}

public interface SomeType1 {}
public interface SomeType2 {}

@Dependent
public static class BeanClass1 implements DerivedType<SomeType1> {}

@Dependent
public static class BeanClass2 implements DerivedType<SomeType2> {}

@ApplicationScoped
public static class Test implements Serializable {

    @Inject
    private BaseType<SomeType1> field; // if not commented throws following exception during deployment: org.jboss.weld.exceptions.DeploymentException: WELD-001409 Ambiguous dependencies for type [BaseType<SomeType1>] with qualifiers [@Default] at injection point [[field] @Inject private Test.field]. Possible dependencies [[Managed Bean [class BeanClass2] with qualifiers [@Any @Default], Managed Bean [class BeanClass1] with qualifiers [@Any @Default]]]

    @Inject
    private Instance<BaseType<SomeType1>> instance;

    @Inject
    private BeanManager bm;

    @PostConstruct
    private void postConstruct() {
        // This block outputs two bean classes and it should be only one:
        // beanClass: BeanClass1@11be5bab
        // beanClass: BeanClass2@764a72e9
        {
            Iterator<BaseType<SomeType1>> iterator = instance.iterator();
            while (iterator.hasNext()) {
                System.out.println("beanClass: " + iterator.next().toString());
        }
    }


        // And this block outputs:
        //
        // bean: Managed Bean [class BeanClass1] with qualifiers [@Any @Default]
        // beanTypes:
        //  - class BeanClass1
        //  - DerivedType<SomeType1>
        //  - class java.lang.Object
        //  - BaseType<T>
        //
        // bean: Managed Bean [class BeanClass2] with qualifiers [@Any @Default]
        // beanTypes:
        //  - DerivedType<SomeType2>
        //  - class java.lang.Object
        //  - class BeanClass2
        //  - BaseType<T>
        {
            Set<Bean<?>> beans = bm.getBeans(new ParameterizedTypeImpl(BaseType.class, new Type[] { SomeType1.class }, null));
            for (Bean<?> bean : beans) {
                System.out.println("\nbean: " + bean+"\nbeanTypes: ");
                for (Type beanType : bean.getTypes()) {
                    System.out.println(" - " + beanType);
                }
            }

            bm.resolve(beans); // if not commeted throws an AmbiguousResolutionException
        }
    }
}

The second block shows the set of bean types of the bean classes BeanClass1 and BeanClass2, according to Weld. There we found that the bean types set contains the type BaseType<T> instead of BaseType<SomeType1> or BaseType<SomeType2>.

So, the bean type of BeanClass2 corresponding to the indirect interface BaseType<P> that Weld remembers has the type variable T instead of the actual type parameter SomeType2. Therefore, and a according to the last point of this specification, BeanClass2 is wrongly considered assignable to BaseType<SomeType1>.

Is this the desired behavior or a bug? Is there a workaround? Was it fixed in a new Weld version.

The test was executed on a JBoss AS 7.1.1 which uses maven artifact org.jboss.as:jboss-as-weld:7.1.1

EDIT: I think the cause of this problem is not type erasure as the first answer suggest (it was deleted) but a bug in Weld. All the information required to generate the bean types is available during runtime. The bug is, I guess, when Weld generates the bean types of the bean class by means of reflection. The type variable resolution should be recursive and, apparently, it is not.

I am sure that the information required to generate the bean types of indirect interfaces is available during runtime because I made and tested a method that accomplishes this using a library - I made some years ago for a memory efficient java serializer - that fortunately has a function that does exactly what we need: generate a/get the Type instance for each ancestor of a java class, resolving type variables recursively. I tryed to put the involved methods down here but I had formating issues when pasting the code; and the fact that the library is long and poorly documented gave me reasons to give up.

To show, at least, the essence of how to resolve the type variables of indirect interfaces I wrote the following code. It works only for a specific class but can be generalized with some effort:

public static Set<Type> getActualInterfacesOfBeanClass1() {
    Set<Type> actualInterfaces = new HashSet<>();

    ParameterizedType directInterface = (ParameterizedType) BeanClass1.class.getGenericInterfaces()[0]; // = DerivedType<SomeType1>  ; assumes only one interface is extended
    actualInterfaces.add(directInterface);

    // because ParameterizedType doesn't have a method like Class.getGenericInterfaces(), we have to do it by hand
    Type indirectInterface; // = BaseType<SomeType1>
    {
        Class<?> directInterfaceRaw = (Class<?>) directInterface.getRawType(); // = DerivedType<T>
        Map<String,Type> resolutionMap = new HashMap<>(); // = { T -> SomeType1 }
        for( int i=0; i<directInterfaceRaw.getTypeParameters().length;++i) {
            resolutionMap.put(directInterfaceRaw.getTypeParameters()[i].getName(),directInterface.getActualTypeArguments()[i]);
        }

        ParameterizedType indirectInterfaceUnresolved = (ParameterizedType) directInterfaceRaw.getGenericInterfaces()[0]; // = BaseType<T> ;  assumes only one interface is extended
        ArrayList<Type> resolvedParameters = new ArrayList<>(); // = { SomeType1 }
        for(Type param : indirectInterfaceUnresolved.getActualTypeArguments()) {
            Type resolvedParam;
            if( param instanceof TypeVariable) {
                resolvedParam = resolutionMap.get(((TypeVariable<?>)param).getName());
            } else {
                resolvedParam = param;
            }
            resolvedParameters.add(resolvedParam);
        }
        indirectInterface = new ParameterizedTypeImpl(indirectInterfaceUnresolved.getRawType(), resolvedParameters.toArray(new Type[resolvedParameters.size()]),indirectInterfaceUnresolved.getOwnerType());
    }
    actualInterfaces.add(indirectInterface);

    return actualInterfaces;
}

I hope this avoids to believe that type erasure is the cause of this issue.

War es hilfreich?

Lösung 2

The least intrusive way I found out to workaround this bug is, for every parameterized bean type of a bean class C that should have the form T<A> but Weld is incorrectly changing the actual type parameter A with a type variable, create a new empty interface that extends T<A> and add this pilot interface to the implemented interfaces list of C and to any other bean class that indirectly extends T<A>.

In particular, for the code above, the bean type BaseType<SomeType1> of the bean class BeanClass1 is incorrectly changed by Weld to BaseType<T>. So, the pilot interface required to workaround the issue would be:

public interface PilotInterface1 extends BaseType<SomeType1> {}

And the bean class with the workaround applied would be:

@Dependent
public static class BeanClass1 implements DerivedType<SomeType1>, PilotInterface1 {}

I suggest putting all the pilot interfaces together in a separate package or file in order to easily delete them when the bug is fixed.

Andere Tipps

I think it's a limitation.

If I got this right, your goal is to make CDI work with generics by injecting and/or producing your generic types.

I did the same some time ago and found that this is a limitation of Java. As Java implements generics using type erasure, CDI can't properly deal with generic injection.

In the end, you found you have BaseType<T>, but due to this limitation CDI can only inject with concrete types, like BaseType<SomeType1>.

Check my case, which I think is a bit simpler but the same principle:

Interface

public interface Persistable implements Serializable {
    ... 
}

Doesn't work

@Named
@RequestScoped
public class MyBean<T extends Persistable> {

    @Inject
    private T model;
}

Works

@Named
@RequestScoped
public class MyBean<T extends Persistable> {

    @Inject
    private Persistable model;

}

Also check this post by the WELD/CDI spec lead.

So, no problem having parameterized beans, the problem is having, in the end, a generic type like your BaseType<T>.

If you could give them a concrete type, this should work as suggested in the post I've mentioned. Not ideal, but...

I hope it helps.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top