Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Injecting beans from auto-configuration by their actual type fails #32763

Closed
maciejwalkowiak opened this issue Oct 18, 2022 · 5 comments
Closed
Labels
status: invalid An issue that we don't feel is valid

Comments

@maciejwalkowiak
Copy link
Contributor

maciejwalkowiak commented Oct 18, 2022

Lets assume following auto-configuration class:

@AutoConfiguration
public class PersonAutoConfiguration {

    @Bean
    PersonService personService() {
        return new PersonServiceImpl();
    }
}

Injecting personService bean by PersonServiceImpl type:

@Component
public class MyComponent {
    private final PersonServiceImpl personService;

    MyComponent(PersonServiceImpl personService) {
        this.personService = personService;
    }
}

fails with:

No qualifying bean of type '...PersonServiceImpl' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

Injecting through an interface type works without problems. Also, injecting by actual type (PersonServiceImpl) works for test classes and applicationContext.getBean(PersonServiceImpl.class) returns an actual bean.

As far as I can see only beans created through auto-configurations are affected. Beans created through regular configurations are injectable through their interfaces and actual classes.

This behaviour has not been observed in Spring Boot 2.7.4 but is reproducible from Spring Boot 3.0.0-M3 (I am not able to run build with older milestones due to missing dependencies).

Sample project that reproduces this issue coming soon.

Sample project that reproduces this issue: https://github.com/maciej-scratches/spring-boot-gh-32763 (./mvnw verify to see the failing test).

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 18, 2022
@wilkinsona
Copy link
Member

wilkinsona commented Oct 18, 2022

As far as I know, it has always been possible for this to happen.

Your @Bean method is hiding the type of the personService bean from the bean factory. Up until the point that the bean is created, the bean factory will only know that it's a PersonService and injection of a PersonServiceImpl will fail. Once the personService bean has been created, the bean factory learns that it is, in fact, a PersonServiceImpl and injection of a PersonServiceImpl will then succeed. In other words if the personService bean is created before MyComponent, it will work. If MyComponent is created first it will fail.

It's possible that a change somewhere (I suspect Spring Framework to be the most likely place) has made it more likely that MyComponent is created first and for the failure to occur. However, you shouldn't rely on a particular ordering and should instead make sure that the signature of your @Bean method uses the most-specific type possible for its return type. There is a tip about this in Boot's reference documentation:

When declaring a @Bean method, provide as much type information as possible in the method’s return type. For example, if your bean’s concrete class implements an interface the bean method’s return type should be the concrete class and not the interface. Providing as much type information as possible in @Bean methods is particularly important when using bean conditions as their evaluation can only rely upon to type information that is available in the method signature.

@wilkinsona wilkinsona closed this as not planned Won't fix, can't repro, duplicate, stale Oct 18, 2022
@wilkinsona wilkinsona added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 18, 2022
@maciejwalkowiak
Copy link
Contributor Author

TIL. Thanks! It never encountered this problem before and it happened when upgrading to Spring Boot 3.0.

Does this advice relates only to beans created in auto-configurations? Because code samples where the interface is a return type are even in Spring Framework docs (https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java-basic-concepts).

Btw other Spring projects in few places make the same mistake as I did. For example:

@wilkinsona
Copy link
Member

wilkinsona commented Oct 18, 2022

It's particularly important for auto-configuration and bean conditions as the conditions are evaluated before beans are created. That means that the only type information that's available is from the signature of the @Bean method. This is really a separate problem to the dependency injection failure that you had.

In terms of dependency injection, the problem can occur in any Spring app. It depends entirely on the order in which the beans are created. Without explicit or implicit dependencies between beans, the ordering isn't really guaranteed and the problem can occur if the beans are created in the "wrong" order.

Thanks for spotting the problem with KafkaAutoConfiguration. I've opened #32766.

Because code samples where the interface is a return type are even in Spring Framework docs

Here's an example from the Framework docs:

@Configuration
public class AppConfig {

   @Bean
   public MyService myService() {
       return new MyServiceImpl();
   }
}

This isn't necessarily wrong. It depends on whether or not you want to be able to inject MyServiceImpl or only ever inject MyService. That said, using MyServiceImpl as the return type won't do any harm and, generally speaking, it's what I would recommend. There are a few rare edge cases, such as if MyServiceImpl is private and you're using AOT/Native.

From the Framework docs again:

The preceding AppConfig class is equivalent to the following Spring XML:

<beans>
   <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

Arguably, this is wrong. In the XML case, the bean factory will know that the bean's type is com.acme.services.MyServiceImpl from the outset. I've opened spring-projects/spring-framework#29338.

@maciejwalkowiak
Copy link
Contributor Author

Thanks a lot @wilkinsona for in-depth explanation!

@snicoll
Copy link
Member

snicoll commented Oct 18, 2022

One more note on AOT/Native. If you don't expose the type in the type signature, then it is impossible for the AOT engine to introspect the actual type of the bean at build-time. This may or may not be a problem depending on the feature you're using. For instance, if you have a close method on the target implementation, we won't be able to register it and infer the reflection hint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

4 participants