2

I don't understand how it is possible to see exception messages like this one

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.util.List<java.lang.String>' available

Aren't type arguments supposed to be erased by runtime? How can things like <java.lang.String> survive compilation (as we know, exceptions occur during runtime)? What kind of supernatural ability does Spring possess?

This question is similar, but none of the two most-upvoted answers answer my question exactly

  • GhostCat again on strike says the fact that a class is generic (not what its actual type is) is preserved, can be retrieved
  • yshavit says you can obtain a superclass's generic type through reflexion
M. Justin
  • 14,487
  • 7
  • 91
  • 130
JoreJoh
  • 113
  • 5

2 Answers2

3

As discussed in a answer to Stack Overflow question Why is this generic type information for member field not erased in Java?, generic type information of fields is reflectively available in Java. This is also true of method/constructor parameters. This explains how Spring can know that a particular generic type is required.

Additionally, bean definitions are often done via concrete classes or bean methods. Both of these cases retain their type information at compile time. This explains how Spring can know what the specific generic type of a bean is.

Putting these two together explains how Spring is able to fail when no bean matching a specific generic signature exists.

Example

To make it concrete, I'm going to give an example. Let's say we have the following generic class:

public class GenericBean<T> {
}

Here are two bean definitions, one which is defined as a bean by using the @Service annotation on a subclass, and one by using @Bean within a Configuration class:

@Service
public class GenericBeanService extends GenericBean<Integer> {
}
@Configuration
public class GenericBeanConfig {
    @Bean
    public GenericBean<String> genericBean() {
        return new GenericBean<>();
    }
}

In both of these cases, the generic type information of these beans is available at runtime using reflection. This means that Spring can use reflection to determine the specific generic types of the beans:

// GenericBean<String>
GenericBeanConfig.class.getMethod("genericBean").getGenericReturnType();

// GenericBean<Integer>
GenericBeanService.class.getGenericSuperclass();

Here is an autowired class that uses the generic beans:

@Service
public class AutowiredClass {
    @Autowired private GenericBean<String> stringBean;
    @Autowired private GenericBean<Integer> integerBean;
}

Here too, the generic type information of the autowired fields is available at runtime using reflection. This means that Spring can use reflection to determine the specific generic types of the beans:

// GenericBean<String>
AutowiredClass.class.getDeclaredField("stringBean").getGenericType()

// GenericBean<Integer>
AutowiredClass.class.getDeclaredField("integerBean").getGenericType()

Since Spring can determine via reflection the generic types of the beans, and the types of the autowired properties, it can therefore correctly assign beans based on their generics.

Unmitigated
  • 76,500
  • 11
  • 62
  • 80
M. Justin
  • 14,487
  • 7
  • 91
  • 130
  • Thank you! Can you expand on: *"Additionally, bean definitions are often done via concrete classes or bean methods. Both of these cases retain their type information at compile time."* – JoreJoh Jul 19 '23 at 22:20
  • 1
    @JoreJoh I've added concrete examples, which I hope suffice. – M. Justin Jul 20 '23 at 04:53
  • Concrete examples are always superior, good job – Vasily Liaskovsky Jul 20 '23 at 14:36
  • Thanks. What does "type erasure" refer to then? Does it refer to the fact that a generic class itself never retains the information of its actual type at runtime, but every `java.lang.reflect` class around it (super `Class`, `Method`, `Field`) does keep it in its bytecode? – JoreJoh Jul 22 '23 at 09:33
  • The primary way type erasure is visible is when using a generic class. For instance, in a method: `List list = new ArrayList<>(); list.add("ABC"); String s = list.get(0);`. Despite being known at compile time that `list` has `String` as its parametric type, that information is not retained in the class. Looking at the bytecode, for instance, would show raw types, equivalent to: `List list = new ArrayList(); list.add("ABC"); String s = (String) list.get(0);`. There's no way, at runtime, to tell that `list` was compiled as a `List`. – M. Justin Jul 23 '23 at 12:55
1

Aren't type arguments supposed to be erased by runtime?

It is correct for class instances, but not for classes themselves. Classes keep information about their type parameters as well as their fields and methods. For example, in this code

    void foo(){
      List<String> list = new ArrayList<String>();
      Class<?> listClass = list.getClass();
    }

list instance is not aware at runtime of its actual type parameter value String, but its class ArrayList (and interface List) is aware of owning a type parameter, declared as E or T (though actually type parameter name might still be erased, only bounds matter). It's fact of "projection" of type parameter into actual type String is erased, not the fact of existence of type param itself.

In same manner, declared class fields and methods also keep their type parameters, either actual or type variables. Even better in this case, as actual parameter values are stored. So, when you write something like this

    // inside Spring component
    @Autowired
    private List<String> listOfStrings;

Spring is able to detect that there is a field named listOfStrings that needs to be autowired and it expects something compatible with List<String>. So, Spring is totally able to determine what is expected on the consuming end.

On the other end, you normally register beans with @Bean or @Component annotations (or derived). And again, these are attached to methods or classes that do keep their type information.

And even when beans are added programmatically, there are still options to provide type information explicitly through GenericApplicationContext.registerBean(), BeanDefinition and ResolvableType.

Ultimately, on both ends there are ways to provide type information, and Spring does great job to connecting bean providers and consumers.

Vasily Liaskovsky
  • 2,248
  • 1
  • 17
  • 32
  • It's a bit confusing to me. In summary, `SomeGenericClass` doesn't keep information about its actual type at runtime, but `Field` does. Is it correct? – JoreJoh Jul 19 '23 at 22:24
  • Both parts incorrect. It's easy to confuse type variable *declaration* and type parameter. As well as class instance and class itself, represented by instance of `Class` class. `SomeGenericClass` *does* keep information about its type variable declaration - it's `T`, but its instance *does not* keep information about class that happens to be in place of `T`. This is essentially what is called *type erasure*. – Vasily Liaskovsky Jul 20 '23 at 00:00
  • `Field` instances do not hold any information about themselves but about fields of some other class. And that information is just normal "hard" values, but indeed can describe generic nature of the represented field. See `Field.getGenericType()` and subclasses of `java.lang.reflect.Type` interface. – Vasily Liaskovsky Jul 20 '23 at 00:02