Affects: 5.x
See below test case:
import org.testng.annotations.Test;
import org.testng.Assert;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class NullBeanIssue {
@Test
public void test_shouldGetNull() {
var ctx = new AnnotationConfigApplicationContext();
ctx.register(MyFactoryBean.class);
ctx.refresh();
var myBean = ctx.getBean("myBean");
// expects null because MyFactoryBean.myBean returns null,
// however, I got a NullBean instance here
Assert.assertNull(myBean);
ctx.close();
}
}
class MyBean {
}
class MyFactoryBean {
@Bean
public MyBean myBean() {
return null; // in real project cases, return value might be null or not null that depends.
}
}
Comment From: quaff
Have you tried latest version? IIRC this is fixed long long ago.
Comment From: quaff
I can verify injection works fine but get bean doesn't work, tested with v6.0.8.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ContextConfiguration(classes = NullBeanTests.Config.class)
@ExtendWith(SpringExtension.class)
public class NullBeanTests {
@Autowired
private String myBean;
@Test
void testInjection() { // will pass
assertThat(myBean).isNull();
}
@Test
public void testGetBean() { // will fail
try (var ctx = new AnnotationConfigApplicationContext()) {
ctx.register(Config.class);
ctx.refresh();
assertThat(ctx.getBean("myBean")).isNull();
}
}
static class Config {
@Bean
public String myBean() {
return null;
}
}
}
Comment From: qiangyt
Have you tried latest version? IIRC this is fixed long long ago.
Not yet tried 6.x, but all 5.x have this issue.
Comment From: qiangyt
Injection works, but applicationContext.getBean() doesn't work. I updated the issue title.
Comment From: sbrannen
@qiangyt, thanks for reporting this.
@quaff, thanks for providing the autowiring/injection test case.
Interestingly, it turns out that autowiring does not consistently work either.
Here's a modified version of the combined tests which demonstrates that.
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitConfig(NullBeanTests.Config.class)
@TestMethodOrder(OrderAnnotation.class)
class NullBeanTests {
@Autowired
String myBean;
@Test
@Order(1)
void fieldInjection() {
assertThat(myBean).isNull();
}
@Test
@Order(2)
void parameterInjection(@Autowired String myBean) {
assertThat(myBean).isNull();
}
@Test
@Order(99)
void getBean() {
try (var ctx = new AnnotationConfigApplicationContext(Config.class)) {
Object bean = ctx.getBean("myBean");
System.err.println(bean != null ? bean.getClass().getName() : null);
assertThat(bean).isNull();
}
}
// @Configuration
static class Config {
@Bean
String myBean() {
return null;
}
}
}
getBean()
always fails.
If you run fieldInjection()
or parameterInjection()
by itself, each will pass.
If you run the NullBeanTests
above unmodified, fieldInjection()
will pass but only because it runs first. The other two test methods fail.
If you change the @Order
of fieldInjection()
to 10, parameterInjection()
will pass and fieldInjection()
will fail.
If you change the @Order
of getBean()
to 0, all three tests will fail.
If you only change the NullBeanTests
above so that String myBean
field is annotated with @Autowired(required = false)
, then fieldInjection()
and parameterInjection()
will both pass.
So, there is something interesting going on here, and it's definitely a bug (and possibly a regression).
We'll look into it.
Comment From: jhoeller
In terms of intended behavior, @Autowired
should consistently resolve to null
wherever necessary (so there may indeed be some subtle bug here) but getBean
calls are actually designed to return a NullBean
instance as of 5.0, whereas the behavior was strictly speaking undefined before. We recommend avoiding null beans completely, but if you keep returning null from a factory method, that's the container behavior that you'll get as of 5.0. This has been the case for more than five years already.
So for getBean
calls, we do not support null
return values there anymore, whereas we do support resolving injection points such as @Autowired
to null
. If you'd like to programmatically test for a NullBean
instance received from getBean
, you may call .equals(null)
on it, but you're not going to receive a plain null
value from getBean
directly anymore.
Comment From: sbrannen
getBean
calls are actually designed to return aNullBean
instance as of 5.0
I knew we used NullBean
internally and that we sometimes adapt it to null
, but I don't know if I ever knew that we literally return NullBean
instances to the application.
Is that documented anywhere?
Comment From: qiangyt
NullBean is an internal class (the class is not public/protected), it is a bit strange to be the result of ApplicationContext.getBean() API.
Comment From: qiangyt
sorry for making mistake of close/re-open the issue.
Comment From: qiangyt
Another case is, for a factory method that returns null, ApplicationContext.getBean("myBean", MyBean.class) will raise a BeanNotOfRequiredTypeException with a message "Bean named 'myBean' is expected to be of type 'org.springframework.beans.factory.support.MyBean' but was actually of type 'org.springframework.beans.factory.support.NullBean", I feel it strange for users.
Comment From: jhoeller
Good point, Sam, I'm afraid it might not be fully documented.
In this view, a "bean" is never null
. Only an injection point can resolve to null
, or rather remain unresolved and therefore evaluate to null
. This also gives strict non-null guarantees to getBean
callers, not forcing them to apply defensive null checks, which includes bean post-processors and other infrastructure components.
Stubbing out a bean instance with null
can only happen from factory methods and is arguably not great practice to begin with. We leniently tolerate it as an indicator for an optional bean and that's ok. However, use an @Autowired
injection point or BeanFactory.getBeanProvider
to resolve such beans, not hard getBean
calls.
As for NullBean
being an internal type, that's a bit unusual indeed. However, there is nothing to do with the type in any case. It's a stub with .equals(null)
behavior and a "null" toString
output that does not fail with an NPE when doing checks against it. Making it public would not provide practical value beyond that.
Comment From: jhoeller
This turns out to be a bug in shortcut resolution for cached field/method arguments. We're replacing the full dependency resolution algorithm with a getBean
call there which does not leniently resolve to null
as the original resolution algorithm would.
In addition, I'll also add a few documentation notes towards not expecting null
from getBean
calls, hinting at getBeanProvider
usage instead.