How is Spring able to see type arguments at runtime? What about the whole type erasure thing?

130 Views Asked by At

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
2

There are 2 best solutions below

5
M. Justin On BEST ANSWER

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.

3
Vasily Liaskovsky On

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.