Search code examples
javaspringgenericsdependency-injectionspring-4

Spring 4 not automatically qualifying generic types on autowire


PROBLEM HAS BEEN IDENTIFIED, POST UPDATED (Scroll to bottom)

I am developing a desktop application currently using Spring (spring-context, 4.1.6.RELEASE) for IoC and dependency injection. I am using an annotation configuration, using @ComponentScan. The issue I am experiencing is supposed to be implemented as a feature in 4.X.X, as it states here and here, but I am getting the old 3.X.X exception.

I have a parameterised interface that represents a generic repository:

public interface DomainRepository<T> {

    T add(T entity) throws ServiceException, IllegalArgumentException;

    // ...etc

}

I then have two concrete implementations of this, ChunkRepositoryImpl and ProjectRepositoryImpl, which are parameterised accordingly. They share some common implementation from an abstract class, but are declared like so:

@Repository
public class ChunkRepositoryImpl extends AbstractRepositoryImpl<Chunk> implements DomainRepository<Chunk> {

    // ...+ various method implementations

}

@Repository
public class ProjectRepositoryImpl extends AbstractRepositoryImpl<Project> implements DomainRepository<Project> {

    // ...+ various method implementations

}

My understanding of the above links leads me to believe that I should be able to autowire these without needing to manually specify the beans via @Qualifier. However, when I do so:

@Autowired
private DomainRepository<Project> repository;

I get the following exception (preceded by a long stack trace of course):

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.foo.bar.repositories.DomainRepository] is defined: expected single matching bean but found 2: chunkRepositoryImpl,projectRepositoryImpl

Can anybody shine a light as to why this might be happening? I would expect this exception in 3.X.X, but it should not happen in 4.X.X. What is the difference between my situation, and the one described here?

UPDATE

I have discovered the source of the problem. One of the methods in my DomainRepository<T> interface is marked as @Async, and makes use of Spring's asynchronous capabilities. Removing this means that the beans are correctly qualified. I hypothesize that Spring transforms classes with @Async methods under the hood into some other class, and this process strips the type information, meaning that it can't tell the beans apart.

This means I now have two questions:

  1. Is this intended behaviour?
  2. Can anybody suggest a workaround?

Here is a project demonstrating the problem. Simply remove the @Async annotation from the DomainRepository<T> interface, and the problem dissappears.


Solution

  • I hypothesize that Spring transforms classes with @Async methods under the hood into some other class, and this process strips the type information, meaning that it can't tell the beans apart.

    Yes. That's exactly what happens.

    Spring 4 supports injecting beans by their full generic signature. Given the injection target

    @Autowired
    private DomainRepository<Project> repository;
    

    and a bean of type ProjectRepositoryImpl, Spring will properly resolve and inject that bean into the field (or method argument, or constructor argument).

    However, in your code, you don't actually have a bean of type ProjectRepositoryImpl, not even of type DomainRepository<Project>. You actually have a bean of type java.lang.Proxy (actually a dynamic subclass of it) that implements DomainRepository, org.springframework.aop.SpringProxy, and org.springframework.aop.framework.Advised.

    With @Async, Spring needs to proxy your bean to add the asynchronous dispatching behavior. This proxy, by default, is a JDK proxy. JDK proxies can only inherit the interfaces of the target type. JDK proxies are produced with the factory method Proxy#newProxyInstance(...). Notice how it only accepts Class arguments, not Type. So it can only receive a type descriptor for DomainRepository, not for DomainRepository<Chunk>.

    Therefore, you have no bean that implements your parameterized target type DocumentRepository<Project>. Spring will fall back to the raw type DocumentRepository and find two candidate beans. It's an ambiguous match so it fails.

    The solution is to use CGLIB proxies with

    @EnableAsync(proxyTargetClass = true)
    

    CGLIB proxies allow Spring to get the full type information, not just interfaces. So your proxy will actually have a type that is a subtype of ProjectRepositoryImpl, for example, which carries with it the DocumentRepository<Project> type information.


    A lot of the above are implementation details and defined in many separate places, official documentation, javadoc, comments, etc. Use them carefully.